Compare commits
116 Commits
8.14.0.162
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972078533b | ||
|
|
1eb8b48bb4 | ||
|
|
221043a998 | ||
|
|
3914c811be | ||
|
|
c4b6b61da7 | ||
|
|
aa353b3f59 | ||
|
|
ee158d4c4c | ||
|
|
459643fd14 | ||
|
|
3b1636e179 | ||
|
|
cad4022e68 | ||
|
|
d12641a3a2 | ||
|
|
f995e51b28 | ||
|
|
213cbcd9ad | ||
|
|
f5892e0aad | ||
|
|
b139b7c7b9 | ||
|
|
3e6dc35321 | ||
|
|
30431a6354 | ||
|
|
6be8862bdb | ||
|
|
2182e5952a | ||
|
|
800a5cc0bc | ||
|
|
82ce1ead86 | ||
|
|
d10259eae1 | ||
|
|
1a9a0dbdd9 | ||
|
|
1b8e0a0c93 | ||
|
|
cef72e5a5a | ||
|
|
eb533a72a2 | ||
|
|
08a3e32943 | ||
|
|
b957357516 | ||
|
|
c38b1309dd | ||
|
|
926432d03a | ||
|
|
6628b9f6fc | ||
|
|
1954342a36 | ||
|
|
f40bc944ae | ||
|
|
feeb1303e5 | ||
|
|
a6e8eda73c | ||
|
|
a661667b24 | ||
|
|
a978b4cc8b | ||
|
|
15ada8dcf0 | ||
|
|
15f9d3dc96 | ||
|
|
fc5102cd54 | ||
|
|
16c179115e | ||
|
|
d28e29fa21 | ||
|
|
265757716a | ||
|
|
0f0c3e6fc6 | ||
|
|
8a53464a41 | ||
|
|
79bbd556a4 | ||
|
|
cfb22a38b3 | ||
|
|
808f3218db | ||
|
|
280fc1f244 | ||
|
|
78130adac7 | ||
|
|
feba86dbfb | ||
|
|
202d8a1f07 | ||
|
|
c6492caae7 | ||
|
|
a82216e06c | ||
|
|
a173d4599a | ||
|
|
6f5bc03b96 | ||
|
|
ba15734132 | ||
|
|
5a57831b26 | ||
|
|
31867c8d06 | ||
|
|
08ae6b3e07 | ||
|
|
28e9247793 | ||
|
|
8b1379149c | ||
|
|
0cc18a5285 | ||
|
|
f64e718ba2 | ||
|
|
185035784c | ||
|
|
dcf02125a0 | ||
|
|
44e6c6cb43 | ||
|
|
dc3a819024 | ||
|
|
c0cedd0026 | ||
|
|
27439824e7 | ||
|
|
c7005df406 | ||
|
|
4caec2f2d3 | ||
|
|
7dded9229a | ||
|
|
aa7bced824 | ||
|
|
8663b50018 | ||
|
|
2259a151d9 | ||
|
|
08bf2bb9e5 | ||
|
|
0206e8c487 | ||
|
|
39780d4bc7 | ||
|
|
49311ef328 | ||
|
|
08371f4c50 | ||
|
|
0d76c69ec1 | ||
|
|
e80b3d8bdb | ||
|
|
79122a2301 | ||
|
|
8f60728454 | ||
|
|
65f577efed | ||
|
|
8f0c315ad7 | ||
|
|
7fd03d6bd2 | ||
|
|
102b164f89 | ||
|
|
c57f731c67 | ||
|
|
8b613f8bc1 | ||
|
|
a178545e1e | ||
|
|
7dde1505d7 | ||
|
|
6d78ec66e1 | ||
|
|
832f2b06eb | ||
|
|
2260eb9b8f | ||
|
|
d535e8bfe2 | ||
|
|
5ba733f275 | ||
|
|
3fafbe67bc | ||
|
|
52f5b28db3 | ||
|
|
0bc63e4b57 | ||
|
|
344081c1b6 | ||
|
|
9e4e2976c6 | ||
|
|
892b51221a | ||
|
|
fa6876eefa | ||
|
|
79fc5037a3 | ||
|
|
0088304b35 | ||
|
|
e4b9550f31 | ||
|
|
7856a0f0e1 | ||
|
|
e0b88ecf86 | ||
|
|
507959b305 | ||
|
|
aa059ff975 | ||
|
|
3ac71942a7 | ||
|
|
28b9200637 | ||
|
|
1c5dfb2e71 | ||
|
|
2cd69719c7 |
4
Podfile
4
Podfile
@ -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'] = 'e3b89de2afc950c9e317f2fff426ae8edc77a397520d2e0afbb717d738213fd5'
|
||||
pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal.git', tag: 'v0.94.1', testspecs: ["Tests"]
|
||||
ENV['LIBSIGNAL_FFI_PREBUILD_CHECKSUM'] = '79f53932ff82f792b70e30bad3b38801da0b882137adaf65ad54d907a94f3d29'
|
||||
pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal.git', tag: 'v0.95.0', testspecs: ["Tests"]
|
||||
# pod 'LibSignalClient', path: '../libsignal', testspecs: ["Tests"]
|
||||
|
||||
ENV['RINGRTC_PREBUILD_CHECKSUM'] = 'c19c813ab5255aa3cd7c2af36374100f7cc69c2fd794cae23baebd6ec9dae90c'
|
||||
|
||||
16
Podfile.lock
16
Podfile.lock
@ -9,8 +9,8 @@ PODS:
|
||||
- LibMobileCoin/CoreHTTP (6.0.2):
|
||||
- SwiftProtobuf (~> 1.5)
|
||||
- libPhoneNumber-iOS (1.2.0)
|
||||
- LibSignalClient (0.94.1)
|
||||
- LibSignalClient/Tests (0.94.1)
|
||||
- LibSignalClient (0.95.0)
|
||||
- LibSignalClient/Tests (0.95.0)
|
||||
- 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.94.1`)
|
||||
- LibSignalClient/Tests (from `https://github.com/signalapp/libsignal.git`, tag `v0.94.1`)
|
||||
- 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`)
|
||||
- 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.94.1
|
||||
:tag: v0.95.0
|
||||
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.94.1
|
||||
:tag: v0.95.0
|
||||
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: cf53cea3c6cd2cac3e87d0f5f34c3a1c59fe1b8f
|
||||
LibSignalClient: a98db1d538243e43ecac040005204bd274cbd8c7
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418
|
||||
@ -143,6 +143,6 @@ SPEC CHECKSUMS:
|
||||
SQLCipher: ff2f045b20d675a73a70f7329395ddd4a2580063
|
||||
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
|
||||
|
||||
PODFILE CHECKSUM: cf592eb2b2ccbf3e467f82142ef4d4096e132343
|
||||
PODFILE CHECKSUM: ee98007764e1569e9dbe4f25053510725b19fc88
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
2
Pods
2
Pods
@ -1 +1 @@
|
||||
Subproject commit 2f7bce71b0b302c4961c940606b79b6f32bfb8d0
|
||||
Subproject commit 5e81462d833ad24e8091d7b6ab675c2cdc94af54
|
||||
@ -1,76 +1,27 @@
|
||||
{
|
||||
"#comment": "NOTE: This file is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run `sds_codegen.sh`.",
|
||||
"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,
|
||||
"#max": 80,
|
||||
"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,
|
||||
"TSPaymentModel": 67,
|
||||
"TSPaymentRequestModel": 66,
|
||||
"TSPrivateStoryThread": 72,
|
||||
"TSRecipientReadReceipt": 12,
|
||||
"TSThread": 2,
|
||||
"TSUnreadIndicatorInteraction": 4,
|
||||
"TestModel": 59
|
||||
"TSUnreadIndicatorInteraction": 4
|
||||
}
|
||||
@ -2440,31 +2440,23 @@ 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):
|
||||
record_type_map_filepath = 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)
|
||||
|
||||
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)
|
||||
max_record_type = old_record_types.get("#max", 0)
|
||||
|
||||
for clazz in global_class_map.values():
|
||||
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
|
||||
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
|
||||
|
||||
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__),)
|
||||
@ -2472,7 +2464,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_map_filepath, json_string)
|
||||
sds_common.write_text_file_if_changed(record_type_json_path, json_string)
|
||||
|
||||
# TODO: We'll need to import SignalServiceKit for non-SSK classes.
|
||||
|
||||
|
||||
@ -77,7 +77,6 @@
|
||||
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 */; };
|
||||
@ -93,7 +92,6 @@
|
||||
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 */; };
|
||||
@ -726,7 +724,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 /* PreKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKey.swift */; };
|
||||
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKeyRecord.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 */; };
|
||||
@ -858,6 +856,7 @@
|
||||
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 */; };
|
||||
@ -1064,7 +1063,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 */; };
|
||||
@ -1080,7 +1079,6 @@
|
||||
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 */; };
|
||||
@ -1164,7 +1162,6 @@
|
||||
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 */; };
|
||||
@ -1454,8 +1451,6 @@
|
||||
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 */; };
|
||||
@ -1678,7 +1673,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 /* IntroducingPINs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINs.swift */; };
|
||||
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.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 */; };
|
||||
@ -2706,6 +2701,7 @@
|
||||
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 */; };
|
||||
@ -2746,6 +2742,7 @@
|
||||
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 */; };
|
||||
@ -2885,8 +2882,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 */; };
|
||||
@ -2901,6 +2898,7 @@
|
||||
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 */; };
|
||||
@ -3826,7 +3824,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 /* RemoteAttestation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */; };
|
||||
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.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 */; };
|
||||
@ -3965,7 +3963,6 @@
|
||||
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 */; };
|
||||
@ -4170,7 +4167,6 @@
|
||||
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>"; };
|
||||
@ -4228,7 +4224,6 @@
|
||||
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>"; };
|
||||
@ -4995,7 +4990,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 /* PreKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKey.swift; sourceTree = "<group>"; };
|
||||
50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyRecord.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>"; };
|
||||
@ -5127,6 +5122,7 @@
|
||||
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>"; };
|
||||
@ -5339,7 +5335,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>"; };
|
||||
@ -5355,7 +5351,6 @@
|
||||
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>"; };
|
||||
@ -5441,7 +5436,6 @@
|
||||
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>"; };
|
||||
@ -5659,8 +5653,6 @@
|
||||
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>"; };
|
||||
@ -5929,7 +5921,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 /* IntroducingPINs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINs.swift; sourceTree = "<group>"; };
|
||||
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINsMegaphone.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>"; };
|
||||
@ -6988,6 +6980,7 @@
|
||||
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>"; };
|
||||
@ -7031,6 +7024,7 @@
|
||||
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>"; };
|
||||
@ -7174,9 +7168,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>"; };
|
||||
@ -7191,6 +7185,7 @@
|
||||
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>"; };
|
||||
@ -8145,7 +8140,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 /* RemoteAttestation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestation.swift; sourceTree = "<group>"; };
|
||||
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestationAuthFetcher.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>"; };
|
||||
@ -8285,7 +8280,6 @@
|
||||
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>"; };
|
||||
@ -9906,6 +9900,22 @@
|
||||
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 = (
|
||||
@ -10301,20 +10311,6 @@
|
||||
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 = (
|
||||
@ -11460,7 +11456,7 @@
|
||||
D9C2D77F299EC11400D79715 /* CreateUsernameMegaphone.swift */,
|
||||
D9C0AE682BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift */,
|
||||
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */,
|
||||
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
|
||||
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */,
|
||||
8837F74023DA0B0F00772A32 /* MegaphoneView.swift */,
|
||||
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */,
|
||||
8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */,
|
||||
@ -13347,8 +13343,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */,
|
||||
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */,
|
||||
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */,
|
||||
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */,
|
||||
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */,
|
||||
D98DD85E28EE53B00089333E /* RemoteMegaphoneModel.swift */,
|
||||
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */,
|
||||
@ -13526,6 +13522,8 @@
|
||||
D97C9FF12DD3FB7200191CE2 /* BackupDisablingManager.swift */,
|
||||
D93FA5BE2DE77E440013879E /* BackupEnablingManager.swift */,
|
||||
D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */,
|
||||
D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */,
|
||||
D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */,
|
||||
D98CA2B22DF2450E0060370E /* BackupRecordKeyViewController.swift */,
|
||||
04E66D412DFF3A3E0059DBAC /* BackupRecoveryKeyReminderCoordinator.swift */,
|
||||
04B975452E43A4AA00E20364 /* BackupRefreshManager.swift */,
|
||||
@ -13912,8 +13910,6 @@
|
||||
children = (
|
||||
D90AA32E2CC9616A00021CB0 /* Signal-Message-Backup-Tests */,
|
||||
D90AA6182CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift */,
|
||||
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */,
|
||||
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */,
|
||||
04E66D432E00AB3A0059DBAC /* BackupSettingsStoreTests.swift */,
|
||||
D9A36B922C7FEDA100CEC0E7 /* LineByLineStringDiff.swift */,
|
||||
);
|
||||
@ -14583,7 +14579,6 @@
|
||||
F900F2DC27F25AB300431E09 /* DonationReceiptViewController.swift */,
|
||||
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */,
|
||||
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */,
|
||||
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */,
|
||||
);
|
||||
path = Donations;
|
||||
sourceTree = "<group>";
|
||||
@ -14595,6 +14590,7 @@
|
||||
D92EFDEB2F68EB7D0031D257 /* AttachmentBackfill */,
|
||||
7255A4C32B98D5A800E95368 /* Attachments */,
|
||||
720547F12B9C8F5E00E2CF2F /* Avatars */,
|
||||
F9C5CA52289453B100548EEE /* Axolotl */,
|
||||
665C0D5A2ADF537000539A37 /* Backups */,
|
||||
F945FE482984795A00C835C7 /* Calls */,
|
||||
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
|
||||
@ -14628,7 +14624,6 @@
|
||||
046092252FBCD28300A8765F /* SafetyTips */,
|
||||
50B791552E8B39230063E71E /* Search */,
|
||||
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
|
||||
F9C5CB98289453B200548EEE /* Security */,
|
||||
F9C5CAB4289453B200548EEE /* Spam */,
|
||||
F9C5CA2F289453B100548EEE /* Storage */,
|
||||
88E34F2522F269B600966CC2 /* StorageService */,
|
||||
@ -14656,6 +14651,7 @@
|
||||
F94261FF289B1B5400460798 /* Account */,
|
||||
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
|
||||
50ED28002F0EDAFB00E57C54 /* Attachments */,
|
||||
50DAF7E12FD87BFD00BE7430 /* Backups */,
|
||||
F945FE4B298481D800C835C7 /* Calls */,
|
||||
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
|
||||
50E0198E2CC2491A0063EA48 /* Concurrency */,
|
||||
@ -14744,6 +14740,7 @@
|
||||
F9C5C950289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage+SDS.swift */,
|
||||
F9C5C997289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.h */,
|
||||
F9C5C958289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.m */,
|
||||
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */,
|
||||
F9C5C93B289453B100548EEE /* OWSIdentityManager.swift */,
|
||||
F9C5C983289453B100548EEE /* OWSMessageDecrypter.swift */,
|
||||
F9C5C973289453B100548EEE /* OWSMessageSend.swift */,
|
||||
@ -15029,7 +15026,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6646572F2AC369EB0099DE1C /* PhoneNumberDiscoverabilityManager */,
|
||||
6659A0242A7C112700066AB7 /* PreKeys */,
|
||||
661170BF2ABA458800A1B16D /* TSAccountManager */,
|
||||
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
|
||||
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
|
||||
@ -15040,7 +15036,6 @@
|
||||
D9F399B12A96D65D001599EC /* IdentityKeyMismatchManager.swift */,
|
||||
5033D46629D76BD0007FEADA /* LocalIdentifiers.swift */,
|
||||
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
|
||||
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */,
|
||||
72552EF32C9EF9E7008614AF /* OWSIdentity.swift */,
|
||||
D9CAF74F2A0ACFF20049193A /* PniDistributionParameterBuilder.swift */,
|
||||
C18E3C712A9FF65D003D1CF1 /* PniDistributionSyncMessage.swift */,
|
||||
@ -15169,7 +15164,6 @@
|
||||
F9C5CA2F289453B100548EEE /* Storage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9C5CA52289453B100548EEE /* AxolotlStore */,
|
||||
F9C5CA31289453B100548EEE /* Database */,
|
||||
667DEE562BC7148E00EFF32D /* MediaGallery */,
|
||||
F9C5CA9B289453B100548EEE /* BaseModel.h */,
|
||||
@ -15238,32 +15232,33 @@
|
||||
path = Snapshots;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F9C5CA52289453B100548EEE /* AxolotlStore */ = {
|
||||
F9C5CA52289453B100548EEE /* Axolotl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9C5CA5F289453B100548EEE /* Model */,
|
||||
667664352A43BBCD00716B84 /* CombinedFingerprints.swift */,
|
||||
50A156C62FA11AA8008FE086 /* Fingerprint.swift */,
|
||||
C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */,
|
||||
504F98B02EAFFAC600DF465B /* KyberPreKeyUseRecord.swift */,
|
||||
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
|
||||
F9C5CA59289453B100548EEE /* OldSenderKeyStore.swift */,
|
||||
50589CDF2E8C4AD5003EF42A /* PreKey.swift */,
|
||||
F9C5CB9F289453B200548EEE /* OWSRecipientIdentity.swift */,
|
||||
F9C5CB99289453B200548EEE /* OWSVerificationState.h */,
|
||||
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.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 = AxolotlStore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F9C5CA5F289453B100548EEE /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.swift */,
|
||||
72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */,
|
||||
72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */,
|
||||
);
|
||||
path = Model;
|
||||
path = Axolotl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F9C5CA85289453B100548EEE /* JobRecords */ = {
|
||||
@ -15312,10 +15307,12 @@
|
||||
F9C5CAD3289453B200548EEE /* API */,
|
||||
88DF819328E112F600F8BA80 /* SignalProxy */,
|
||||
669E8FE528B4149200043D28 /* BaseOWSURLSessionMock.swift */,
|
||||
727328062CA6CF530080E2C7 /* Certificates.swift */,
|
||||
F9C5CAC4289453B200548EEE /* ChatConnectionManager.swift */,
|
||||
509A8DC12E25817E0024BF14 /* ConnectionLock.swift */,
|
||||
F9C5CAF7289453B200548EEE /* ContentProxy.swift */,
|
||||
F9C5CAF2289453B200548EEE /* HttpHeaders.swift */,
|
||||
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
|
||||
F9C5CAF1289453B200548EEE /* NetworkInterfaceSet.swift */,
|
||||
F9C5CAC8289453B200548EEE /* OutageDetection.swift */,
|
||||
72328C8A2C6C7322000EA728 /* OWSCensorshipConfiguration.swift */,
|
||||
@ -15392,7 +15389,7 @@
|
||||
F9D5BFCC2979A017001737E5 /* OWSRequestFactory+Spam.swift */,
|
||||
D95C39E7296DEBFB00A9DA23 /* OWSRequestFactory+Usernames.swift */,
|
||||
F9C5CAE2289453B200548EEE /* OWSRequestFactory.swift */,
|
||||
F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */,
|
||||
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */,
|
||||
66C2B1302A05D28A008DDE72 /* TSRequest.swift */,
|
||||
);
|
||||
path = Requests;
|
||||
@ -15413,7 +15410,6 @@
|
||||
058B49922C66804B00307D38 /* AVAssetExportSession+Async.swift */,
|
||||
F9C5CB64289453B200548EEE /* Batching.swift */,
|
||||
F9C5CB40289453B200548EEE /* Bench.swift */,
|
||||
668FE09A28B923A4008B9071 /* Bool+SSK.swift */,
|
||||
E7D7C93E28B580AC003F043B /* Bundle+OWS.swift */,
|
||||
88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */,
|
||||
76387BEF28F4ED73002C7BA5 /* CaseIterable.swift */,
|
||||
@ -15542,19 +15538,6 @@
|
||||
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 = (
|
||||
@ -18076,9 +18059,11 @@
|
||||
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 */,
|
||||
@ -18469,7 +18454,7 @@
|
||||
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */,
|
||||
8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */,
|
||||
663883572D4C0360008EA898 /* InternalSQLClientViewController.swift in Sources */,
|
||||
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */,
|
||||
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */,
|
||||
32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */,
|
||||
45C845AD291466C0005F6EA5 /* JournalingOrderedDictionary.swift in Sources */,
|
||||
5045F44229E0DB7100058E5F /* LaunchJobs.swift in Sources */,
|
||||
@ -18584,7 +18569,6 @@
|
||||
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 */,
|
||||
@ -19093,7 +19077,6 @@
|
||||
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 */,
|
||||
@ -19267,8 +19250,8 @@
|
||||
F9C5CE44289453B400548EEE /* Error+IsRetryable.swift in Sources */,
|
||||
F9C5CE23289453B400548EEE /* Error+SSK.swift in Sources */,
|
||||
D9C7CEB428EB8495001E87B6 /* ExperienceUpgrade.swift in Sources */,
|
||||
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */,
|
||||
D9C7CECB28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift in Sources */,
|
||||
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */,
|
||||
D98BC5332EE387A30052A81F /* ExpirationJob.swift in Sources */,
|
||||
C1FB9B752B16498C00D51A3B /* ExternalPendingDonationStore.swift in Sources */,
|
||||
F9C5CE57289453B400548EEE /* Factories.swift in Sources */,
|
||||
@ -19398,7 +19381,6 @@
|
||||
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 */,
|
||||
@ -19563,6 +19545,7 @@
|
||||
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 */,
|
||||
@ -19685,12 +19668,11 @@
|
||||
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 */,
|
||||
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */,
|
||||
6659A0262A7C11A800066AB7 /* PreKeyManagerImpl.swift in Sources */,
|
||||
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */,
|
||||
50589CDE2E8C44D5003EF42A /* PreKeyStore.swift in Sources */,
|
||||
F9C5CD52289453B300548EEE /* PreKeyStoreImpl.swift in Sources */,
|
||||
C17345BB2A5E000300C6426D /* PreKeyTarget.swift in Sources */,
|
||||
@ -19764,7 +19746,7 @@
|
||||
6646573B2AC388C70099DE1C /* RegistrationStateChangeManager.swift in Sources */,
|
||||
6646573D2AC3894D0099DE1C /* RegistrationStateChangeManagerImpl.swift in Sources */,
|
||||
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */,
|
||||
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */,
|
||||
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */,
|
||||
F9C5CE17289453B400548EEE /* RemoteConfigManager.swift in Sources */,
|
||||
D98DD86028EE53B00089333E /* RemoteMegaphoneModel.swift in Sources */,
|
||||
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */,
|
||||
@ -19842,7 +19824,6 @@
|
||||
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 */,
|
||||
@ -20113,8 +20094,6 @@
|
||||
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 */,
|
||||
@ -20211,6 +20190,7 @@
|
||||
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 */,
|
||||
|
||||
@ -87,7 +87,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
||||
self.refreshConnection(isAppActive: false, shouldRunCron: false)
|
||||
self.refreshConnection(isAppActive: false)
|
||||
}
|
||||
|
||||
clearAppropriateNotificationsAndRestoreBadgeCount()
|
||||
@ -369,13 +369,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var screenLockUI = ScreenLockUI(appReadiness: appReadiness)
|
||||
|
||||
private func configureGlobalUI(in window: UIWindow) {
|
||||
let screenLockUI = AppEnvironment.shared.screenLockUI
|
||||
let windowManager = AppEnvironment.shared.windowManagerRef
|
||||
|
||||
Theme.setupSignalAppearance()
|
||||
|
||||
screenLockUI.setupWithRootWindow(window)
|
||||
AppEnvironment.shared.windowManagerRef.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
|
||||
windowManager.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
|
||||
screenLockUI.startObserving()
|
||||
}
|
||||
|
||||
@ -716,6 +717,7 @@ 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()
|
||||
|
||||
@ -807,7 +809,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, shouldRunCron: false)
|
||||
self.refreshConnection(isAppActive: false)
|
||||
}
|
||||
|
||||
if registeredState != nil {
|
||||
@ -1390,7 +1392,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
refreshConnection(isAppActive: true, shouldRunCron: true)
|
||||
refreshConnection(isAppActive: true)
|
||||
|
||||
// Every time we become active...
|
||||
if registeredState != nil {
|
||||
@ -1458,7 +1460,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
/// is in the background.
|
||||
private var backgroundFetchHandle: BackgroundTaskHandle?
|
||||
|
||||
private func refreshConnection(isAppActive: Bool, shouldRunCron: Bool) {
|
||||
private func refreshConnection(isAppActive: Bool) {
|
||||
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
|
||||
|
||||
let oldActiveConnectionTokens = self.activeConnectionTokens
|
||||
@ -1466,9 +1468,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// If we're active, open a connection.
|
||||
self.activeConnectionTokens = chatConnectionManager.requestConnections()
|
||||
oldActiveConnectionTokens.forEach { $0.releaseConnection() }
|
||||
if shouldRunCron {
|
||||
self.startCronTask()
|
||||
}
|
||||
|
||||
// Start a new Cron task on activate.
|
||||
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()
|
||||
@ -1485,17 +1488,14 @@ 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,8 +1768,24 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return false
|
||||
}
|
||||
let isVideo = isVideoCall(intent)
|
||||
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
||||
|
||||
Task { @MainActor [appReadiness] in
|
||||
do {
|
||||
try await appReadiness.waitForAppReady()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
let callService = AppEnvironment.shared.callService!
|
||||
let screenLockUI = AppEnvironment.shared.screenLockUI
|
||||
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
|
||||
@ -1787,7 +1803,6 @@ 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")
|
||||
@ -1799,6 +1814,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
callService.initiateCall(to: callTarget, isVideo: isVideo)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1813,17 +1829,12 @@ 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.
|
||||
@ -1931,9 +1942,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
Task { @MainActor [appReadiness] () -> Void in
|
||||
defer { completionHandler() }
|
||||
|
||||
try await self.appReadiness.waitForAppReady()
|
||||
do {
|
||||
try await self.appReadiness.waitForAppReady()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
@ -1942,7 +1959,11 @@ 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)
|
||||
try await NotificationActionHandler.handleNotificationResponse(
|
||||
response,
|
||||
appReadiness: appReadiness,
|
||||
screenLockUI: screenLockUI,
|
||||
)
|
||||
|
||||
// Then wait for any enqueued messages (e.g., read receipts) to be sent.
|
||||
try await backgroundMessageFetcher.waitForFetchingProcessingAndSideEffects()
|
||||
|
||||
@ -22,12 +22,12 @@ public class AppEnvironment: NSObject {
|
||||
@MainActor
|
||||
var ownedObjects = [AnyObject]()
|
||||
|
||||
let cvAudioPlayerRef: CVAudioPlayer
|
||||
let deviceTransferServiceRef: DeviceTransferService
|
||||
let pushRegistrationManagerRef: PushRegistrationManager
|
||||
|
||||
let cvAudioPlayerRef = CVAudioPlayer()
|
||||
let speechManagerRef = SpeechManager()
|
||||
let windowManagerRef = WindowManager()
|
||||
let screenLockUI: ScreenLockUI
|
||||
let speechManagerRef: SpeechManager
|
||||
let windowManagerRef: WindowManager
|
||||
|
||||
private(set) var appIconBadgeUpdater: AppIconBadgeUpdater!
|
||||
private(set) var avatarHistoryManager: AvatarHistoryManager!
|
||||
@ -44,8 +44,12 @@ 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()
|
||||
|
||||
@ -253,7 +257,6 @@ 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
|
||||
@ -325,12 +328,6 @@ 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)
|
||||
|
||||
45
Signal/Backups/BackupNeverShareRecoveryKeySheet.swift
Normal file
45
Signal/Backups/BackupNeverShareRecoveryKeySheet.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
105
Signal/Backups/BackupPlanOptionView.swift
Normal file
105
Signal/Backups/BackupPlanOptionView.swift
Normal file
@ -0,0 +1,105 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,9 @@ 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 {
|
||||
@ -115,7 +118,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?.copyToClipboard()
|
||||
self?.copyToClipboardWithConfirmation()
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -175,6 +178,26 @@ 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]],
|
||||
|
||||
@ -16,6 +16,7 @@ class BackupSettingsViewController:
|
||||
enum OnAppearAction {
|
||||
case presentWelcomeToBackupsSheet
|
||||
case automaticallyStartBackup(completion: ((UIViewController) -> Void)?)
|
||||
case disableOptimizeLocalStorage
|
||||
}
|
||||
|
||||
private let accountEntropyPoolManager: AccountEntropyPoolManager
|
||||
@ -68,7 +69,6 @@ class BackupSettingsViewController:
|
||||
backupSubscriptionManager: DependenciesBridge.shared.backupSubscriptionManager,
|
||||
db: DependenciesBridge.shared.db,
|
||||
deviceSleepManager: deviceSleepManager,
|
||||
remoteConfig: SSKEnvironment.shared.remoteConfigManagerRef,
|
||||
subscriptionConfigManager: DependenciesBridge.shared.subscriptionConfigManager,
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
)
|
||||
@ -92,7 +92,6 @@ class BackupSettingsViewController:
|
||||
backupSubscriptionManager: BackupSubscriptionManager,
|
||||
db: DB,
|
||||
deviceSleepManager: DeviceSleepManager,
|
||||
remoteConfig: RemoteConfigProvider,
|
||||
subscriptionConfigManager: SubscriptionConfigManager,
|
||||
tsAccountManager: TSAccountManager,
|
||||
) {
|
||||
@ -122,7 +121,7 @@ class BackupSettingsViewController:
|
||||
|
||||
self.onAppearAction = onAppearAction
|
||||
switch onAppearAction {
|
||||
case .presentWelcomeToBackupsSheet, nil:
|
||||
case nil, .presentWelcomeToBackupsSheet, .disableOptimizeLocalStorage:
|
||||
break
|
||||
case .automaticallyStartBackup(let completion):
|
||||
self.onBackupComplete = completion
|
||||
@ -147,7 +146,6 @@ class BackupSettingsViewController:
|
||||
),
|
||||
hasBackupFailed: backupFailureStateManager.hasFailedBackup(tx: tx),
|
||||
isBackgroundAppRefreshDisabled: Self.isBackgroundAppRefreshDisabled(),
|
||||
isOptimizeStorageEnabled: remoteConfig.currentConfig().isOptimizeStorageEnabled,
|
||||
)
|
||||
|
||||
return viewModel
|
||||
@ -182,6 +180,8 @@ class BackupSettingsViewController:
|
||||
presentWelcomeToBackupsSheet()
|
||||
case .automaticallyStartBackup:
|
||||
performManualBackup()
|
||||
case .disableOptimizeLocalStorage:
|
||||
setOptimizeLocalStorage(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -621,28 +621,87 @@ class BackupSettingsViewController:
|
||||
final class WelcomeToBackupsSheet: HeroSheetViewController {
|
||||
override var canBeDismissed: Bool { false }
|
||||
|
||||
init(onConfirm: @escaping () -> Void) {
|
||||
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
|
||||
}
|
||||
|
||||
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: 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() },
|
||||
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,
|
||||
),
|
||||
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 welcomeToBackupsSheet = WelcomeToBackupsSheet { [self] in
|
||||
viewModel.performManualBackup()
|
||||
dismiss(animated: true)
|
||||
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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
present(welcomeToBackupsSheet, animated: true)
|
||||
@ -1021,35 +1080,38 @@ class BackupSettingsViewController:
|
||||
// MARK: -
|
||||
|
||||
fileprivate func setOptimizeLocalStorage(_ newOptimizeLocalStorage: Bool) {
|
||||
let isPaidPlanTester: Bool = db.write { tx in
|
||||
let hasMadeAtLeastOneBackup: Bool? = db.write { tx in
|
||||
let currentBackupPlan = backupPlanManager.backupPlan(tx: tx)
|
||||
let newBackupPlan: BackupPlan
|
||||
let isPaidPlanTester: Bool
|
||||
let lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
|
||||
|
||||
let newBackupPlan: BackupPlan
|
||||
switch currentBackupPlan {
|
||||
case .disabled, .disabling, .free:
|
||||
owsFailDebug("Shouldn't be setting Optimize Local Storage: \(currentBackupPlan)")
|
||||
return false
|
||||
case .disabled,
|
||||
.disabling,
|
||||
.free,
|
||||
.paid(optimizeLocalStorage: newOptimizeLocalStorage),
|
||||
.paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage),
|
||||
.paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage):
|
||||
return nil
|
||||
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 isPaidPlanTester
|
||||
return lastBackupDetails != nil
|
||||
}
|
||||
|
||||
// If disabling Optimize Local Storage, offer to start downloads now.
|
||||
if !newOptimizeLocalStorage {
|
||||
if
|
||||
hasMadeAtLeastOneBackup == true,
|
||||
!newOptimizeLocalStorage
|
||||
{
|
||||
// If disabling Optimize Local Storage with media potentially
|
||||
// offloaded, offer to start downloads now.
|
||||
showDownloadOffloadedMediaSheet()
|
||||
} else if isPaidPlanTester {
|
||||
showOffloadedMediaForTestersWarningSheet(onAcknowledge: {})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1088,54 +1150,41 @@ 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, .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:
|
||||
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 {
|
||||
let warningSheet = ActionSheetController(
|
||||
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.",
|
||||
),
|
||||
title: warningTitle,
|
||||
message: warningMessage,
|
||||
)
|
||||
warningSheet.addAction(ActionSheetAction(
|
||||
title: OWSLocalizedString(
|
||||
@ -1144,9 +1193,31 @@ class BackupSettingsViewController:
|
||||
),
|
||||
style: .destructive,
|
||||
handler: { [self] _ in
|
||||
db.write { tx in
|
||||
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
|
||||
}
|
||||
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)
|
||||
},
|
||||
))
|
||||
warningSheet.addAction(ActionSheetAction(
|
||||
@ -1161,6 +1232,10 @@ class BackupSettingsViewController:
|
||||
warningSheet.addAction(.cancel)
|
||||
|
||||
presentActionSheet(warningSheet)
|
||||
} else {
|
||||
db.write { tx in
|
||||
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
db.write { tx in
|
||||
@ -1364,10 +1439,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.",
|
||||
@ -1542,8 +1617,6 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
/// from running.)
|
||||
@Published var isBackgroundAppRefreshDisabled: Bool
|
||||
|
||||
@Published var isOptimizeStorageEnabled: Bool
|
||||
|
||||
weak var actionsDelegate: ActionsDelegate?
|
||||
|
||||
init(
|
||||
@ -1560,7 +1633,6 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
mediaTierCapacityOverflow: UInt64?,
|
||||
hasBackupFailed: Bool,
|
||||
isBackgroundAppRefreshDisabled: Bool,
|
||||
isOptimizeStorageEnabled: Bool,
|
||||
) {
|
||||
self.backupSubscriptionConfiguration = backupSubscriptionConfiguration
|
||||
|
||||
@ -1580,8 +1652,6 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
self.mediaTierCapacityOverflow = mediaTierCapacityOverflow
|
||||
self.hasBackupFailed = hasBackupFailed
|
||||
self.isBackgroundAppRefreshDisabled = isBackgroundAppRefreshDisabled
|
||||
|
||||
self.isOptimizeStorageEnabled = isOptimizeStorageEnabled
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -1640,16 +1710,21 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
|
||||
// MARK: -
|
||||
|
||||
var optimizeLocalStorageAvailable: Bool {
|
||||
/// Whether the "Optimze Storage" feature is available, per the current
|
||||
/// `BackupPlan`.
|
||||
var isOptimizeLocalStorageAvailable: Bool {
|
||||
switch backupPlan {
|
||||
case .disabled, .disabling, .free:
|
||||
false
|
||||
case .paid, .paidExpiringSoon, .paidAsTester:
|
||||
case .paid, .paidAsTester:
|
||||
true
|
||||
case .paidExpiringSoon(let optimizeLocalStorage):
|
||||
// Only allow disabling Optimize Storage if expiring soon, not enabling.
|
||||
optimizeLocalStorage
|
||||
}
|
||||
}
|
||||
|
||||
var optimizeLocalStorage: Bool {
|
||||
var isOptimizeLocalStorageEnabled: Bool {
|
||||
switch backupPlan {
|
||||
case .disabled, .disabling, .free:
|
||||
false
|
||||
@ -1922,44 +1997,32 @@ struct BackupSettingsView: View {
|
||||
viewModel: viewModel,
|
||||
)
|
||||
|
||||
if viewModel.isOptimizeStorageEnabled {
|
||||
Toggle(
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
|
||||
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
|
||||
),
|
||||
isOn: Binding(
|
||||
get: { viewModel.optimizeLocalStorage },
|
||||
set: { viewModel.setOptimizeLocalStorage($0) },
|
||||
),
|
||||
).disabled(!viewModel.optimizeLocalStorageAvailable)
|
||||
}
|
||||
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: {
|
||||
if viewModel.isOptimizeStorageEnabled {
|
||||
let footerText: String = if
|
||||
viewModel.optimizeLocalStorageAvailable,
|
||||
viewModel.isPaidPlanTester
|
||||
{
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester.",
|
||||
)
|
||||
} else if viewModel.optimizeLocalStorageAvailable {
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
|
||||
)
|
||||
} else {
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
|
||||
)
|
||||
}
|
||||
|
||||
Text(footerText)
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
.font(.caption)
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
||||
Text(footerText)
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
SignalSection {
|
||||
@ -3178,7 +3241,6 @@ private extension BackupSettingsViewModel {
|
||||
mediaTierCapacityOverflow: mediaTierCapacityOverflow,
|
||||
hasBackupFailed: hasBackupFailed,
|
||||
isBackgroundAppRefreshDisabled: isBackgroundAppRefreshDisabled,
|
||||
isOptimizeStorageEnabled: false,
|
||||
)
|
||||
let actionsDelegate = PreviewActionsDelegate()
|
||||
viewModel.actionsDelegate = actionsDelegate
|
||||
@ -3217,7 +3279,8 @@ private extension BackupSettingsViewModel {
|
||||
backupSubscriptionLoadingState: .loaded(.paidButExpiring(
|
||||
expirationDate: Date().addingTimeInterval(.week),
|
||||
)),
|
||||
backupPlan: .paidExpiringSoon(optimizeLocalStorage: false),
|
||||
backupPlan: .paidExpiringSoon(optimizeLocalStorage: true),
|
||||
latestBackupAttachmentDownloadUpdateState: .suspended,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,10 @@ import SignalUI
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
|
||||
class ChooseBackupPlanViewController: HostingController<ChooseBackupPlanView> {
|
||||
class ChooseBackupPlanViewController:
|
||||
HostingController<ChooseBackupPlanView>,
|
||||
ChooseBackupPlanViewModel.ActionsDelegate
|
||||
{
|
||||
typealias OnConfirmPlanSelectionBlock = (ChooseBackupPlanViewController, PlanSelection) -> Void
|
||||
|
||||
enum StoreKitAvailability {
|
||||
@ -118,11 +121,9 @@ class ChooseBackupPlanViewController: HostingController<ChooseBackupPlanView> {
|
||||
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):
|
||||
@ -233,7 +234,7 @@ struct ChooseBackupPlanView: View {
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
PlanOptionView(
|
||||
BackupPlanOptionView(
|
||||
title: OWSLocalizedString(
|
||||
"CHOOSE_BACKUP_PLAN_FREE_PLAN_TITLE",
|
||||
comment: "Title for the free plan option, when choosing a Backup plan.",
|
||||
@ -247,11 +248,11 @@ struct ChooseBackupPlanView: View {
|
||||
viewModel.freeMediaTierDays,
|
||||
),
|
||||
bullets: [
|
||||
PlanOptionView.BulletPoint(iconKey: "thread", text: OWSLocalizedString(
|
||||
BackupPlanOptionView.BulletPoint(icon: .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.",
|
||||
)),
|
||||
PlanOptionView.BulletPoint(iconKey: "album-tilt", text: String.localizedStringWithFormat(
|
||||
BackupPlanOptionView.BulletPoint(icon: .albumTilt, text: String.localizedStringWithFormat(
|
||||
OWSLocalizedString(
|
||||
"CHOOSE_BACKUP_PLAN_BULLET_RECENT_MEDIA_BACKUP_%d",
|
||||
tableName: "PluralAware",
|
||||
@ -269,7 +270,7 @@ struct ChooseBackupPlanView: View {
|
||||
|
||||
Spacer().frame(height: 16)
|
||||
|
||||
PlanOptionView(
|
||||
BackupPlanOptionView(
|
||||
title: {
|
||||
switch viewModel.storeKitAvailability {
|
||||
case .available(let paidPlanDisplayPrice):
|
||||
@ -292,15 +293,15 @@ struct ChooseBackupPlanView: View {
|
||||
comment: "Subtitle for the paid plan option, when choosing a Backup plan.",
|
||||
),
|
||||
bullets: [
|
||||
PlanOptionView.BulletPoint(iconKey: "thread", text: OWSLocalizedString(
|
||||
BackupPlanOptionView.BulletPoint(icon: .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.",
|
||||
)),
|
||||
PlanOptionView.BulletPoint(iconKey: "album-tilt", text: OWSLocalizedString(
|
||||
BackupPlanOptionView.BulletPoint(icon: .albumTilt, 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.",
|
||||
)),
|
||||
PlanOptionView.BulletPoint(iconKey: "data", text: String.nonPluralLocalizedStringWithFormat(
|
||||
BackupPlanOptionView.BulletPoint(icon: .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' }}.",
|
||||
@ -383,106 +384,6 @@ 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 {
|
||||
|
||||
@ -31,9 +31,8 @@ struct DisplayableAccountEntropyPool {
|
||||
.uppercased()
|
||||
.map { char in
|
||||
switch char {
|
||||
// TODO: Reenable this once support is available for all platforms
|
||||
// case "0": "="
|
||||
// case "O", "o": "#"
|
||||
case "0": "="
|
||||
case "O", "o": "#"
|
||||
default: char
|
||||
}
|
||||
},
|
||||
|
||||
@ -51,6 +51,9 @@ 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,
|
||||
|
||||
@ -240,7 +240,7 @@ extension CallControlsOverflowView: MessageReactionPickerDelegate {
|
||||
self.react(with: reaction)
|
||||
}
|
||||
|
||||
func didSelectAnyEmoji() {
|
||||
func didSelectShowFullEmojiPicker() {
|
||||
let sheet = EmojiPickerSheet(
|
||||
message: nil,
|
||||
reactionPickerConfigurationListener: self,
|
||||
|
||||
@ -290,7 +290,7 @@ class IndividualCallSheetDataSource: CallDrawerSheetDataSource {
|
||||
isLocalUser: false,
|
||||
isUnknown: false,
|
||||
isAudioMuted: self.individualCall.isRemoteAudioMuted,
|
||||
isVideoMuted: self.individualCall.isRemoteVideoEnabled.negated,
|
||||
isVideoMuted: !self.individualCall.isRemoteVideoEnabled,
|
||||
isPresenting: self.individualCall.isRemoteSharingScreen,
|
||||
))
|
||||
}
|
||||
|
||||
@ -125,6 +125,7 @@ extension NewCallViewController: RecipientContextMenuHelperDelegate {
|
||||
// MARK: - RecipientPickerDelegate
|
||||
|
||||
extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelegate {
|
||||
|
||||
func recipientPicker(
|
||||
_ recipientPickerViewController: RecipientPickerViewController,
|
||||
selectionStyleForRecipient recipient: PickedRecipient,
|
||||
@ -133,7 +134,10 @@ 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)
|
||||
@ -143,7 +147,12 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega
|
||||
}
|
||||
}
|
||||
|
||||
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController, accessoryViewForRecipient recipient: PickedRecipient, transaction: DBReadTransaction) -> ContactCellAccessoryView? {
|
||||
func recipientPicker(
|
||||
_ recipientPickerViewController: RecipientPickerViewController,
|
||||
contactCellAccessoryForRecipient recipient: PickedRecipient,
|
||||
transaction: DBReadTransaction,
|
||||
) -> ContactCellView.Accessory? {
|
||||
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 20
|
||||
|
||||
@ -159,7 +159,7 @@ class MessageUserSubsetSheet: OWSTableSheetViewController {
|
||||
|
||||
cell.selectionStyle = .none
|
||||
|
||||
let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser)
|
||||
var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser)
|
||||
configuration.forceDarkAppearance = self?.forceDarkMode ?? false
|
||||
|
||||
if
|
||||
|
||||
@ -114,9 +114,7 @@ class CVAttachmentProgressView: ManualLayoutView {
|
||||
|
||||
addLayoutBlock { view in
|
||||
guard let view = view as? CVAttachmentProgressView else { return }
|
||||
DispatchQueue.main.async {
|
||||
view.loadInitialStateIfNeeded()
|
||||
}
|
||||
view.loadInitialStateIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,6 +199,12 @@ class CVAttachmentProgressView: ManualLayoutView {
|
||||
name: AttachmentDownloads.attachmentDownloadProgressNotification,
|
||||
object: nil,
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(processDownloadStoppedNotification(notification:)),
|
||||
name: AttachmentDownloads.attachmentDownloadStoppedNotification,
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,6 +363,22 @@ 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()
|
||||
|
||||
@ -148,15 +148,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
componentView.outerStack.isAccessibilityElement = true
|
||||
componentView.outerStack.accessibilityLabel = titleString
|
||||
componentView.outerStack.accessibilityTraits = .button
|
||||
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.",
|
||||
)
|
||||
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: collapseSet.isExpanded)
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
@ -187,6 +179,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
componentView.chevronLabel.transform = willBeExpanded
|
||||
? CGAffineTransform(rotationAngle: expandedRotation)
|
||||
: .identity
|
||||
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: willBeExpanded)
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
animation.fromValue = fromAngle
|
||||
@ -347,6 +340,18 @@ 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,
|
||||
|
||||
@ -150,6 +150,7 @@ private class CVQuotedMessageViewAdapter: CVQuotedMessageViewDelegate {
|
||||
DependenciesBridge.shared.attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
|
||||
message,
|
||||
priority: .userInitiated,
|
||||
useThumbnails: false,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
|
||||
@ -528,7 +528,6 @@ 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
|
||||
}
|
||||
}
|
||||
@ -1197,7 +1196,7 @@ private extension CVComponentState.Builder {
|
||||
self.collapseSet = CVComponentState.CollapseSet(
|
||||
collapsedInteractions: collapseSetInteraction.collapsedInteractions,
|
||||
collapseSetType: collapseSetInteraction.collapseSetType,
|
||||
isExpanded: collapseSetInteraction.isExpanded,
|
||||
isExpanded: viewStateSnapshot.expandedCollapseSetIds.contains(collapseSetInteraction.uniqueId),
|
||||
finalTimerDescription: collapseSetInteraction.finalTimerDescription,
|
||||
)
|
||||
return build()
|
||||
@ -1373,8 +1372,15 @@ private extension CVComponentState.Builder {
|
||||
.paid(let optimizeLocalStorage),
|
||||
.paidAsTester(let optimizeLocalStorage),
|
||||
.paidExpiringSoon(let optimizeLocalStorage):
|
||||
if optimizeLocalStorage {
|
||||
mediaAlbumHasSkippedAttachment = !canAutoDownloadAttachment(referencedAttachment: attachment)
|
||||
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
|
||||
}
|
||||
@ -1757,10 +1763,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.negated
|
||||
).fullTextValue.isEmpty
|
||||
} ?? false
|
||||
|
||||
switch cvAttachment {
|
||||
|
||||
@ -195,16 +195,22 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
if safetySection.shouldShowProfileNamesEducation {
|
||||
innerViews.append(UIView.spacer(withHeight: vSpacingNotVerifiedLabel))
|
||||
|
||||
let nameNotVerifiedButton = componentView.profileNamesEducationButton
|
||||
var buttonConfiguration = headerButtonConfigurationBase()
|
||||
buttonConfiguration.baseBackgroundColor = .Signal.warningLabel.withAlphaComponent(0.2)
|
||||
buttonConfiguration.contentInsets = notVerifierButtonContentInsets
|
||||
|
||||
let nameNotVerifiedButtonLabelConfig = nameNotVerifiedConfig()
|
||||
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)
|
||||
}
|
||||
nameNotVerifiedButtonLabelConfig.applyForRendering(buttonConfiguration: &buttonConfiguration)
|
||||
|
||||
let nameNotVerifiedButton = UIButton(
|
||||
configuration: buttonConfiguration,
|
||||
primaryAction: UIAction { _ in
|
||||
componentDelegate.didTapNameEducation(type: safetySection.threadType)
|
||||
},
|
||||
)
|
||||
innerViews.append(nameNotVerifiedButton)
|
||||
|
||||
componentView.profileNamesEducationButton = nameNotVerifiedButton
|
||||
} else if safetySection.isOfficialChat {
|
||||
innerViews.append(UIView.spacer(withHeight: vSpacingNotVerifiedLabel))
|
||||
|
||||
@ -254,19 +260,25 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
}
|
||||
|
||||
if safetySection.shouldShowSafetyTipsButton {
|
||||
let showTipsButton = componentView.showTipsButton
|
||||
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &showTipsButton.configuration!)
|
||||
showTipsButton.configuration?.baseBackgroundColor =
|
||||
var buttonConfiguration = headerButtonConfigurationBase()
|
||||
buttonConfiguration.contentInsets = safetyButtonContentInsets
|
||||
buttonConfiguration.baseBackgroundColor =
|
||||
conversationStyle.hasWallpaper ? .Signal.MaterialBase.button : .Signal.secondaryFill
|
||||
showTipsButton.addAction(
|
||||
UIAction { _ in
|
||||
|
||||
let safetyTipsButtonLabelConfig = safetyTipsButtonLabelConfig()
|
||||
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &buttonConfiguration)
|
||||
|
||||
let showTipsButton = UIButton(
|
||||
configuration: buttonConfiguration,
|
||||
primaryAction: UIAction { _ in
|
||||
componentDelegate.didTapSafetyTips()
|
||||
},
|
||||
for: .primaryActionTriggered,
|
||||
)
|
||||
|
||||
innerViews.append(UIView.spacer(withHeight: vSpacingSafetyButton))
|
||||
innerViews.append(showTipsButton)
|
||||
|
||||
componentView.showTipsButton = showTipsButton
|
||||
}
|
||||
}
|
||||
|
||||
@ -449,7 +461,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
)
|
||||
}
|
||||
|
||||
private var safetyTipsButtonLabelConfig: CVLabelConfig {
|
||||
private func safetyTipsButtonLabelConfig() -> CVLabelConfig {
|
||||
CVLabelConfig.unstyledText(
|
||||
OWSLocalizedString(
|
||||
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
|
||||
@ -643,10 +655,14 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
private let vSpacingSafetySectionDefault: CGFloat = 8
|
||||
|
||||
private let safetyButtonContentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 5)
|
||||
private let hPaddingGroupDetails: CGFloat = 25
|
||||
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 vPaddingNotVerifiedButton: CGFloat = 2
|
||||
private let hPaddingNotVerifiedButton: CGFloat = 12
|
||||
private let hPaddingGroupDetails: CGFloat = 25
|
||||
|
||||
private let vOffsetThreadDetailsOutline: CGFloat = 16
|
||||
|
||||
@ -689,20 +705,18 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
if let safetySection = threadDetails.safetySection {
|
||||
if safetySection.shouldShowProfileNamesEducation {
|
||||
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
|
||||
let notVerifiedSize = CVText.measureLabel(
|
||||
let buttonSize = CVText.measureLabel(
|
||||
config: nameNotVerifiedConfig(),
|
||||
maxWidth: maxContentWidth,
|
||||
)
|
||||
let notVerifiedSizeWithPadding = CGSize(width: notVerifiedSize.width + hPaddingNotVerifiedButton * 2, height: notVerifiedSize.height + vPaddingNotVerifiedButton * 2)
|
||||
innerSubviewInfos.append(notVerifiedSizeWithPadding.asManualSubviewInfo)
|
||||
) + notVerifierButtonContentInsets.asSize
|
||||
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
|
||||
} else if safetySection.isOfficialChat {
|
||||
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
|
||||
let officialLabelSize = CVText.measureLabel(
|
||||
let buttonSize = CVText.measureLabel(
|
||||
config: officialLabelConfig(),
|
||||
maxWidth: maxContentWidth,
|
||||
)
|
||||
let officialLabelSizeWithPadding = CGSize(width: officialLabelSize.width + hPaddingNotVerifiedButton * 2, height: officialLabelSize.height + vPaddingNotVerifiedButton * 2)
|
||||
innerSubviewInfos.append(officialLabelSizeWithPadding.asManualSubviewInfo)
|
||||
) + notVerifierButtonContentInsets.asSize
|
||||
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@ -738,7 +752,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)
|
||||
@ -787,7 +801,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
if let safetySection = threadDetails.safetySection {
|
||||
if
|
||||
safetySection.shouldShowSafetyTipsButton,
|
||||
componentView.showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
|
||||
let showTipsButton = componentView.showTipsButton,
|
||||
showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
|
||||
{
|
||||
componentDelegate.didTapSafetyTips()
|
||||
return true
|
||||
@ -804,7 +819,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
if
|
||||
safetySection.shouldShowProfileNamesEducation,
|
||||
componentView.profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
|
||||
let profileNamesEducationButton = componentView.profileNamesEducationButton,
|
||||
profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
|
||||
{
|
||||
componentDelegate.didTapNameEducation(type: safetySection.threadType)
|
||||
return true
|
||||
@ -839,17 +855,13 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
fileprivate let titleButton = CVButton()
|
||||
fileprivate let bioLabel = CVLabel()
|
||||
|
||||
fileprivate let profileNamesEducationButton = OWSRoundedButton()
|
||||
fileprivate var profileNamesEducationButton: UIButton?
|
||||
fileprivate let officialLabel = CVLabel()
|
||||
|
||||
fileprivate let reviewCarefullyLabel = CVLabel()
|
||||
fileprivate let detailsButton = CVButton()
|
||||
fileprivate let mutualGroupsLabel = CVLabel()
|
||||
fileprivate let showTipsButton: UIButton = {
|
||||
let button = UIButton(configuration: .gray())
|
||||
button.configuration?.contentInsets = NSDirectionalEdgeInsets(hMargin: 10, vMargin: 5)
|
||||
return button
|
||||
}()
|
||||
fileprivate var showTipsButton: UIButton?
|
||||
|
||||
fileprivate let groupDescriptionPreviewView = GroupDescriptionPreviewView(
|
||||
shouldDeactivateConstraints: true,
|
||||
|
||||
@ -59,7 +59,7 @@ extension TSInfoMessage.PersistableGroupUpdateItem {
|
||||
)
|
||||
{
|
||||
owsAssertDebug(
|
||||
isTail.negated,
|
||||
!isTail,
|
||||
"Collapsed item with a following request shouldn't be a tail!",
|
||||
)
|
||||
return nextItemAction
|
||||
|
||||
@ -9,6 +9,7 @@ public import UIKit
|
||||
|
||||
public protocol ConversationInputTextViewDelegate: AnyObject {
|
||||
func didAttemptAttachmentPaste()
|
||||
func didAttemptAccountEntropyPoolPaste(completePaste: @escaping () -> Void)
|
||||
func inputTextViewSendMessagePressed()
|
||||
func textViewDidChange(_ textView: UITextView)
|
||||
}
|
||||
@ -199,9 +200,50 @@ 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 {
|
||||
|
||||
@ -1219,7 +1219,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
lazy var sendButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.accessibilityLabel = MessageStrings.sendButton
|
||||
button.ows_adjustsImageWhenDisabled = true
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "sendButton")
|
||||
button.setImage(UIImage(imageLiteralResourceName: "send-blue-28"), for: .normal)
|
||||
|
||||
@ -33,6 +33,8 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
viewState.expandedCollapseSets.insert(collapseSetId)
|
||||
}
|
||||
loadCoordinator.enqueueReload(
|
||||
updatedInteractionIds: [collapseSetId],
|
||||
deletedInteractionIds: [],
|
||||
preferredScrollContinuityAnchorInteractionId: collapseSetId,
|
||||
)
|
||||
}
|
||||
@ -181,7 +183,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 {
|
||||
Task.detached {
|
||||
let attachmentDownloadManager = DependenciesBridge.shared.attachmentDownloadManager
|
||||
let attachmentStore = DependenciesBridge.shared.attachmentStore
|
||||
let backupAttachmentDownloadStore = DependenciesBridge.shared.backupAttachmentDownloadStore
|
||||
@ -192,17 +194,22 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let messageHasAnyEnqueuedBackupDownloads = db.read { tx in
|
||||
enum DownloadTypeToEnqueue {
|
||||
case thumbnail
|
||||
case fullsize
|
||||
}
|
||||
|
||||
let messageTypeToDownload: DownloadTypeToEnqueue? = db.read { tx in
|
||||
let referencedAttachments = attachmentStore.fetchReferencedAttachmentsOwnedByMessage(
|
||||
messageRowId: messageRowId,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
return referencedAttachments.contains { referencedAttachment in
|
||||
let downloadTypes: [DownloadTypeToEnqueue] = referencedAttachments.compactMap { 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 false
|
||||
return nil
|
||||
}
|
||||
// Otherwise use presence in the backup download queue to indicate
|
||||
// downloadability; this just functionally bumps the priority so the
|
||||
@ -213,22 +220,60 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
tx: tx,
|
||||
)
|
||||
switch enqueuedDownload?.state {
|
||||
case nil, .done, .ineligible:
|
||||
return false
|
||||
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 .ready:
|
||||
return true
|
||||
return .fullsize
|
||||
}
|
||||
}
|
||||
|
||||
if downloadTypes.contains(.fullsize) {
|
||||
return .fullsize
|
||||
} else if downloadTypes.contains(.thumbnail) {
|
||||
return .thumbnail
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if messageHasAnyEnqueuedBackupDownloads {
|
||||
switch messageTypeToDownload {
|
||||
case .fullsize:
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,6 +287,7 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
|
||||
message,
|
||||
priority: .userInitiated,
|
||||
useThumbnails: false,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -1409,6 +1455,7 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
|
||||
public func didTapSafetyTips() {
|
||||
let viewController = SafetyTipsViewController(
|
||||
mode: .messageRequest,
|
||||
primaryButton: SafetyTipsViewController.Button(
|
||||
title: CommonStrings.viewMoreButton,
|
||||
action: { [weak self] in
|
||||
|
||||
@ -295,6 +295,58 @@ 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()
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import SignalServiceKit
|
||||
|
||||
class CollapseSetInteraction: TSInteraction {
|
||||
final class CollapseSetInteraction: TSInteraction {
|
||||
|
||||
enum MessagesType: Equatable {
|
||||
case groupUpdates
|
||||
@ -18,8 +18,6 @@ class CollapseSetInteraction: TSInteraction {
|
||||
|
||||
let collapseSetType: MessagesType
|
||||
|
||||
let isExpanded: Bool
|
||||
|
||||
let finalTimerDescription: String?
|
||||
|
||||
override var isDynamicInteraction: Bool { true }
|
||||
@ -32,12 +30,10 @@ 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,
|
||||
@ -45,13 +41,17 @@ class CollapseSetInteraction: TSInteraction {
|
||||
|
||||
let firstInteraction = collapsedInteractions[0]
|
||||
super.init(
|
||||
customUniqueId: "CollapseSet_\(firstInteraction.timestamp)",
|
||||
customUniqueId: Self.id(firstInteraction: firstInteraction),
|
||||
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,
|
||||
|
||||
@ -427,6 +427,23 @@ 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()
|
||||
|
||||
|
||||
@ -80,6 +80,10 @@ 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
|
||||
@ -132,30 +136,35 @@ 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, _):
|
||||
@ -163,6 +172,7 @@ public class CVLoader: NSObject {
|
||||
aroundInteractionId: interactionId,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: transaction,
|
||||
)
|
||||
}
|
||||
@ -171,36 +181,18 @@ public class CVLoader: NSObject {
|
||||
throw error
|
||||
}
|
||||
|
||||
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
|
||||
let expandedInteractions = messageLoader.loadedDisplayableInteractions.flatMap { interaction in
|
||||
if
|
||||
let collapseSet = interaction as? CollapseSetInteraction,
|
||||
viewStateSnapshot.expandedCollapseSetIds.contains(collapseSet.uniqueId)
|
||||
{
|
||||
try messageLoader.loadOlderMessagePage(
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
tx: transaction,
|
||||
)
|
||||
processedInteractions = Self.preprocessInteractions(
|
||||
messageLoader.loadedInteractions,
|
||||
loadContext: loadContext,
|
||||
)
|
||||
extraLoads += 1
|
||||
return [collapseSet] + collapseSet.collapsedInteractions
|
||||
}
|
||||
return [interaction]
|
||||
}
|
||||
|
||||
let itemModels = self.buildItemModels(
|
||||
interactions: processedInteractions,
|
||||
interactions: expandedInteractions,
|
||||
loadContext: loadContext,
|
||||
updatedInteractionIds: updatedInteractionIds,
|
||||
localAci: localAci,
|
||||
@ -272,214 +264,6 @@ 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(
|
||||
|
||||
@ -42,7 +42,7 @@ struct CVViewStateSnapshot {
|
||||
let hasActiveCall: Bool
|
||||
let currentGroupThreadCallGroupId: GroupIdentifier?
|
||||
|
||||
let expandedCollapseSets: Set<String>
|
||||
let expandedCollapseSetIds: 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,
|
||||
expandedCollapseSets: viewState.expandedCollapseSets,
|
||||
expandedCollapseSetIds: viewState.expandedCollapseSets,
|
||||
)
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ struct CVViewStateSnapshot {
|
||||
oldestUnreadMessageSortId: nil,
|
||||
hasActiveCall: false,
|
||||
currentGroupThreadCallGroupId: nil,
|
||||
expandedCollapseSets: [],
|
||||
expandedCollapseSetIds: [],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,13 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
|
||||
private enum Constants {
|
||||
/// The maximum number of interactions to keep in memory. We start dropping
|
||||
/// interactions (in an LRU fashion) once we've exceeded this value.
|
||||
/// 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.
|
||||
///
|
||||
/// TODO: Should we reduce this value?
|
||||
static let maxInteractionCount = 500
|
||||
static let maxDisplayableInteractionCount = 500
|
||||
|
||||
static let maxCollapseSetSize = 50
|
||||
}
|
||||
|
||||
protocol MessageLoaderBatchFetcher {
|
||||
@ -28,11 +30,19 @@ 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.
|
||||
@ -90,10 +100,61 @@ 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(
|
||||
@ -101,6 +162,7 @@ class MessageLoader {
|
||||
count: initialLoadCount,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -108,6 +170,7 @@ class MessageLoader {
|
||||
func loadNewerMessagePage(
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
try ensureLoaded(
|
||||
@ -115,6 +178,7 @@ class MessageLoader {
|
||||
count: initialLoadCount * 2,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -122,6 +186,7 @@ class MessageLoader {
|
||||
func loadOlderMessagePage(
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
try ensureLoaded(
|
||||
@ -129,6 +194,7 @@ class MessageLoader {
|
||||
count: initialLoadCount * 2,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -136,6 +202,7 @@ class MessageLoader {
|
||||
func loadNewestMessagePage(
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
try ensureLoaded(
|
||||
@ -143,6 +210,7 @@ class MessageLoader {
|
||||
count: initialLoadCount,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -151,6 +219,7 @@ class MessageLoader {
|
||||
focusMessageId: String?,
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
if let focusMessageId {
|
||||
@ -159,12 +228,14 @@ class MessageLoader {
|
||||
count: initialLoadCount,
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
} else {
|
||||
try loadNewestMessagePage(
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -173,13 +244,15 @@ class MessageLoader {
|
||||
func loadSameLocation(
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
try ensureLoaded(
|
||||
.sameLocation,
|
||||
count: max(initialLoadCount, loadedInteractions.count),
|
||||
count: max(initialLoadCount, loadedDisplayableInteractions.count),
|
||||
reusableInteractions: reusableInteractions,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
preprocessingContext: preprocessingContext,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
@ -195,21 +268,122 @@ class MessageLoader {
|
||||
count: Int,
|
||||
reusableInteractions: [String: TSInteraction],
|
||||
deletedInteractionIds: Set<String>?,
|
||||
preprocessingContext: MessageLoaderPreprocessingContext?,
|
||||
tx: DBReadTransaction,
|
||||
) throws {
|
||||
owsAssertDebug(count > 0)
|
||||
let count = count.clamp(1, Constants.maxInteractionCount)
|
||||
let loadBatch = try buildLoadBatch(
|
||||
|
||||
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(
|
||||
direction,
|
||||
count: count,
|
||||
deletedInteractionIds: deletedInteractionIds,
|
||||
tx: tx,
|
||||
)
|
||||
loadedInteractions = fetchInteractions(
|
||||
uniqueIds: loadBatch.uniqueIds,
|
||||
|
||||
var loadedPage = buildLoadedPage(
|
||||
for: loadBatch,
|
||||
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
|
||||
}
|
||||
@ -228,24 +402,6 @@ 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)
|
||||
@ -265,8 +421,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)
|
||||
try fetchNewer(after: rowId, count: count - olderCount, batch: &batch)
|
||||
let olderCount = try fetchOlder(before: rowId, count: count / 2, batch: &batch, tx: tx)
|
||||
try fetchNewer(after: rowId, count: count - olderCount, batch: &batch, tx: tx)
|
||||
return batch
|
||||
}
|
||||
|
||||
@ -311,7 +467,7 @@ class MessageLoader {
|
||||
return batch
|
||||
case .older:
|
||||
var batch = priorLoad.batch
|
||||
try fetchOlder(before: priorLoad.range.lowerBound, count: count, batch: &batch)
|
||||
try fetchOlder(before: priorLoad.range.lowerBound, count: count, batch: &batch, tx: tx)
|
||||
return batch
|
||||
case .sameLocation where !priorLoad.batch.canLoadNewer:
|
||||
// If we're loading at the same location and are already at the end of the
|
||||
@ -319,13 +475,13 @@ class MessageLoader {
|
||||
fallthrough
|
||||
case .newer:
|
||||
var batch = priorLoad.batch
|
||||
try fetchNewer(after: priorLoad.range.upperBound, count: count, batch: &batch)
|
||||
try fetchNewer(after: priorLoad.range.upperBound, count: count, batch: &batch, tx: tx)
|
||||
return batch
|
||||
case .sameLocation:
|
||||
var batch = priorLoad.batch
|
||||
if batch.uniqueIds.count < initialLoadCount {
|
||||
try fetchOlder(before: priorLoad.range.lowerBound, count: initialLoadCount, batch: &batch)
|
||||
try fetchNewer(after: priorLoad.range.upperBound, count: initialLoadCount, batch: &batch)
|
||||
try fetchOlder(before: priorLoad.range.lowerBound, count: initialLoadCount, batch: &batch, tx: tx)
|
||||
try fetchNewer(after: priorLoad.range.upperBound, count: initialLoadCount, batch: &batch, tx: tx)
|
||||
}
|
||||
return batch
|
||||
case .around(interactionUniqueId: let uniqueId):
|
||||
@ -343,6 +499,32 @@ 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] = [:],
|
||||
@ -360,6 +542,268 @@ 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: -
|
||||
@ -447,8 +891,6 @@ 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 {
|
||||
@ -458,8 +900,6 @@ 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
|
||||
@ -494,24 +934,4 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,9 +32,6 @@ 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,
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import GRDB
|
||||
import MultipeerConnectivity
|
||||
import SignalServiceKit
|
||||
|
||||
@ -366,7 +367,15 @@ 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 { _ in
|
||||
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!)
|
||||
do {
|
||||
let dbCopy = try Self.makeLocalCopy(databaseFile: database.database)
|
||||
let walCopy = try Self.makeLocalCopy(databaseFile: database.wal)
|
||||
|
||||
@ -113,7 +113,7 @@ extension EmojiReactionPickerConfigViewController: MessageReactionPickerDelegate
|
||||
present(picker, animated: true)
|
||||
}
|
||||
|
||||
func didSelectAnyEmoji() {
|
||||
func didSelectShowFullEmojiPicker() {
|
||||
// No-op for configuration
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "official_wallpaper_reduced.pdf",
|
||||
"filename" : "official-wallpaper.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
Signal/Images.xcassets/official-wallpaper.imageset/official-wallpaper.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/official-wallpaper.imageset/official-wallpaper.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
12
Signal/Images.xcassets/safety-tips/safetytip_48_04.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/safety-tips/safetytip_48_04.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "safetytip_48_pin.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/safety-tips/safetytip_48_04.imageset/safetytip_48_pin.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/safety-tips/safetytip_48_04.imageset/safetytip_48_pin.pdf
vendored
Normal file
Binary file not shown.
12
Signal/Images.xcassets/safety-tips/safetytip_48_05.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/safety-tips/safetytip_48_05.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "safetytip_48_lock.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/safety-tips/safetytip_48_05.imageset/safetytip_48_lock.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/safety-tips/safetytip_48_05.imageset/safetytip_48_lock.pdf
vendored
Normal file
Binary file not shown.
@ -3,369 +3,666 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Contacts
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class ExperienceUpgradeManager {
|
||||
|
||||
private weak static var lastPresented: ExperienceUpgradeView?
|
||||
private enum StoreKeys {
|
||||
static let lastMegaphoneDismissDate = "lastExperienceUpgradeDismissDate"
|
||||
}
|
||||
|
||||
static func presentNext(fromViewController: UIViewController) -> Bool {
|
||||
let db = DependenciesBridge.shared.db
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
private static var lastPresentedMegaphone: Megaphone?
|
||||
private static var lastPresentedMegaphoneView: MegaphoneView?
|
||||
|
||||
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 optionalNext = db.read { transaction -> ExperienceUpgrade? in
|
||||
let tx = transaction
|
||||
|
||||
let lastMegaphoneDismissDate: Date
|
||||
let nextMegaphone: Megaphone?
|
||||
(
|
||||
lastMegaphoneDismissDate,
|
||||
nextMegaphone,
|
||||
) = db.read { tx in
|
||||
guard
|
||||
let registeredState = try? tsAccountManager.registeredState(tx: tx),
|
||||
let registrationDate = tsAccountManager.registrationDate(tx: tx)
|
||||
else {
|
||||
return nil
|
||||
return (.distantPast, nil)
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let timeIntervalSinceRegistration = now.timeIntervalSince(registrationDate)
|
||||
let lastMegaphoneDismissDate = keyValueStore.fetchValue(
|
||||
Date.self,
|
||||
forKey: StoreKeys.lastMegaphoneDismissDate,
|
||||
tx: tx,
|
||||
) ?? .distantPast
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return false
|
||||
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,
|
||||
)
|
||||
}
|
||||
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:
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
if shouldClearNewDeviceNotification {
|
||||
DependenciesBridge.shared.db.write { tx in
|
||||
DependenciesBridge.shared.deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
db.write { tx in
|
||||
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldClearBackupsEnabledDetails {
|
||||
DependenciesBridge.shared.db.write { tx in
|
||||
BackupSettingsStore().clearLastBackupEnabledDetails(tx: tx)
|
||||
db.write { tx in
|
||||
backupSettingsStore.clearLastBackupEnabledDetails(tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
// If we already have presented this experience upgrade, do nothing.
|
||||
guard
|
||||
let next = optionalNext,
|
||||
lastPresented?.experienceUpgrade.manifest != next.manifest
|
||||
else {
|
||||
if optionalNext == nil {
|
||||
dismissLastPresented()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
guard let nextMegaphone else {
|
||||
_ = dismissLastPresented(now: now)
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
let lastPresentedMegaphone,
|
||||
lastPresentedMegaphone.experienceUpgrade.manifest == nextMegaphone.experienceUpgrade.manifest
|
||||
{
|
||||
megaphone.present(fromViewController: fromViewController)
|
||||
lastPresented = megaphone
|
||||
didPresentView = true
|
||||
} else {
|
||||
didPresentView = false
|
||||
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
|
||||
ExperienceUpgradeFinder.markAsViewed(experienceUpgrade: next, transaction: tx)
|
||||
keyValueStore.writeValue(
|
||||
now,
|
||||
forKey: StoreKeys.lastMegaphoneDismissDate,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
|
||||
return didPresentView
|
||||
lastPresentedMegaphoneView.dismiss()
|
||||
self.lastPresentedMegaphone = nil
|
||||
self.lastPresentedMegaphoneView = nil
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Experience Specific Helpers
|
||||
// MARK: - Megaphone Preconditions
|
||||
|
||||
static func dismissPINReminderIfNecessary() {
|
||||
dismissLastPresented(ifMatching: .pinReminder)
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)
|
||||
private static func checkPreconditionsForNotificationsPermissionsReminder() -> Bool {
|
||||
let (promise, future) = Promise<Bool>.pending()
|
||||
|
||||
transaction.addSyncCompletion {
|
||||
Task { @MainActor in
|
||||
dismissLastPresented(ifMatching: manifest)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
future.resolve(settings.authorizationStatus == .authorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func dismissLastPresented(ifMatching manifest: ExperienceUpgradeManifest? = nil) {
|
||||
guard let lastPresented else {
|
||||
return
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
|
||||
guard promise.result == nil else { return }
|
||||
future.reject(OWSGenericError("timeout fetching notification permissions"))
|
||||
}
|
||||
|
||||
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:
|
||||
do {
|
||||
return !(try promise.wait())
|
||||
} catch {
|
||||
Logger.warn("failed to query notification permission")
|
||||
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
|
||||
}
|
||||
private enum NewLinkedDeviceNotificationResult {
|
||||
case display(MostRecentlyLinkedDeviceDetails)
|
||||
case skip
|
||||
case clearNotification
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExperienceUpgradeView
|
||||
private static func checkPreconditionsForNewLinkedDeviceNotification(
|
||||
tx: DBReadTransaction,
|
||||
) -> NewLinkedDeviceNotificationResult {
|
||||
guard
|
||||
let mostRecentlyLinkedDeviceDetails = deviceStore.mostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
else {
|
||||
return .skip
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
func markAsCompleteWithSneakyTransaction() {
|
||||
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
||||
ExperienceUpgradeFinder.markAsComplete(
|
||||
experienceUpgrade: self.experienceUpgrade,
|
||||
transaction: transaction,
|
||||
)
|
||||
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.
|
||||
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
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,20 +8,36 @@ public import SignalServiceKit
|
||||
|
||||
/// Handles fetching and parsing remote megaphones.
|
||||
public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation> {
|
||||
private let experienceUpgradeStore: ExperienceUpgradeStore
|
||||
|
||||
override init(
|
||||
db: DB,
|
||||
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
|
||||
) {
|
||||
self.experienceUpgradeStore = ExperienceUpgradeStore()
|
||||
|
||||
super.init(
|
||||
db: db,
|
||||
remoteReleaseNotesService: remoteReleaseNotesService,
|
||||
)
|
||||
}
|
||||
|
||||
/// Update our local persisted megaphone state with freshly-fetched
|
||||
/// megaphones from the service. Updates existing megaphones if present,
|
||||
/// and creates new ones if necessary. Removes any locally-persisted
|
||||
/// megaphones that no longer exist on the service.
|
||||
override func updatePersistedData(
|
||||
withFetchedData fetchedTranslations: [(RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation)],
|
||||
transaction: DBWriteTransaction,
|
||||
transaction tx: DBWriteTransaction,
|
||||
) {
|
||||
// Get the current remote megaphones.
|
||||
var localRemoteMegaphones: [String: ExperienceUpgrade] = [:]
|
||||
ExperienceUpgrade.anyEnumerate(transaction: transaction) { upgrade, _ in
|
||||
if case .remoteMegaphone = upgrade.manifest {
|
||||
localRemoteMegaphones[upgrade.uniqueId] = upgrade
|
||||
// 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
|
||||
}
|
||||
|
||||
experienceUpgradesByMegaphoneId[model.manifest.id] = experienceUpgrade
|
||||
}
|
||||
|
||||
// Insert all megaphones we got from the service. If we already have a
|
||||
@ -30,23 +46,28 @@ public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneMo
|
||||
// For example, if the user's locale has changed we may have updated
|
||||
// translations.
|
||||
for (manifest, translation) in fetchedTranslations {
|
||||
let serviceMegaphone = RemoteMegaphoneModel(manifest: manifest, translation: translation)
|
||||
if let existingLocalMegaphone = localRemoteMegaphones[serviceMegaphone.id] {
|
||||
existingLocalMegaphone.updateManifestRemoteMegaphone(withRefetchedMegaphone: serviceMegaphone)
|
||||
existingLocalMegaphone.anyUpsert(transaction: transaction)
|
||||
|
||||
localRemoteMegaphones.removeValue(forKey: serviceMegaphone.id)
|
||||
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: serviceMegaphone))
|
||||
.anyInsert(transaction: transaction)
|
||||
experienceUpgrade = .makeNew(withManifest: .remoteMegaphone(megaphone: remoteMegaphoneModel))
|
||||
}
|
||||
|
||||
experienceUpgradeStore.upsertRemoteMegaphone(
|
||||
experienceUpgrade: experienceUpgrade,
|
||||
newRemoteMegaphoneModel: remoteMegaphoneModel,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove records for any remaining local megaphones, which are no
|
||||
// longer on the service.
|
||||
for (_, experienceUpgradeToRemove) in localRemoteMegaphones {
|
||||
experienceUpgradeToRemove.anyRemove(transaction: transaction)
|
||||
for (_, experienceUpgradeToRemove) in experienceUpgradesByMegaphoneId {
|
||||
experienceUpgradeStore.remove(
|
||||
experienceUpgrade: experienceUpgradeToRemove,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
class BackupEnablementMegaphone: MegaphoneView {
|
||||
class BackupEnablementMegaphone: Megaphone {
|
||||
init(
|
||||
experienceUpgrade: ExperienceUpgrade,
|
||||
fromViewController: UIViewController,
|
||||
@ -22,7 +22,7 @@ class BackupEnablementMegaphone: MegaphoneView {
|
||||
"BACKUP_ENABLEMENT_REMINDER_MEGAPHONE_BODY",
|
||||
comment: "Body for Backup enablement reminder megaphone",
|
||||
)
|
||||
imageName = "backups-logo"
|
||||
image = .backupsLogo
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString(
|
||||
"BACKUP_ENABLEMENT_REMINDER_MEGAPHONE_ACTION",
|
||||
@ -33,10 +33,9 @@ class BackupEnablementMegaphone: MegaphoneView {
|
||||
comment: "Snooze text for Backup enablement reminder megaphone",
|
||||
)
|
||||
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
|
||||
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
|
||||
SignalApp.shared.showAppSettings(mode: .backups())
|
||||
self?.markAsSnoozedWithSneakyTransaction()
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
let secondaryButton = snoozeButton(
|
||||
@ -44,7 +43,7 @@ class BackupEnablementMegaphone: MegaphoneView {
|
||||
snoozeTitle: secondaryButtonTitle,
|
||||
)
|
||||
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
buttons = [primaryButton, secondaryButton]
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
|
||||
@ -7,7 +7,7 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
class BackupsEnabledNotificationMegaphone: MegaphoneView {
|
||||
class BackupsEnabledNotificationMegaphone: Megaphone {
|
||||
private let db: DB
|
||||
private let backupSettingsStore: BackupSettingsStore
|
||||
init(
|
||||
@ -34,33 +34,33 @@ class BackupsEnabledNotificationMegaphone: MegaphoneView {
|
||||
),
|
||||
backupsEnabledTime.formatted(date: .omitted, time: .shortened),
|
||||
)
|
||||
imageName = "backups-logo"
|
||||
image = .backupsLogo
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString(
|
||||
"BACKUPS_VIEW_SETTINGS_BUTTON",
|
||||
comment: "Action text for backups enabled megaphone taking user to backup settings",
|
||||
)
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
|
||||
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
|
||||
SignalApp.shared.showAppSettings(mode: .backups())
|
||||
self?.markAsViewed()
|
||||
self?.dismiss(animated: true)
|
||||
self?.stopShowing()
|
||||
}
|
||||
|
||||
let secondaryButton = MegaphoneView.Button(title: CommonStrings.okButton) { [weak self] in
|
||||
self?.markAsViewed()
|
||||
self?.dismiss(animated: true)
|
||||
let secondaryButton = Button(title: CommonStrings.okButton) { [weak self] in
|
||||
self?.stopShowing()
|
||||
}
|
||||
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
buttons = [primaryButton, secondaryButton]
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func markAsViewed() {
|
||||
private func stopShowing() {
|
||||
db.write { tx in
|
||||
backupSettingsStore.clearLastBackupEnabledDetails(tx: tx)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,7 @@
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
weak var actionSheetController: ActionSheetController?
|
||||
|
||||
class ContactPermissionReminderMegaphone: Megaphone {
|
||||
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
|
||||
super.init(experienceUpgrade: experienceUpgrade)
|
||||
|
||||
@ -20,15 +18,16 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
"CONTACT_PERMISSION_REMINDER_MEGAPHONE_BODY",
|
||||
comment: "Body for contact permission reminder megaphone",
|
||||
)
|
||||
imageName = "contacts"
|
||||
image = .contacts
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString(
|
||||
"CONTACT_PERMISSION_REMINDER_MEGAPHONE_ACTION",
|
||||
comment: "Action text for contact permission reminder megaphone",
|
||||
)
|
||||
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
|
||||
guard let self else { return }
|
||||
let primaryButton = Button(title: primaryButtonTitle) {
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.isCancelable = true
|
||||
|
||||
let turnOnView: TurnOnPermissionView
|
||||
if #available(iOS 18, *) {
|
||||
@ -37,6 +36,7 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
turnOnView = TurnOnPermissionView(
|
||||
fromActionSheetController: actionSheetController,
|
||||
title: OWSLocalizedString(
|
||||
"CONTACT_PERMISSION_ACTION_SHEET_2_TITLE",
|
||||
comment: "Title for contact permission action sheet",
|
||||
@ -71,6 +71,7 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
)
|
||||
} else {
|
||||
turnOnView = TurnOnPermissionView(
|
||||
fromActionSheetController: actionSheetController,
|
||||
title: OWSLocalizedString(
|
||||
"CONTACT_PERMISSION_ACTION_SHEET_TITLE",
|
||||
comment: "Title for contact permission action sheet",
|
||||
@ -98,11 +99,8 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
)
|
||||
}
|
||||
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.customHeader = turnOnView
|
||||
actionSheetController.isCancelable = true
|
||||
fromViewController.presentActionSheet(actionSheetController)
|
||||
self.actionSheetController = actionSheetController
|
||||
}
|
||||
|
||||
let secondaryButton = snoozeButton(
|
||||
@ -112,15 +110,11 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
|
||||
comment: "Snooze action text for contact permission reminder megaphone",
|
||||
),
|
||||
)
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
|
||||
buttons = [primaryButton, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
class CreateUsernameMegaphone: MegaphoneView {
|
||||
class CreateUsernameMegaphone: Megaphone {
|
||||
private let usernameSelectionCoordinator: UsernameSelectionCoordinator
|
||||
|
||||
init(
|
||||
@ -29,7 +29,7 @@ class CreateUsernameMegaphone: MegaphoneView {
|
||||
comment: "Body text for an interactive in-app prompt to set up a Signal username.",
|
||||
)
|
||||
|
||||
imageName = "usernames-48-color"
|
||||
image = .usernames48
|
||||
imageContentMode = .center
|
||||
|
||||
let setUpButton = Button(title: CommonStrings.learnMore) { [weak self, weak fromViewController] in
|
||||
@ -46,7 +46,7 @@ class CreateUsernameMegaphone: MegaphoneView {
|
||||
self.onNotNowTapped()
|
||||
}
|
||||
|
||||
setButtons(primary: setUpButton, secondary: notNowButton)
|
||||
buttons = [setUpButton, notNowButton]
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use other constructor!")
|
||||
@ -56,15 +56,10 @@ class CreateUsernameMegaphone: MegaphoneView {
|
||||
|
||||
private func onSetUpTapped(fromViewController: UIViewController) {
|
||||
markAsSnoozedWithSneakyTransaction()
|
||||
|
||||
dismiss(animated: true) {
|
||||
self.usernameSelectionCoordinator.present(fromViewController: fromViewController)
|
||||
}
|
||||
usernameSelectionCoordinator.present(fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
private func onNotNowTapped() {
|
||||
markAsSnoozedWithSneakyTransaction()
|
||||
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,7 @@
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
final class InactiveLinkedDeviceReminderMegaphone: MegaphoneView {
|
||||
private var inactiveLinkedDeviceFinder: InactiveLinkedDeviceFinder {
|
||||
DependenciesBridge.shared.inactiveLinkedDeviceFinder
|
||||
}
|
||||
|
||||
final class InactiveLinkedDeviceReminderMegaphone: Megaphone {
|
||||
private let inactiveLinkedDevice: InactiveLinkedDevice
|
||||
|
||||
/// The number of days until the linked device represented by this megaphone
|
||||
@ -50,22 +46,21 @@ final class InactiveLinkedDeviceReminderMegaphone: MegaphoneView {
|
||||
inactiveLinkedDevice.displayName,
|
||||
)
|
||||
|
||||
imageName = "inactive-linked-device-reminder-megaphone"
|
||||
image = .inactiveLinkedDeviceReminderMegaphone
|
||||
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.",
|
||||
)) {
|
||||
DependenciesBridge.shared.db.asyncWrite(
|
||||
block: { tx in
|
||||
self.inactiveLinkedDeviceFinder.permanentlyDisableFinders(tx: tx)
|
||||
},
|
||||
completionQueue: .main,
|
||||
completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
},
|
||||
)
|
||||
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)
|
||||
}
|
||||
let gotItButton = snoozeButton(
|
||||
fromViewController: fromViewController,
|
||||
@ -74,7 +69,8 @@ final class InactiveLinkedDeviceReminderMegaphone: MegaphoneView {
|
||||
comment: "Title for a button in an in-app megaphone about a user's inactive linked device, temporarily dismissing the megaphone.",
|
||||
),
|
||||
)
|
||||
setButtons(primary: gotItButton, secondary: dontRemindMeButton)
|
||||
|
||||
buttons = [gotItButton, dontRemindMeButton]
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use other constructor!")
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import SafariServices
|
||||
import SignalServiceKit
|
||||
|
||||
final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
|
||||
final class InactivePrimaryDeviceReminderMegaphone: Megaphone {
|
||||
init(
|
||||
fromViewController: UIViewController,
|
||||
experienceUpgrade: ExperienceUpgrade,
|
||||
@ -23,7 +23,7 @@ final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
|
||||
comment: "Body for an in-app megaphone about a user's inactive primary device.",
|
||||
)
|
||||
|
||||
imageName = "phone-warning"
|
||||
image = .phoneWarning
|
||||
imageContentMode = .center
|
||||
|
||||
let viewControllerRef = fromViewController
|
||||
@ -41,7 +41,8 @@ final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
|
||||
comment: "Title for a button in an in-app megaphone about a user's inactive primary device, temporarily dismissing the megaphone.",
|
||||
),
|
||||
)
|
||||
setButtons(primary: gotItButton, secondary: learnMoreButton)
|
||||
|
||||
buttons = [gotItButton, learnMoreButton]
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Use other constructor!")
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@ -7,91 +7,107 @@ import Lottie
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
class Megaphone {
|
||||
struct Button {
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
}
|
||||
|
||||
private var buttons: [Button] = []
|
||||
func setButtons(primary: Button, secondary: Button? = nil) {
|
||||
assert(!hasPresented)
|
||||
let experienceUpgrade: ExperienceUpgrade
|
||||
var image: UIImage?
|
||||
var imageContentMode: UIView.ContentMode = .scaleAspectFit
|
||||
var titleText: String?
|
||||
var bodyText: String?
|
||||
var buttons: [Button] = []
|
||||
|
||||
if let secondary {
|
||||
buttons = [primary, secondary]
|
||||
} else {
|
||||
buttons = [primary]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var isPresented: Bool { superview != nil }
|
||||
// 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]
|
||||
|
||||
private let darkThemeBackgroundOverlay = UIView()
|
||||
private let stackView = UIStackView()
|
||||
init(experienceUpgrade: ExperienceUpgrade) {
|
||||
self.experienceUpgrade = experienceUpgrade
|
||||
|
||||
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
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
@ -118,23 +134,16 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var hasPresented = false
|
||||
// MARK: -
|
||||
|
||||
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 imageName != nil || image != nil || animation != nil {
|
||||
topStackSubviews = [createImageContainer(), labelStack]
|
||||
if let image {
|
||||
topStackSubviews = [createImageContainer(image: image), labelStack]
|
||||
} else {
|
||||
topStackSubviews = [labelStack]
|
||||
}
|
||||
@ -146,51 +155,31 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
topStackView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
|
||||
|
||||
stackView.addArrangedSubview(topStackView)
|
||||
|
||||
// Buttons
|
||||
|
||||
if buttons.count > 0 {
|
||||
stackView.addArrangedSubview(createButtonsStack())
|
||||
} else {
|
||||
assert(buttons.isEmpty)
|
||||
addDismissButton()
|
||||
}
|
||||
stackView.addArrangedSubview(createButtonsStack())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
hasPresented = true
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
removeFromSuperview()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
private func applyTheme() {
|
||||
darkThemeBackgroundOverlay.isHidden = !Theme.isDarkThemeEnabled
|
||||
}
|
||||
|
||||
@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 {
|
||||
private func createLabelStack() -> UIStackView {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
@ -216,47 +205,15 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
return labelStack
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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()
|
||||
|
||||
container.autoSetDimension(.width, toSize: 64)
|
||||
container.autoSetDimension(.height, toSize: 64, relation: .greaterThanOrEqual)
|
||||
@ -264,7 +221,10 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
return container
|
||||
}
|
||||
|
||||
func createButtonView(_ button: Button, font: UIFont = .regularFont(ofSize: 15)) -> OWSFlatButton {
|
||||
private func createButtonView(
|
||||
_ button: Megaphone.Button,
|
||||
font: UIFont = .regularFont(ofSize: 15),
|
||||
) -> OWSFlatButton {
|
||||
let buttonView = OWSFlatButton()
|
||||
|
||||
buttonView.setTitle(title: button.title, font: font, titleColor: Theme.darkThemePrimaryColor)
|
||||
@ -275,30 +235,26 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
return buttonView
|
||||
}
|
||||
|
||||
func createButtonsStack() -> UIStackView {
|
||||
private func createButtonsStack() -> UIStackView {
|
||||
let buttonsStack = UIStackView()
|
||||
buttonsStack.addBackgroundView(withBackgroundColor: .ows_blackAlpha20)
|
||||
|
||||
switch buttons.count {
|
||||
case 1:
|
||||
buttonsStack.addArrangedSubview(createButtonView(buttons[0]))
|
||||
buttonsStack.addArrangedSubview(createButtonView(
|
||||
buttons[0],
|
||||
font: .regularFont(ofSize: 15),
|
||||
))
|
||||
case 2:
|
||||
var previousButton: UIView?
|
||||
for button in buttons {
|
||||
let buttonView = createButtonView(
|
||||
button,
|
||||
font: previousButton == nil ? UIFont.semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
|
||||
font: previousButton == nil ? .semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
|
||||
)
|
||||
|
||||
switch buttonOrientation {
|
||||
case .vertical:
|
||||
buttonsStack.addArrangedSubview(buttonView)
|
||||
case .horizontal:
|
||||
buttonsStack.insertArrangedSubview(buttonView, at: 0)
|
||||
}
|
||||
buttonsStack.insertArrangedSubview(buttonView, at: 0)
|
||||
|
||||
previousButton?.autoMatch(.width, to: .width, of: buttonView)
|
||||
|
||||
previousButton = buttonView
|
||||
}
|
||||
|
||||
@ -307,44 +263,14 @@ class MegaphoneView: UIView, ExperienceUpgradeView {
|
||||
divider.backgroundColor = .ows_whiteAlpha20
|
||||
dividerContainer.addSubview(divider)
|
||||
buttonsStack.insertArrangedSubview(dividerContainer, at: 1)
|
||||
|
||||
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)
|
||||
}
|
||||
buttonsStack.axis = .horizontal
|
||||
divider.autoSetDimension(.width, toSize: 1)
|
||||
divider.autoPinWidthToSuperview()
|
||||
divider.autoPinHeightToSuperview(withMargin: 8)
|
||||
default:
|
||||
owsFailDebug("only supports 1 or 2 buttons")
|
||||
owsFail("Megaphones must have one or two 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import SignalServiceKit
|
||||
|
||||
final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
|
||||
final class NewLinkedDeviceNotificationMegaphone: Megaphone {
|
||||
private let db: DB
|
||||
private let deviceStore: OWSDeviceStore
|
||||
|
||||
@ -19,7 +19,7 @@ final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
|
||||
self.deviceStore = deviceStore
|
||||
super.init(experienceUpgrade: experienceUpgrade)
|
||||
|
||||
imageName = "inactive-linked-device-reminder-megaphone"
|
||||
image = .inactiveLinkedDeviceReminderMegaphone
|
||||
imageContentMode = .center
|
||||
titleText = OWSLocalizedString(
|
||||
"LINKED_DEVICE_NOTIFICATION_TITLE",
|
||||
@ -45,18 +45,16 @@ final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
|
||||
),
|
||||
) { [weak self] in
|
||||
SignalApp.shared.showAppSettings(mode: .linkedDevices)
|
||||
self?.markAsViewed()
|
||||
self?.dismiss()
|
||||
self?.stopShowing()
|
||||
}
|
||||
|
||||
let acknowledgeButton = Button(
|
||||
title: CommonStrings.acknowledgeButton,
|
||||
) { [weak self] in
|
||||
self?.markAsViewed()
|
||||
self?.dismiss()
|
||||
self?.stopShowing()
|
||||
}
|
||||
|
||||
setButtons(primary: acknowledgeButton, secondary: viewDeviceButton)
|
||||
buttons = [acknowledgeButton, viewDeviceButton]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -64,9 +62,11 @@ final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func markAsViewed() {
|
||||
private func stopShowing() {
|
||||
db.write { tx in
|
||||
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,7 @@
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class NotificationPermissionReminderMegaphone: MegaphoneView {
|
||||
weak var actionSheetController: ActionSheetController?
|
||||
|
||||
class NotificationPermissionReminderMegaphone: Megaphone {
|
||||
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
|
||||
super.init(experienceUpgrade: experienceUpgrade)
|
||||
|
||||
@ -20,17 +18,19 @@ class NotificationPermissionReminderMegaphone: MegaphoneView {
|
||||
"NOTIFICATION_PERMISSION_REMINDER_MEGAPHONE_BODY",
|
||||
comment: "Body for notification permission reminder megaphone",
|
||||
)
|
||||
imageName = "notificationMegaphone"
|
||||
image = .notificationMegaphone
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString(
|
||||
"NOTIFICATION_PERMISSION_REMINDER_MEGAPHONE_ACTION",
|
||||
comment: "Action text for notification permission reminder megaphone",
|
||||
)
|
||||
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
|
||||
guard let self else { return }
|
||||
let primaryButton = Button(title: primaryButtonTitle) {
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.isCancelable = true
|
||||
|
||||
let turnOnView = TurnOnPermissionView(
|
||||
fromActionSheetController: actionSheetController,
|
||||
title: OWSLocalizedString(
|
||||
"NOTIFICATION_PERMISSION_ACTION_SHEET_TITLE",
|
||||
comment: "Title for notification permission action sheet",
|
||||
@ -64,11 +64,8 @@ class NotificationPermissionReminderMegaphone: MegaphoneView {
|
||||
],
|
||||
)
|
||||
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.customHeader = turnOnView
|
||||
actionSheetController.isCancelable = true
|
||||
fromViewController.presentActionSheet(actionSheetController)
|
||||
self.actionSheetController = actionSheetController
|
||||
}
|
||||
|
||||
let secondaryButton = snoozeButton(
|
||||
@ -78,26 +75,29 @@ class NotificationPermissionReminderMegaphone: MegaphoneView {
|
||||
comment: "Snooze action text for contact permission reminder megaphone",
|
||||
),
|
||||
)
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
|
||||
buttons = [primaryButton, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class TurnOnPermissionView: UIStackView {
|
||||
struct Step {
|
||||
let icon: UIImage?
|
||||
let text: String
|
||||
}
|
||||
|
||||
init(title: String, message: String, steps: [Step], button: UIButton? = nil) {
|
||||
init(
|
||||
fromActionSheetController: ActionSheetController,
|
||||
title: String,
|
||||
message: String,
|
||||
steps: [Step],
|
||||
) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
axis = .vertical
|
||||
@ -121,10 +121,14 @@ class TurnOnPermissionView: UIStackView {
|
||||
}
|
||||
|
||||
// Button
|
||||
let primaryButton = button ?? UIButton(
|
||||
let primaryButton = UIButton(
|
||||
configuration: .largePrimary(title: CommonStrings.goToSettingsButton),
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
self?.goToSettings()
|
||||
primaryAction: UIAction { [weak self, weak fromActionSheetController] _ in
|
||||
guard let self, let fromActionSheetController else { return }
|
||||
|
||||
fromActionSheetController.dismiss(animated: true) {
|
||||
self.goToSettings()
|
||||
}
|
||||
},
|
||||
)
|
||||
let buttonContainer = UIView.container()
|
||||
|
||||
@ -7,46 +7,47 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
class PinReminderMegaphone: MegaphoneView {
|
||||
class PinReminderMegaphone: Megaphone {
|
||||
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")
|
||||
imageName = "PIN_megaphone"
|
||||
image = .pinMegaphone
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString("PIN_REMINDER_MEGAPHONE_ACTION", comment: "Action text for PIN reminder megaphone")
|
||||
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
|
||||
let vc = PinReminderViewController { result in
|
||||
let primaryButton = Button(title: primaryButtonTitle) { [weak fromViewController] in
|
||||
guard let fromViewController else { return }
|
||||
|
||||
let vc = PinReminderViewController { [weak self] pinReminderViewController, result in
|
||||
// Always dismiss the PIN reminder view (we dismiss the *megaphone* later).
|
||||
fromViewController.dismiss(animated: true)
|
||||
pinReminderViewController.dismiss(animated: true)
|
||||
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .succeeded:
|
||||
self.dismiss(animated: false)
|
||||
self.presentToastForNewRepetitionInterval(
|
||||
presentToastForNewRepetitionInterval(
|
||||
wasSuccessful: true,
|
||||
fromViewController: fromViewController,
|
||||
)
|
||||
case .canceled(didGuessWrong: true):
|
||||
self.dismiss(animated: false)
|
||||
self.presentToastForNewRepetitionInterval(
|
||||
presentToastForNewRepetitionInterval(
|
||||
wasSuccessful: false,
|
||||
fromViewController: fromViewController,
|
||||
)
|
||||
case .changedPin:
|
||||
self.dismiss(animated: false)
|
||||
case .canceled(didGuessWrong: false):
|
||||
case .changedPin, .canceled(didGuessWrong: false):
|
||||
break
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
|
||||
}
|
||||
|
||||
fromViewController.present(vc, animated: true)
|
||||
}
|
||||
|
||||
setButtons(primary: primaryButton)
|
||||
buttons = [primaryButton]
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
@ -103,6 +104,6 @@ class PinReminderMegaphone: MegaphoneView {
|
||||
toastText = MegaphoneStrings.weWillRemindYouLater
|
||||
}
|
||||
|
||||
presentToast(text: toastText, fromViewController: fromViewController)
|
||||
fromViewController.presentToast(text: toastText)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
class RecoveryKeyReminderMegaphone: MegaphoneView {
|
||||
class RecoveryKeyReminderMegaphone: Megaphone {
|
||||
init(
|
||||
experienceUpgrade: ExperienceUpgrade,
|
||||
fromViewController: UIViewController,
|
||||
@ -22,7 +22,7 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
|
||||
"BACKUP_KEY_REMINDER_MEGAPHONE_BODY",
|
||||
comment: "Body for Recovery Key reminder megaphone",
|
||||
)
|
||||
imageName = "backups-key"
|
||||
image = .backupsKey
|
||||
|
||||
let primaryButtonTitle = OWSLocalizedString(
|
||||
"BACKUP_KEY_REMINDER_MEGAPHONE_ACTION",
|
||||
@ -33,7 +33,7 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
|
||||
comment: "Snooze text for Recovery Key reminder megaphone",
|
||||
)
|
||||
|
||||
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) {
|
||||
let primaryButton = Button(title: primaryButtonTitle) {
|
||||
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
|
||||
let backupSettingsStore = BackupSettingsStore()
|
||||
let db = DependenciesBridge.shared.db
|
||||
@ -46,11 +46,17 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
|
||||
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()
|
||||
}
|
||||
@ -60,19 +66,10 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
|
||||
snoozeTitle: secondaryButtonTitle,
|
||||
)
|
||||
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
buttons = [primaryButton, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class RemoteMegaphone: MegaphoneView {
|
||||
class RemoteMegaphone: Megaphone {
|
||||
private let megaphoneModel: RemoteMegaphoneModel
|
||||
|
||||
init(
|
||||
@ -31,7 +31,7 @@ class RemoteMegaphone: MegaphoneView {
|
||||
}
|
||||
|
||||
if let primary = megaphoneModel.presentablePrimaryAction {
|
||||
let primaryButton = MegaphoneView.Button(title: primary.presentableText) { [weak self, weak fromViewController] in
|
||||
let primaryButton = Button(title: primary.presentableText) { [weak self, weak fromViewController] in
|
||||
guard
|
||||
let self,
|
||||
let fromViewController
|
||||
@ -45,7 +45,7 @@ class RemoteMegaphone: MegaphoneView {
|
||||
}
|
||||
|
||||
if let secondary = megaphoneModel.presentableSecondaryAction {
|
||||
let secondaryButton = MegaphoneView.Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
|
||||
let secondaryButton = Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
|
||||
guard
|
||||
let self,
|
||||
let fromViewController
|
||||
@ -58,9 +58,9 @@ class RemoteMegaphone: MegaphoneView {
|
||||
)
|
||||
}
|
||||
|
||||
setButtons(primary: primaryButton, secondary: secondaryButton)
|
||||
buttons = [primaryButton, secondaryButton]
|
||||
} else {
|
||||
setButtons(primary: primaryButton)
|
||||
buttons = [primaryButton]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,16 +80,13 @@ class RemoteMegaphone: MegaphoneView {
|
||||
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
|
||||
@ -134,7 +131,6 @@ class RemoteMegaphone: MegaphoneView {
|
||||
guard let self else { return }
|
||||
// Snooze regardless of outcome.
|
||||
self.markAsSnoozedWithSneakyTransaction()
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
|
||||
guard
|
||||
@ -153,7 +149,6 @@ class RemoteMegaphone: MegaphoneView {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ public class NotificationActionHandler {
|
||||
class func handleNotificationResponse(
|
||||
_ response: UNNotificationResponse,
|
||||
appReadiness: AppReadinessSetter,
|
||||
screenLockUI: ScreenLockUI,
|
||||
) async throws {
|
||||
owsAssertDebug(appReadiness.isAppReady)
|
||||
|
||||
@ -63,6 +64,7 @@ public class NotificationActionHandler {
|
||||
}
|
||||
switch responseAction {
|
||||
case .callBack:
|
||||
try await screenLockUI.waitForScreenUnlockThrowingPrevious()
|
||||
try await self.callBack(userInfo: userInfo)
|
||||
case .markAsRead:
|
||||
try await markAsRead(userInfo: userInfo)
|
||||
|
||||
@ -362,23 +362,11 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
self.tsAccountManager.setRegistrationId(aciRegistrationId, for: .aci, tx: tx)
|
||||
self.tsAccountManager.setRegistrationId(pniRegistrationId, for: .pni, 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.svr.storeKeys(
|
||||
fromProvisioningMessage: provisionMessage,
|
||||
authedDevice: .explicit(authedDevice),
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
self.receiptManager.setAreReadReceiptsEnabled(
|
||||
provisionMessage.areReadReceiptsEnabled,
|
||||
|
||||
@ -50,7 +50,7 @@ public class ProvisioningManager {
|
||||
var aciIdentityKeyPair: ECKeyPair
|
||||
var pniIdentityKeyPair: ECKeyPair
|
||||
var areReadReceiptsEnabled: Bool
|
||||
var rootKey: LinkingProvisioningMessage.RootKey
|
||||
var aep: SignalServiceKit.AccountEntropyPool
|
||||
var mediaRootBackupKey: MediaRootBackupKey
|
||||
var profileKey: Aes256Key
|
||||
}
|
||||
@ -64,13 +64,11 @@ 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.")
|
||||
@ -80,7 +78,7 @@ public class ProvisioningManager {
|
||||
aciIdentityKeyPair: aciIdentityKeyPair,
|
||||
pniIdentityKeyPair: pniIdentityKeyPair,
|
||||
areReadReceiptsEnabled: areReadReceiptsEnabled,
|
||||
rootKey: rootKey,
|
||||
aep: accountEntropyPool,
|
||||
mediaRootBackupKey: mrbk,
|
||||
profileKey: profileKey,
|
||||
)
|
||||
@ -105,7 +103,7 @@ public class ProvisioningManager {
|
||||
let provisioningCode = try await deviceProvisioningService.requestDeviceProvisioningCode()
|
||||
|
||||
let provisioningMessage = LinkingProvisioningMessage(
|
||||
rootKey: provisioningState.rootKey,
|
||||
aep: provisioningState.aep,
|
||||
aci: myAci,
|
||||
phoneNumber: myPhoneNumber,
|
||||
pni: myPni,
|
||||
|
||||
@ -52,10 +52,11 @@ class LinkAndSyncSecondaryProgressViewModel: ObservableObject {
|
||||
|
||||
guard !didTapCancel else { return }
|
||||
|
||||
self.isIndeterminate = progress
|
||||
.progress(for: .waitingForBackup)?
|
||||
.isFinished.negated
|
||||
?? true
|
||||
self.isIndeterminate = !(
|
||||
progress
|
||||
.progress(for: .waitingForBackup)?
|
||||
.isFinished ?? false
|
||||
)
|
||||
|
||||
if
|
||||
let downloadSource = progress.progressForChild(
|
||||
|
||||
@ -99,8 +99,6 @@ public class _RegistrationCoordinator_CNContactsStoreWrapper: _RegistrationCoord
|
||||
|
||||
public protocol _RegistrationCoordinator_ExperienceManagerShim {
|
||||
|
||||
func clearIntroducingPinsExperience(_ tx: DBWriteTransaction)
|
||||
|
||||
func enableAllGetStartedCards(_ tx: DBWriteTransaction)
|
||||
}
|
||||
|
||||
@ -108,10 +106,6 @@ 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)
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ public enum RegistrationBackupRestoreError {
|
||||
case incorrectRecoveryKey
|
||||
case recoveryKeyRegistrationFailed
|
||||
case versionMismatch
|
||||
case retryableSVRBError
|
||||
case unretryableSVRBError
|
||||
case networkError
|
||||
case rateLimited
|
||||
@ -81,14 +80,10 @@ 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
|
||||
@ -271,7 +266,7 @@ public class RegistrationCoordinatorBackupErrorPresenterImpl:
|
||||
)
|
||||
}
|
||||
})
|
||||
case .retryableSVRBError, .cancellation:
|
||||
case .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.",
|
||||
|
||||
@ -881,7 +881,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
var hasBackedUpToSVR = false
|
||||
var didSkipSVRBackup = false
|
||||
var shouldBackUpToSVR: Bool {
|
||||
return hasBackedUpToSVR.negated && didSkipSVRBackup.negated
|
||||
return !hasBackedUpToSVR && !didSkipSVRBackup
|
||||
}
|
||||
|
||||
var backupMetadataHeader: BackupNonce.MetadataHeader?
|
||||
@ -1269,7 +1269,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
// but these values aren't persisted to their final destination until the very end of
|
||||
// registration, so persiting the these values once at the start is the easiest way to
|
||||
// avoid problems.
|
||||
// Note: We should not reuse existing registration ids if we are reregistering
|
||||
// Note: We should generate new registration ids if we are reregistering
|
||||
updatePersistedState(tx) {
|
||||
if $0.aciRegistrationId == nil {
|
||||
$0.aciRegistrationId = RegistrationIdGenerator.generate()
|
||||
@ -1431,16 +1431,6 @@ 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.
|
||||
@ -2175,7 +2165,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
|
||||
private func loadSVRAuthCredentialCandidates(_ tx: DBReadTransaction) {
|
||||
let svr2AuthCredentialCandidates: [SVR2AuthCredential] = deps.svrAuthCredentialStore.getAuthCredentials(tx)
|
||||
if svr2AuthCredentialCandidates.isEmpty.negated {
|
||||
if !svr2AuthCredentialCandidates.isEmpty {
|
||||
inMemoryState.svr2AuthCredentialCandidates = svr2AuthCredentialCandidates
|
||||
}
|
||||
}
|
||||
@ -2730,7 +2720,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
guard
|
||||
(
|
||||
inMemoryState.accountEntropyPool != nil ||
|
||||
persistedState.hasGivenUpTryingToRestoreWithSVR.negated
|
||||
!persistedState.hasGivenUpTryingToRestoreWithSVR
|
||||
)
|
||||
else {
|
||||
// If we haven't set an AEP, and have already exhausted our SVR backup attempts, we are stuck.
|
||||
@ -3908,7 +3898,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
}
|
||||
|
||||
if let reglockToken = self.reglockToken(for: accountIdentity.e164) {
|
||||
if inMemoryState.hasSetReglock.negated {
|
||||
if !inMemoryState.hasSetReglock {
|
||||
return await self.enableReglock(accountIdentity: accountIdentity, reglockToken: reglockToken)
|
||||
}
|
||||
} else {
|
||||
@ -4380,7 +4370,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
|
||||
switch mode {
|
||||
case .reRegistering(let state):
|
||||
if persistedState.hasResetForReRegistration.negated {
|
||||
if !persistedState.hasResetForReRegistration {
|
||||
db.write { tx in
|
||||
let isPrimaryDevice = deps.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
|
||||
let discoverability = deps.phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx)
|
||||
|
||||
@ -261,9 +261,7 @@ 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
|
||||
}
|
||||
@ -276,11 +274,6 @@ 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
|
||||
@ -294,19 +287,6 @@ 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
|
||||
@ -345,12 +325,6 @@ 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>(
|
||||
|
||||
@ -75,7 +75,7 @@ class RegistrationLoadingViewController: OWSViewController, OWSNavigationChildCo
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if spinnerView.isAnimating.negated {
|
||||
if !spinnerView.isAnimating {
|
||||
spinnerView.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ public class RegistrationNavigationController: OWSNavigationController {
|
||||
return
|
||||
}
|
||||
|
||||
if let loadingMode, step.isSealed.negated {
|
||||
if let loadingMode, !step.isSealed {
|
||||
logger.info("Pushing loading controller")
|
||||
isLoading = true
|
||||
|
||||
@ -624,17 +624,6 @@ extension RegistrationNavigationController: RegistrationPinPresenter {
|
||||
func submitWithCreateNewPinInstead() {
|
||||
pushNextController(coordinator.skipAndCreateNewPINCode())
|
||||
}
|
||||
|
||||
func enterRecoveryKey() {
|
||||
pushNextController(
|
||||
.value(.enterRecoveryKey(
|
||||
RegistrationEnterAccountEntropyPoolState(
|
||||
canShowBackButton: true,
|
||||
canShowNoKeyHelpButton: false,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension RegistrationNavigationController: RegistrationPinAttemptsExhaustedAndMustCreateNewPinPresenter {
|
||||
|
||||
@ -89,8 +89,6 @@ protocol RegistrationPinPresenter: AnyObject {
|
||||
func submitWithCreateNewPinInstead()
|
||||
|
||||
func exitRegistration()
|
||||
|
||||
func enterRecoveryKey()
|
||||
}
|
||||
|
||||
// MARK: - RegistrationPinViewController
|
||||
@ -454,18 +452,6 @@ 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)
|
||||
}
|
||||
@ -682,15 +668,6 @@ 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(
|
||||
|
||||
@ -333,8 +333,8 @@ class RegistrationVerificationViewController: OWSViewController {
|
||||
}
|
||||
|
||||
explanationLabel.text = explanationLabelText()
|
||||
wrongNumberButton.isHidden = state.canChangeE164.negated
|
||||
helpButton.isHidden = state.showHelpText.negated
|
||||
wrongNumberButton.isHidden = !state.canChangeE164
|
||||
helpButton.isHidden = !state.showHelpText
|
||||
|
||||
verificationCodeView.updateColors()
|
||||
}
|
||||
|
||||
@ -1306,6 +1306,42 @@ limitations under the License.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2014 Alex Crichton
|
||||
Copyright (c) 2020 Ivan Nikulin <ifaaan@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>boring-sys 5.0.2</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>
|
||||
@ -1554,42 +1590,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2014 Alex Crichton
|
||||
Copyright (c) 2020 Ivan Nikulin <ifaaan@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>boring-sys 5.0.2</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>
|
||||
@ -1932,7 +1932,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>cesu8 1.1.0, jni-sys-macros 0.4.1, neon 1.1.1, objc2-core-foundation 0.3.2, objc2-io-kit 0.3.2, pbjson 0.9.0, pbjson-build 0.9.0, pbjson-types 0.9.0, protobuf-parse 3.7.2, tonic-prost-build 0.14.5, windows 0.61.3, windows 0.62.2, windows-collections 0.2.0, windows-collections 0.3.2, windows-core 0.61.2, windows-core 0.62.2, windows-future 0.2.1, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.1.3, windows-link 0.2.1, windows-numerics 0.2.0, windows-numerics 0.3.1, windows-result 0.3.4, windows-result 0.4.1, windows-strings 0.4.2, windows-strings 0.5.1, windows-threading 0.1.0, windows-threading 0.2.1</string>
|
||||
<string>cesu8 1.1.0, jni-sys-macros 0.4.1, neon 1.1.1, objc2-core-foundation 0.3.2, objc2-io-kit 0.3.2, protobuf-parse 3.7.2, tonic-prost-build 0.14.5, windows 0.61.3, windows 0.62.2, windows-collections 0.2.0, windows-collections 0.3.2, windows-core 0.61.2, windows-core 0.62.2, windows-future 0.2.1, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.1.3, windows-link 0.2.1, windows-numerics 0.2.0, windows-numerics 0.3.1, windows-result 0.3.4, windows-result 0.4.1, windows-strings 0.4.2, windows-strings 0.5.1, windows-threading 0.1.0, windows-threading 0.2.1</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
@ -2628,42 +2628,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
|
||||
<key>License</key>
|
||||
<string>BSD 3-Clause "New" or "Revised" License</string>
|
||||
<key>Title</key>
|
||||
<string>curve25519-dalek 4.1.3</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved.
|
||||
@ -2694,6 +2658,42 @@ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
|
||||
<key>License</key>
|
||||
<string>BSD 3-Clause "New" or "Revised" License</string>
|
||||
<key>Title</key>
|
||||
<string>curve25519-dalek 4.1.3</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
|
||||
<key>License</key>
|
||||
<string>BSD 3-Clause "New" or "Revised" License</string>
|
||||
@ -9835,6 +9835,41 @@ DEALINGS IN THE SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2020 InfluxData
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>pbjson 0.9.0, pbjson-build 0.9.0, pbjson-types 0.9.0</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2013-2025 The rust-url developers
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>8.14</string>
|
||||
<string>8.16</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
16
Signal/Symbols.xcassets/error/error-triangle.imageset/Contents.json
vendored
Normal file
16
Signal/Symbols.xcassets/error/error-triangle.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "error-triangle.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
107
Signal/Symbols.xcassets/error/error-triangle.imageset/error-triangle.pdf
vendored
Normal file
107
Signal/Symbols.xcassets/error/error-triangle.imageset/error-triangle.pdf
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.980103 15.426270 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
11.019876 2.073738 m
|
||||
10.324054 2.073738 9.775503 1.481371 9.828871 0.787598 c
|
||||
10.252174 -4.715347 l
|
||||
10.283031 -5.116498 10.617537 -5.426263 11.019876 -5.426263 c
|
||||
11.422214 -5.426263 11.756720 -5.116498 11.787577 -4.715347 c
|
||||
12.210880 0.787598 l
|
||||
12.264248 1.481371 11.715697 2.073738 11.019876 2.073738 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.980103 20.426270 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
9.769861 -12.926263 m
|
||||
9.769861 -12.235908 10.329505 -11.676263 11.019861 -11.676263 c
|
||||
11.710217 -11.676263 12.269861 -12.235908 12.269861 -12.926263 c
|
||||
12.269861 -13.616619 11.710217 -14.176262 11.019861 -14.176262 c
|
||||
10.329505 -14.176262 9.769861 -13.616619 9.769861 -12.926263 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.980103 2.568329 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
8.107189 18.687922 m
|
||||
9.410621 20.914618 12.629106 20.914612 13.932535 18.687920 c
|
||||
21.572298 5.636655 l
|
||||
22.889338 3.386715 21.266691 0.556679 18.659622 0.556679 c
|
||||
3.380099 0.556679 l
|
||||
0.773029 0.556679 -0.849612 3.386719 0.467425 5.636659 c
|
||||
8.107189 18.687922 l
|
||||
h
|
||||
12.422260 17.803856 m
|
||||
11.794682 18.875969 10.245042 18.875969 9.617465 17.803858 c
|
||||
1.977700 4.752595 l
|
||||
1.343571 3.669292 2.124843 2.306679 3.380099 2.306679 c
|
||||
18.659622 2.306679 l
|
||||
19.914881 2.306679 20.696152 3.669289 20.062023 4.752591 c
|
||||
12.422260 17.803856 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1436
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001526 00000 n
|
||||
0000001549 00000 n
|
||||
0000001722 00000 n
|
||||
0000001796 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1855
|
||||
%%EOF
|
||||
@ -390,33 +390,21 @@ class UsernameLinkPresentQRCodeViewController: OWSTableViewController2 {
|
||||
}
|
||||
|
||||
private func buildResetButtonView() -> UIView {
|
||||
let button = OWSRoundedButton { [weak self] in
|
||||
self?.tappedResetButton()
|
||||
}
|
||||
let button = UIButton(
|
||||
configuration: .smallSecondary(title: resetButtonString),
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
self?.didTapResetButton()
|
||||
},
|
||||
)
|
||||
|
||||
button.setTitle(resetButtonString, for: .normal)
|
||||
|
||||
button.ows_contentEdgeInsets = UIEdgeInsets(hMargin: 16, vMargin: 6)
|
||||
button.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .ows_whiteAlpha70
|
||||
button.titleLabel!.font = .dynamicTypeSubheadline.bold()
|
||||
button.setTitleColor(Theme.primaryTextColor, for: .normal)
|
||||
|
||||
button.configureForMultilineTitle()
|
||||
|
||||
button.dimsWhenHighlighted = true
|
||||
button.dimsWhenDisabled = true
|
||||
|
||||
switch usernameLinkState {
|
||||
case .resetting:
|
||||
if case .resetting = usernameLinkState {
|
||||
button.isEnabled = false
|
||||
case .available, .corrupted:
|
||||
button.isEnabled = true
|
||||
}
|
||||
|
||||
return CenteringStackView(centeredSubviews: [button])
|
||||
}
|
||||
|
||||
private func tappedResetButton() {
|
||||
private func didTapResetButton() {
|
||||
let actionSheet = ActionSheetController(message: OWSLocalizedString(
|
||||
"USERNAME_LINK_QR_CODE_VIEW_RESET_SHEET_MESSAGE",
|
||||
comment: "A message explaining what will happen if the user resets their QR code.",
|
||||
|
||||
@ -8,7 +8,7 @@ import SignalUI
|
||||
|
||||
class UsernameSelectionCoordinator {
|
||||
struct Context {
|
||||
let databaseStorage: SDSDatabaseStorage
|
||||
let databaseStorage: DB
|
||||
let networkManager: NetworkManager
|
||||
let storageServiceManager: StorageServiceManager
|
||||
let usernameEducationManager: UsernameEducationManager
|
||||
|
||||
@ -23,7 +23,7 @@ class UsernameSelectionViewController: OWSViewController, OWSNavigationChildCont
|
||||
/// A wrapper for injected dependencies.
|
||||
struct Context {
|
||||
let networkManager: NetworkManager
|
||||
let databaseStorage: SDSDatabaseStorage
|
||||
let databaseStorage: DB
|
||||
let localUsernameManager: LocalUsernameManager
|
||||
let storageServiceManager: StorageServiceManager
|
||||
}
|
||||
|
||||
@ -355,7 +355,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
|
||||
SSKEnvironment.shared.ows2FAManagerRef.setAreRemindersEnabled(false, transaction: transaction)
|
||||
}
|
||||
|
||||
ExperienceUpgradeManager.dismissPINReminderIfNecessary()
|
||||
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
|
||||
} else {
|
||||
self.updateTableContents()
|
||||
}
|
||||
@ -498,7 +498,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
|
||||
// MARK: -
|
||||
|
||||
private func showChangePin() {
|
||||
let vc = PinSetupViewController(mode: .changing) { [weak self] _, _ in
|
||||
let vc = PinSetupViewController(mode: .changing) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.navigationController?.popToViewController(self, animated: true)
|
||||
}
|
||||
@ -508,7 +508,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
|
||||
private func showCreatePin() {
|
||||
let vc = PinSetupViewController(
|
||||
mode: .creating,
|
||||
) { [weak self] _, _ in
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.navigationController?.popToViewController(self, animated: true)
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ class AdvancedPinSettingsTableViewController: OWSTableViewController2 {
|
||||
private func showCreatePin() {
|
||||
let viewController = PinSetupViewController(
|
||||
mode: .creating,
|
||||
completionHandler: { [weak self] _, _ in
|
||||
onSuccess: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.navigationController?.popToViewController(self, animated: true)
|
||||
self.updateTableContents()
|
||||
|
||||
@ -411,6 +411,7 @@ class AppSettingsViewController: OWSTableViewController2 {
|
||||
infoStack.autoPinTrailingToSuperviewMargin()
|
||||
|
||||
if let usernameLinkButton = profileCellUsernameLinkButton() {
|
||||
usernameLinkButton.sizeToFit() // this is required
|
||||
cell.accessoryView = usernameLinkButton
|
||||
} else {
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
@ -509,11 +510,7 @@ class AppSettingsViewController: OWSTableViewController2 {
|
||||
return profileInfoStack
|
||||
}
|
||||
|
||||
/// If we have a username, produces a button that takes the user to their
|
||||
/// username link QR code.
|
||||
///
|
||||
/// Note that this button does not use autolayout, so as to play nice with
|
||||
/// ``UITableViewCell``'s accessory view.
|
||||
/// If we have a username, produces a button that takes the user to their username link QR code.
|
||||
private func profileCellUsernameLinkButton() -> UIButton? {
|
||||
let localUsername: String
|
||||
let localUsernameLink: Usernames.UsernameLink
|
||||
@ -526,34 +523,26 @@ class AppSettingsViewController: OWSTableViewController2 {
|
||||
localUsernameLink = usernameLink
|
||||
}
|
||||
|
||||
let usernameLinkButton = OWSRoundedButton { [weak self] in
|
||||
guard let self else { return }
|
||||
var buttonConfiguration = UIButton.Configuration.roundGray(image: .qrCode)
|
||||
buttonConfiguration.contentInsets = .init(margin: 8) // makes 40 dp button
|
||||
return UIButton(
|
||||
configuration: buttonConfiguration,
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
let usernameLinkController = UsernameLinkQRCodeContentController(
|
||||
db: DependenciesBridge.shared.db,
|
||||
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
|
||||
username: localUsername,
|
||||
usernameLink: localUsernameLink,
|
||||
changeDelegate: self,
|
||||
scanDelegate: self,
|
||||
)
|
||||
let usernameLinkController = UsernameLinkQRCodeContentController(
|
||||
db: DependenciesBridge.shared.db,
|
||||
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
|
||||
username: localUsername,
|
||||
usernameLink: localUsernameLink,
|
||||
changeDelegate: self,
|
||||
scanDelegate: self,
|
||||
)
|
||||
|
||||
let navController = OWSNavigationController(rootViewController: usernameLinkController)
|
||||
self.present(navController, animated: true)
|
||||
}
|
||||
|
||||
if Theme.isDarkThemeEnabled {
|
||||
usernameLinkButton.backgroundColor = .ows_gray65
|
||||
usernameLinkButton.setTemplateImage(Theme.iconImage(.qrCode), tintColor: .ows_gray15)
|
||||
} else {
|
||||
usernameLinkButton.backgroundColor = .ows_gray05
|
||||
usernameLinkButton.setImage(Theme.iconImage(.qrCode), for: .normal)
|
||||
}
|
||||
|
||||
usernameLinkButton.bounds = CGRect(origin: .zero, size: .square(36))
|
||||
usernameLinkButton.imageView?.autoSetDimensions(to: .square(20))
|
||||
|
||||
return usernameLinkButton
|
||||
let navController = OWSNavigationController(rootViewController: usernameLinkController)
|
||||
self.present(navController, animated: true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private func didTapDonate() {
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
class PaypalButton: UIButton {
|
||||
private let actionBlock: () -> Void
|
||||
|
||||
init(actionBlock: @escaping () -> Void) {
|
||||
self.actionBlock = actionBlock
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addTarget(self, action: #selector(didTouchUpInside), for: .touchUpInside)
|
||||
|
||||
configureStyling()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not implemented.")
|
||||
}
|
||||
|
||||
// MARK: Styling
|
||||
|
||||
private func configureStyling() {
|
||||
setImage(UIImage(named: "paypal-logo"), for: .normal)
|
||||
ows_adjustsImageWhenDisabled = false
|
||||
ows_adjustsImageWhenHighlighted = false
|
||||
if #available(iOS 26.0, *) {
|
||||
configuration = .prominentGlass()
|
||||
tintColor = UIColor(rgbHex: 0xF6C757)
|
||||
} else {
|
||||
layer.cornerRadius = 12
|
||||
backgroundColor = UIColor(rgbHex: 0xF6C757)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
private func didTouchUpInside() {
|
||||
actionBlock()
|
||||
}
|
||||
}
|
||||
@ -64,16 +64,25 @@ class InternalBackupSettingsViewController: OWSTableViewController2 {
|
||||
let vc = InternalListMediaViewController()
|
||||
self?.navigationController?.pushViewController(vc, animated: true)
|
||||
})
|
||||
if RemoteConfig.current.isOptimizeStorageEnabled {
|
||||
section.add(.switch(
|
||||
withText: "Aggressive optimize media",
|
||||
subtitle: "Don't keep recent attachments when optimize media is enabled",
|
||||
isOn: { Attachment.offloadingThresholdOverride },
|
||||
actionBlock: { _ in
|
||||
Attachment.offloadingThresholdOverride = !Attachment.offloadingThresholdOverride
|
||||
},
|
||||
))
|
||||
}
|
||||
section.add(.switch(
|
||||
withText: "Regenerate backup thumbnails",
|
||||
subtitle: "Regenerate backup thumbnails on next offloading run",
|
||||
isOn: { db.read(block: backupSettingsStore.shouldGenerateThumbnailsOnNextOffloading(tx:)) },
|
||||
actionBlock: { _ in
|
||||
db.write { tx in
|
||||
let currentValue = backupSettingsStore.shouldGenerateThumbnailsOnNextOffloading(tx: tx)
|
||||
backupSettingsStore.setShouldGenerateThumbnailsOnNextOffloading(!currentValue, tx: tx)
|
||||
}
|
||||
},
|
||||
))
|
||||
section.add(.switch(
|
||||
withText: "Aggressive optimize media",
|
||||
subtitle: "Don't keep recent attachments when optimize media is enabled",
|
||||
isOn: { Attachment.offloadingThresholdOverride },
|
||||
actionBlock: { _ in
|
||||
Attachment.offloadingThresholdOverride = !Attachment.offloadingThresholdOverride
|
||||
},
|
||||
))
|
||||
|
||||
contents.add(section)
|
||||
|
||||
|
||||
@ -42,9 +42,6 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
|
||||
let voiceMessageCacheSize = folderSizeRecursive(of: VoiceMessageInterruptedDraftStore.draftVoiceMessageDirectory)
|
||||
let librarySize = folderSizeRecursive(ofPath: OWSFileSystem.appLibraryDirectoryPath()) ?? 0
|
||||
let libraryCachesSize = folderSizeRecursive(ofPath: OWSFileSystem.cachesDirectoryPath()) ?? 0
|
||||
let documentsSize = folderSizeRecursive(ofPath: OWSFileSystem.appDocumentDirectoryPath())
|
||||
let sharedDataSize = folderSizeRecursive(ofPath: OWSFileSystem.appSharedDataDirectoryPath())
|
||||
|
||||
let bundleSize = folderSizeRecursive(ofPath: Bundle.main.bundlePath)
|
||||
let tmpSize = folderSizeRecursive(ofPath: NSTemporaryDirectory())
|
||||
}
|
||||
@ -55,10 +52,8 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
|
||||
nonisolated static func build() async -> InternalDiskUsageViewController {
|
||||
await Task.yield()
|
||||
try! await DependenciesBridge.shared.orphanedAttachmentCleaner.runUntilFinished()
|
||||
let diskUsageTask = Task { DiskUsage() }
|
||||
let orphanedAttachmentByteCountTask = Task { await Self.orphanAttachmentByteCount() }
|
||||
let diskUsage = await diskUsageTask.value
|
||||
let orphanedAttachmentByteCount = await orphanedAttachmentByteCountTask.value
|
||||
async let orphanedAttachmentByteCount = Self.orphanAttachmentByteCount()
|
||||
let diskUsage = DiskUsage()
|
||||
return await InternalDiskUsageViewController(
|
||||
diskUsage: diskUsage,
|
||||
orphanedAttachmentByteCount: orphanedAttachmentByteCount,
|
||||
@ -87,29 +82,6 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
|
||||
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
|
||||
let knownFilesSize: UInt64 = [UInt64?](
|
||||
arrayLiteral:
|
||||
diskUsage.dbSize,
|
||||
diskUsage.dbWalSize,
|
||||
diskUsage.dbShmSize,
|
||||
diskUsage.attachmentSize,
|
||||
diskUsage.emojiCacheSize,
|
||||
diskUsage.stickerCacheSize,
|
||||
diskUsage.avatarCacheSize,
|
||||
diskUsage.voiceMessageCacheSize,
|
||||
diskUsage.librarySize,
|
||||
)
|
||||
.compacted()
|
||||
.reduce(0, +)
|
||||
var totalFilesystemSize: UInt64 = [UInt64?](
|
||||
arrayLiteral:
|
||||
diskUsage.librarySize,
|
||||
diskUsage.documentsSize,
|
||||
diskUsage.sharedDataSize,
|
||||
)
|
||||
.compacted()
|
||||
.reduce(0, +)
|
||||
|
||||
let diskUsageSection = OWSTableSection(title: "Disk Usage")
|
||||
diskUsageSection.add(.copyableItem(label: "DB Size", value: byteCountFormatter.string(for: diskUsage.dbSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "DB WAL Size", value: byteCountFormatter.string(for: diskUsage.dbWalSize)))
|
||||
@ -120,36 +92,12 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
|
||||
diskUsageSection.add(.copyableItem(label: "Sticker cache size", value: byteCountFormatter.string(for: diskUsage.stickerCacheSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Avatar cache size", value: byteCountFormatter.string(for: diskUsage.avatarCacheSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Voice message drafts size", value: byteCountFormatter.string(for: diskUsage.voiceMessageCacheSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Library (minus caches) folder size", value: byteCountFormatter.string(for: diskUsage.librarySize - diskUsage.libraryCachesSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Library folder size (includes Caches)", value: byteCountFormatter.string(for: diskUsage.librarySize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Caches folder size", value: byteCountFormatter.string(for: diskUsage.libraryCachesSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Ancillary files size", value: byteCountFormatter.string(for: totalFilesystemSize - knownFilesSize)))
|
||||
|
||||
if TSConstants.isUsingProductionService {
|
||||
let stagingSharedDataSize = folderSizeRecursive(
|
||||
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsStaging().applicationGroup)!.path,
|
||||
)
|
||||
diskUsageSection.add(.copyableItem(label: "Staging app group size", value: byteCountFormatter.string(for: stagingSharedDataSize)))
|
||||
totalFilesystemSize += stagingSharedDataSize ?? 0
|
||||
} else {
|
||||
let prodSharedDataSize = folderSizeRecursive(
|
||||
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsProduction().applicationGroup)!.path,
|
||||
)
|
||||
diskUsageSection.add(.copyableItem(label: "Prod app group size", value: byteCountFormatter.string(for: prodSharedDataSize)))
|
||||
totalFilesystemSize += prodSharedDataSize ?? 0
|
||||
}
|
||||
diskUsageSection.add(.copyableItem(
|
||||
label: "Total filesystem size",
|
||||
subtitle: "Should match \"Documents & Data\" in Settings>General>Storage>Signal",
|
||||
value: byteCountFormatter.string(for: totalFilesystemSize),
|
||||
))
|
||||
diskUsageSection.add(.copyableItem(label: "Tmp size", value: byteCountFormatter.string(for: diskUsage.tmpSize)))
|
||||
diskUsageSection.add(.copyableItem(label: "Bundle size", value: byteCountFormatter.string(for: diskUsage.bundleSize)))
|
||||
contents.add(diskUsageSection)
|
||||
|
||||
let otherDiskUsageSection = OWSTableSection(title: "Other Disk Usage")
|
||||
otherDiskUsageSection.add(.copyableItem(label: "Tmp size", value: byteCountFormatter.string(for: diskUsage.tmpSize)))
|
||||
otherDiskUsageSection.add(.copyableItem(label: "Bundle size", value: byteCountFormatter.string(for: diskUsage.bundleSize)))
|
||||
|
||||
contents.add(otherDiskUsageSection)
|
||||
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
|
||||
@ -353,11 +353,9 @@ extension LinkedDevicesViewModel: LinkDeviceViewControllerDelegate {
|
||||
if let details, Date() > details.shouldRemindUserAfter {
|
||||
db.write { tx in
|
||||
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
ExperienceUpgradeManager.clearExperienceUpgrade(
|
||||
.newLinkedDeviceNotification,
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
|
||||
}
|
||||
|
||||
SSKEnvironment.shared.notificationPresenterRef.clearDeliveredNewLinkedDevicesNotifications()
|
||||
|
||||
@ -25,6 +25,9 @@ class PaymentsViewPassphraseGridViewController: OWSViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let screenLockUI = AppEnvironment.shared.screenLockUI
|
||||
screenLockUI.sensitiveContentDidLoad(inViewController: self)
|
||||
|
||||
title = OWSLocalizedString(
|
||||
"SETTINGS_PAYMENTS_VIEW_PASSPHRASE_TITLE",
|
||||
comment: "Title for the 'view payments passphrase' view of the app settings.",
|
||||
|
||||
@ -95,10 +95,7 @@ class BlockListViewController: OWSTableViewController2 {
|
||||
OWSTableItem(
|
||||
dequeueCellBlock: { [weak self] tableView in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier) as! ContactTableViewCell
|
||||
let config = ContactCellConfiguration(
|
||||
address: address,
|
||||
localUserDisplayMode: .asUser,
|
||||
)
|
||||
let config = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser)
|
||||
if self != nil {
|
||||
SSKEnvironment.shared.databaseStorageRef.read { transaction in
|
||||
cell.configure(configuration: config, transaction: transaction)
|
||||
|
||||
@ -204,8 +204,8 @@ class AttachmentFormatPickerView: UIView {
|
||||
private static func cases(except: [AttachmentType]) -> [AttachmentType] {
|
||||
let showGifSearch = RemoteConfig.current.enableGifSearch
|
||||
return allCases.filter { (value: AttachmentType) in
|
||||
if value == .gif, showGifSearch.negated { return false }
|
||||
return except.contains(value).negated
|
||||
if value == .gif, !showGifSearch { return false }
|
||||
return !except.contains(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ public class ContextMenuReactionBarAccessory: ContextMenuTargetedPreviewAccessor
|
||||
if let focusedEmoji = reactionPicker.focusedEmoji {
|
||||
switch focusedEmoji {
|
||||
case .more:
|
||||
didSelectAnyEmoji()
|
||||
didSelectShowFullEmojiPicker()
|
||||
case .emoji(let emoji):
|
||||
let isRemoving = emoji == self.itemViewModel?.reactionState?.localUserEmoji
|
||||
if let index = reactionPicker.currentEmojiSet().firstIndex(of: emoji) {
|
||||
@ -125,7 +125,7 @@ public class ContextMenuReactionBarAccessory: ContextMenuTargetedPreviewAccessor
|
||||
}
|
||||
}
|
||||
|
||||
func didSelectAnyEmoji() {
|
||||
func didSelectShowFullEmojiPicker() {
|
||||
guard let message = itemViewModel?.interaction as? TSMessage else {
|
||||
owsFailDebug("Not sending reaction for unexpected interaction type")
|
||||
return
|
||||
|
||||
@ -45,6 +45,62 @@ class DebugUIMisc: DebugUIPage {
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
if let groupThread = thread as? TSGroupThread {
|
||||
items += [
|
||||
OWSTableItem(title: "Insert 50 group update messages", actionBlock: {
|
||||
let updateItems: [TSInfoMessage.PersistableGroupUpdateItem] = [
|
||||
.genericUpdateByLocalUser,
|
||||
.genericUpdateByUnknownUser,
|
||||
.nameRemovedByLocalUser,
|
||||
.nameRemovedByUnknownUser,
|
||||
.avatarChangedByLocalUser,
|
||||
.avatarChangedByUnknownUser,
|
||||
.avatarRemovedByLocalUser,
|
||||
.avatarRemovedByUnknownUser,
|
||||
.localUserLeft,
|
||||
.localUserRemovedByUnknownUser,
|
||||
.localUserWasInvitedByLocalUser,
|
||||
.localUserWasInvitedByUnknownUser,
|
||||
.localUserAcceptedInviteFromUnknownUser,
|
||||
.localUserJoined,
|
||||
.localUserAddedByLocalUser,
|
||||
.localUserAddedByUnknownUser,
|
||||
.localUserDeclinedInviteFromUnknownUser,
|
||||
.localUserInviteRevokedByUnknownUser,
|
||||
.localUserRequestedToJoin,
|
||||
.localUserRequestApprovedByUnknownUser,
|
||||
.localUserRequestCanceledByLocalUser,
|
||||
.localUserRequestRejectedByUnknownUser,
|
||||
.inviteLinkResetByLocalUser,
|
||||
.inviteLinkResetByUnknownUser,
|
||||
.inviteLinkEnabledWithoutApprovalByLocalUser,
|
||||
.inviteLinkEnabledWithApprovalByLocalUser,
|
||||
.inviteLinkDisabledByLocalUser,
|
||||
.inviteLinkApprovalDisabledByLocalUser,
|
||||
.inviteLinkApprovalEnabledByLocalUser,
|
||||
.localUserJoinedViaInviteLink,
|
||||
.wasMigrated,
|
||||
.localUserInvitedAfterMigration,
|
||||
.createdByLocalUser,
|
||||
.createdByUnknownUser,
|
||||
.inviteFriendsToNewlyCreatedGroup,
|
||||
].shuffled()
|
||||
SSKEnvironment.shared.databaseStorageRef.write { tx in
|
||||
for i in 0..<50 {
|
||||
let item = updateItems[i % updateItems.count]
|
||||
let infoMessage = TSInfoMessage(
|
||||
thread: groupThread,
|
||||
messageType: .typeGroupUpdate,
|
||||
infoMessageUserInfo: [.groupUpdateItems: TSInfoMessage.PersistableGroupUpdateItemsWrapper([item])],
|
||||
)
|
||||
infoMessage.anyInsert(transaction: tx)
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
return OWSTableSection(title: name, items: items)
|
||||
}
|
||||
|
||||
|
||||
@ -211,11 +211,24 @@ class DonateChoosePaymentMethodSheet: StackSheetViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func createPaypalButton() -> PaypalButton {
|
||||
PaypalButton { [weak self] in
|
||||
guard let self else { return }
|
||||
self.didChoosePaymentMethod(self, .paypal)
|
||||
private func createPaypalButton() -> UIButton {
|
||||
var configuration: UIButton.Configuration = if #available(iOS 26, *) { .prominentGlass() } else { .borderedProminent() }
|
||||
configuration.image = UIImage(resource: .paypalLogo)
|
||||
configuration.baseBackgroundColor = UIColor(rgbHex: 0xF6C757)
|
||||
if #available(iOS 26, *) {
|
||||
configuration.cornerStyle = .capsule
|
||||
} else {
|
||||
configuration.cornerStyle = .fixed
|
||||
configuration.background.cornerRadius = 12
|
||||
}
|
||||
|
||||
return UIButton(
|
||||
configuration: configuration,
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.didChoosePaymentMethod(self, .paypal)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private func createCreditOrDebitCardButton() -> UIButton {
|
||||
|
||||
@ -79,16 +79,16 @@ class CLVViewState {
|
||||
}
|
||||
}
|
||||
|
||||
enum BackupSubscriptionFailedToRedeemAlertType: CaseIterable {
|
||||
enum BackupSubscriptionAlreadyRedeemedAlertType: CaseIterable {
|
||||
case avatarBadge
|
||||
case menuItem
|
||||
}
|
||||
|
||||
var backupSubscriptionFailedToRedeemAlerts: Set<BackupSubscriptionFailedToRedeemAlertType> = [] {
|
||||
var backupSubscriptionAlreadyRedeemedAlerts: Set<BackupSubscriptionAlreadyRedeemedAlertType> = [] {
|
||||
didSet {
|
||||
settingsButtonCreator.updateState(
|
||||
showBackupsSubscriptionAlreadyRedeemedAvatarBadge: backupSubscriptionFailedToRedeemAlerts.contains(.avatarBadge),
|
||||
showBackupsSubscriptionAlreadyRedeemedMenuItem: backupSubscriptionFailedToRedeemAlerts.contains(.menuItem),
|
||||
showBackupsSubscriptionAlreadyRedeemedAvatarBadge: backupSubscriptionAlreadyRedeemedAlerts.contains(.avatarBadge),
|
||||
showBackupsSubscriptionAlreadyRedeemedMenuItem: backupSubscriptionAlreadyRedeemedAlerts.contains(.menuItem),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,10 @@ class ChatListFYISheetCoordinator {
|
||||
let probablyHasCurrentSubscription: Bool
|
||||
}
|
||||
|
||||
struct BackupSubscriptionExpiringSoonWithPendingDownloads {
|
||||
let warning: BackupSubscriptionIssueStore.IAPSubscriptionExpiringSoonWarning
|
||||
}
|
||||
|
||||
struct BackupSubscriptionExpired {
|
||||
enum SubscriptionType {
|
||||
case iap
|
||||
@ -49,6 +53,7 @@ class ChatListFYISheetCoordinator {
|
||||
case badgeThanks(BadgeThanks)
|
||||
case badgeIssue(BadgeIssue)
|
||||
case badgeExpiration(BadgeExpiration)
|
||||
case backupSubscriptionExpiringSoonWithPendingDownloads(BackupSubscriptionExpiringSoonWithPendingDownloads)
|
||||
case backupSubscriptionExpired(BackupSubscriptionExpired)
|
||||
case backupSubscriptionFailedToRenew(BackupSubscriptionFailedToRenew)
|
||||
case keyTransparencySelfCheckFailed(KeyTransparencySelfCheckFailed)
|
||||
@ -57,11 +62,13 @@ class ChatListFYISheetCoordinator {
|
||||
}
|
||||
|
||||
private let backupArchiveErrorStore: BackupArchiveErrorStore
|
||||
private let backupAttachmentDownloadStore: BackupAttachmentDownloadStore
|
||||
private let backupExportJobRunner: BackupExportJobRunner
|
||||
private let backupSubscriptionIssueStore: BackupSubscriptionIssueStore
|
||||
private let dateProvider: DateProvider
|
||||
private let db: DB
|
||||
private let donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore
|
||||
private let donationSubscriptionManager: DonationSubscriptionManager
|
||||
private let db: DB
|
||||
private let keyTransparencyStore: KeyTransparencyStore
|
||||
private let networkManager: NetworkManager
|
||||
private let profileManager: ProfileManager
|
||||
@ -69,21 +76,25 @@ class ChatListFYISheetCoordinator {
|
||||
|
||||
init(
|
||||
backupArchiveErrorStore: BackupArchiveErrorStore,
|
||||
backupAttachmentDownloadStore: BackupAttachmentDownloadStore,
|
||||
backupExportJobRunner: BackupExportJobRunner,
|
||||
backupSubscriptionIssueStore: BackupSubscriptionIssueStore,
|
||||
dateProvider: @escaping DateProvider,
|
||||
db: DB,
|
||||
donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore,
|
||||
donationSubscriptionManager: DonationSubscriptionManager,
|
||||
db: DB,
|
||||
keyTransparencyStore: KeyTransparencyStore,
|
||||
networkManager: NetworkManager,
|
||||
profileManager: ProfileManager,
|
||||
) {
|
||||
self.backupArchiveErrorStore = backupArchiveErrorStore
|
||||
self.backupAttachmentDownloadStore = backupAttachmentDownloadStore
|
||||
self.backupExportJobRunner = backupExportJobRunner
|
||||
self.backupSubscriptionIssueStore = backupSubscriptionIssueStore
|
||||
self.dateProvider = dateProvider
|
||||
self.db = db
|
||||
self.donationReceiptCredentialResultStore = donationReceiptCredentialResultStore
|
||||
self.donationSubscriptionManager = donationSubscriptionManager
|
||||
self.db = db
|
||||
self.keyTransparencyStore = keyTransparencyStore
|
||||
self.networkManager = networkManager
|
||||
self.profileManager = profileManager
|
||||
@ -106,6 +117,8 @@ class ChatListFYISheetCoordinator {
|
||||
// MARK: -
|
||||
|
||||
private func nextSheetToPresent(tx: DBReadTransaction) -> FYISheet? {
|
||||
let now = dateProvider()
|
||||
|
||||
if let sheet = shouldShowSMSVerificationCodeSentSheet(tx: tx) {
|
||||
return sheet
|
||||
} else if let sheet = shouldShowBadgeThanksSheet(successMode: .oneTimeBoost, tx: tx) {
|
||||
@ -128,6 +141,15 @@ class ChatListFYISheetCoordinator {
|
||||
mostRecentSubscriptionPaymentMethod: donationSubscriptionManager.getMostRecentSubscriptionPaymentMethod(tx: tx),
|
||||
probablyHasCurrentSubscription: donationSubscriptionManager.probablyHasCurrentSubscription(tx: tx),
|
||||
))
|
||||
} else if
|
||||
let warning = backupSubscriptionIssueStore.shouldWarnIAPSubscriptionExpiringSoon(tx: tx),
|
||||
warning.date < now,
|
||||
// Only show the warning if there are downloads we still need to do.
|
||||
backupAttachmentDownloadStore.hasAnyIncompleteDownloads(isThumbnail: false, tx: tx)
|
||||
{
|
||||
return .backupSubscriptionExpiringSoonWithPendingDownloads(FYISheet.BackupSubscriptionExpiringSoonWithPendingDownloads(
|
||||
warning: warning,
|
||||
))
|
||||
} else if backupSubscriptionIssueStore.shouldWarnIAPSubscriptionExpired(tx: tx) {
|
||||
return .backupSubscriptionExpired(FYISheet.BackupSubscriptionExpired(subscriptionType: .iap))
|
||||
} else if backupSubscriptionIssueStore.shouldWarnTestFlightSubscriptionExpired(tx: tx) {
|
||||
@ -260,6 +282,8 @@ class ChatListFYISheetCoordinator {
|
||||
await _present(badgeIssue: badgeIssue, from: chatListViewController)
|
||||
case .badgeExpiration(let badgeExpiration):
|
||||
await _present(badgeExpiration: badgeExpiration, from: chatListViewController)
|
||||
case .backupSubscriptionExpiringSoonWithPendingDownloads(let backupSubscriptionExpiringSoonWithPendingDownloads):
|
||||
await _present(backupSubscriptionExpiringSoonWithPendingDownloads: backupSubscriptionExpiringSoonWithPendingDownloads, from: chatListViewController)
|
||||
case .backupSubscriptionExpired(let backupSubscriptionExpired):
|
||||
await _present(backupSubscriptionExpired: backupSubscriptionExpired, from: chatListViewController)
|
||||
case .backupSubscriptionFailedToRenew(let backupSubscriptionFailedToRenew):
|
||||
@ -432,6 +456,31 @@ class ChatListFYISheetCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private func _present(
|
||||
backupSubscriptionExpiringSoonWithPendingDownloads: FYISheet.BackupSubscriptionExpiringSoonWithPendingDownloads,
|
||||
from chatListViewController: ChatListViewController,
|
||||
) async {
|
||||
let logger = PrefixedLogger(prefix: "[Backups]")
|
||||
logger.info("Showing BackupSubscriptionExpiringSoonWithPendingDownloads FYI sheet.")
|
||||
|
||||
let warning = backupSubscriptionExpiringSoonWithPendingDownloads.warning
|
||||
|
||||
let sheet = BackupSubscriptionExpiringSoonWithPendingDownloadsHeroSheet(
|
||||
iapSubscriptionExpiringSoonWarning: warning,
|
||||
onDownloadBackupNow: {
|
||||
chatListViewController.showAppSettings(mode: .backups(onAppearAction: .disableOptimizeLocalStorage))
|
||||
},
|
||||
)
|
||||
chatListViewController.present(sheet, animated: true) { [self] in
|
||||
db.write { tx in
|
||||
backupSubscriptionIssueStore.setDidWarnIAPSubscriptionExpiringSoon(
|
||||
warning: warning,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _present(
|
||||
backupSubscriptionExpired: FYISheet.BackupSubscriptionExpired,
|
||||
from chatListViewController: ChatListViewController,
|
||||
@ -545,6 +594,63 @@ extension ChatListViewController: BadgeIssueSheetDelegate {
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class BackupSubscriptionExpiringSoonWithPendingDownloadsHeroSheet: HeroSheetViewController {
|
||||
override var canBeDismissed: Bool { false }
|
||||
|
||||
init(
|
||||
iapSubscriptionExpiringSoonWarning: BackupSubscriptionIssueStore.IAPSubscriptionExpiringSoonWarning,
|
||||
onDownloadBackupNow: @escaping () -> Void,
|
||||
) {
|
||||
let title = switch iapSubscriptionExpiringSoonWarning {
|
||||
case .firstWarning:
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_FIRST_WARNING_TITLE",
|
||||
comment: "Title for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
|
||||
)
|
||||
case .secondWarning:
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_SECOND_WARNING_TITLE",
|
||||
comment: "Title for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
|
||||
)
|
||||
}
|
||||
|
||||
super.init(
|
||||
hero: .circleIcon(
|
||||
icon: .backupErrorBold,
|
||||
iconSize: 40,
|
||||
tintColor: .Signal.red,
|
||||
backgroundColor: UIColor(rgbHex: 0xFFDDDB),
|
||||
),
|
||||
title: title,
|
||||
body: OWSLocalizedString(
|
||||
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_BODY",
|
||||
comment: "Body for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
|
||||
),
|
||||
primaryButton: HeroSheetViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_PRIMARY_BUTTON",
|
||||
comment: "Primary button for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
|
||||
),
|
||||
action: { sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
onDownloadBackupNow()
|
||||
}
|
||||
},
|
||||
),
|
||||
secondaryButton: HeroSheetViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_SECONDARY_BUTTON",
|
||||
comment: "Secondary button for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
|
||||
),
|
||||
style: .secondaryDestructive,
|
||||
action: .dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class BackupSubscriptionExpiredHeroSheet: HeroSheetViewController {
|
||||
init(
|
||||
subscriptionType: ChatListFYISheetCoordinator.FYISheet.BackupSubscriptionExpired.SubscriptionType,
|
||||
|
||||
@ -111,8 +111,8 @@ extension ChatListViewController {
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(reloadExperienceUpgrades),
|
||||
name: .inactivePrimaryDeviceChanged,
|
||||
selector: #selector(reconcileExperienceUpgrades),
|
||||
name: .megaphoneStateDidChange,
|
||||
object: nil,
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
@ -262,11 +262,8 @@ extension ChatListViewController {
|
||||
private func applicationDidBecomeActive(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
reconcileExperienceUpgrades()
|
||||
updateShouldBeUpdatingView()
|
||||
|
||||
if !ExperienceUpgradeManager.presentNext(fromViewController: self) {
|
||||
presentGetStartedBannerIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -344,13 +341,6 @@ extension ChatListViewController {
|
||||
updateUsernameReminderView()
|
||||
loadCoordinator.loadIfNecessary()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func reloadExperienceUpgrades() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
_ = ExperienceUpgradeManager.presentNext(fromViewController: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@ -273,17 +273,17 @@ extension ChatListViewController {
|
||||
}
|
||||
|
||||
public func updateBackupSubscriptionFailedToRedeemAlertsWithSneakyTx() {
|
||||
typealias BackupSubscriptionFailedToRedeemAlertType = CLVViewState.BackupSubscriptionFailedToRedeemAlertType
|
||||
typealias BackupSubscriptionAlreadyRedeemedAlertType = CLVViewState.BackupSubscriptionAlreadyRedeemedAlertType
|
||||
|
||||
let db = DependenciesBridge.shared.db
|
||||
let backupSubscriptionIssueStore = BackupSubscriptionIssueStore()
|
||||
|
||||
viewState.backupSubscriptionFailedToRedeemAlerts = db.read { tx in
|
||||
var alerts = Set<BackupSubscriptionFailedToRedeemAlertType>()
|
||||
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListBadge(tx: tx) {
|
||||
viewState.backupSubscriptionAlreadyRedeemedAlerts = db.read { tx in
|
||||
var alerts = Set<BackupSubscriptionAlreadyRedeemedAlertType>()
|
||||
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionAlreadyRedeemedChatListBadge(tx: tx) {
|
||||
alerts.insert(.avatarBadge)
|
||||
}
|
||||
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListMenuItem(tx: tx) {
|
||||
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionAlreadyRedeemedChatListMenuItem(tx: tx) {
|
||||
alerts.insert(.menuItem)
|
||||
}
|
||||
return alerts
|
||||
|
||||
@ -233,16 +233,16 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
defer {
|
||||
hasEverAppeared = true
|
||||
}
|
||||
|
||||
appReadiness.setUIIsReady()
|
||||
|
||||
if getStartedBanner == nil, !hasEverPresentedExperienceUpgrade, ExperienceUpgradeManager.presentNext(fromViewController: self) {
|
||||
hasEverPresentedExperienceUpgrade = true
|
||||
} else if !hasEverAppeared {
|
||||
presentGetStartedBannerIfNecessary()
|
||||
}
|
||||
|
||||
presentGetStartedBannerIfNecessary()
|
||||
reconcileExperienceUpgrades()
|
||||
requestReviewIfAppropriate()
|
||||
showFYISheetIfNecessary()
|
||||
|
||||
viewState.searchResultsController.viewDidAppear(animated)
|
||||
|
||||
@ -253,10 +253,8 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
|
||||
}
|
||||
}
|
||||
|
||||
showFYISheetIfNecessary()
|
||||
Task { try await self.checkForFailedServiceExtensionLaunches() }
|
||||
|
||||
hasEverAppeared = true
|
||||
if viewState.multiSelectState.isActive {
|
||||
showToolbar()
|
||||
} else {
|
||||
@ -361,17 +359,26 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Experience Upgrades
|
||||
|
||||
@objc
|
||||
func reconcileExperienceUpgrades() {
|
||||
ExperienceUpgradeManager.reconcilePresentedExperienceUpgrade(fromViewController: self)
|
||||
}
|
||||
|
||||
// MARK: - FYI sheets
|
||||
|
||||
@objc
|
||||
func showFYISheetIfNecessary() {
|
||||
let fyiSheetCoordinator = ChatListFYISheetCoordinator(
|
||||
backupArchiveErrorStore: BackupArchiveErrorStore(),
|
||||
backupAttachmentDownloadStore: BackupAttachmentDownloadStore(),
|
||||
backupExportJobRunner: DependenciesBridge.shared.backupExportJobRunner,
|
||||
backupSubscriptionIssueStore: BackupSubscriptionIssueStore(),
|
||||
dateProvider: { Date() },
|
||||
db: DependenciesBridge.shared.db,
|
||||
donationReceiptCredentialResultStore: DependenciesBridge.shared.donationReceiptCredentialResultStore,
|
||||
donationSubscriptionManager: DependenciesBridge.shared.donationSubscriptionManager,
|
||||
db: DependenciesBridge.shared.db,
|
||||
keyTransparencyStore: KeyTransparencyStore(),
|
||||
networkManager: SSKEnvironment.shared.networkManagerRef,
|
||||
profileManager: SSKEnvironment.shared.profileManagerRef,
|
||||
@ -382,7 +389,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UI Components
|
||||
// MARK: UI Components -
|
||||
|
||||
private lazy var emptyChatListView: UIView = {
|
||||
let titleLabel = UILabel.explanationTextLabel(text: NSLocalizedString(
|
||||
@ -905,6 +912,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
|
||||
}
|
||||
|
||||
// In Production this will pop up at most 3 times per 365 days.
|
||||
Logger.info("requesting review")
|
||||
SKStoreReviewController.requestReview(in: windowScene)
|
||||
Self.didRequestReview = true
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ enum SafetyTipsSheet {
|
||||
),
|
||||
handler: { [weak fromViewController] _ in
|
||||
let safetyTipsVC = SafetyTipsViewController(
|
||||
mode: .smsRequest,
|
||||
primaryButton: SafetyTipsViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"SETTINGS_ACCOUNT_BUTTON",
|
||||
|
||||
@ -167,7 +167,7 @@ class StoryPageViewController: UIPageViewController {
|
||||
// and an ongoing paging drag transition but the scrollview isn't dragging) and resolve it
|
||||
// by closing the transition out ourselves.
|
||||
if
|
||||
pendingTransitionViewControllers.isEmpty.negated,
|
||||
!pendingTransitionViewControllers.isEmpty,
|
||||
isTransitioningByScroll,
|
||||
!isUserDraggingScrollView
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user