Compare commits
194 Commits
8.13.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 | ||
|
|
cdb96e1029 | ||
|
|
961936b0ca | ||
|
|
46445edfe7 | ||
|
|
ab454da687 | ||
|
|
025a9ff9be | ||
|
|
c91c15ec7f | ||
|
|
975834e1f6 | ||
|
|
77bc1008ad | ||
|
|
ed9f3615ba | ||
|
|
aad90b9f5b | ||
|
|
dc3827ed5f | ||
|
|
f5806db594 | ||
|
|
18871a45bd | ||
|
|
ec69b9425f | ||
|
|
732d375c82 | ||
|
|
ce442092f3 | ||
|
|
2faeff7589 | ||
|
|
8384fab6a1 | ||
|
|
3a3ffde3dd | ||
|
|
6e45f851f2 | ||
|
|
a6476a9e79 | ||
|
|
276d778e22 | ||
|
|
92b54a1ceb | ||
|
|
ea190d9ae0 | ||
|
|
507d23b760 | ||
|
|
e39fb58e06 | ||
|
|
d655b7b7a4 | ||
|
|
29b863abf8 | ||
|
|
7beb7330a0 | ||
|
|
e14f223e79 | ||
|
|
334f6b9888 | ||
|
|
5f7cbb5f66 | ||
|
|
0ca83a1b50 | ||
|
|
665fda1f2a | ||
|
|
ab097068a8 | ||
|
|
ebd1292f61 | ||
|
|
118e6289ab | ||
|
|
73e5108c1e | ||
|
|
fa65a9e9b5 | ||
|
|
1ea6b1b1d9 | ||
|
|
b8a90daaf0 | ||
|
|
e7a0d760ca | ||
|
|
e541922e02 | ||
|
|
7166c115af | ||
|
|
5897252015 | ||
|
|
7771cf159d | ||
|
|
7f55a610d8 | ||
|
|
c859d83b1d | ||
|
|
c23f22445a | ||
|
|
3cbd9ece13 | ||
|
|
140a572d85 | ||
|
|
a6387b9bfd | ||
|
|
d3b8a06e00 | ||
|
|
48fb1065c6 | ||
|
|
87eb0382b3 | ||
|
|
30c949e930 | ||
|
|
60554470e6 | ||
|
|
4ab7d1d12d | ||
|
|
a7eb78f46c | ||
|
|
153efb2d45 | ||
|
|
ba2b662d37 | ||
|
|
543085bd26 | ||
|
|
a65ec79c04 | ||
|
|
2ea672abb3 | ||
|
|
e7d209ee66 | ||
|
|
df2e8557e9 | ||
|
|
46b3f825a2 | ||
|
|
7b7727287c | ||
|
|
cf47211efe | ||
|
|
3cb8044133 | ||
|
|
8ddef58df8 | ||
|
|
7f240db8a4 | ||
|
|
d5747a432a | ||
|
|
c1c292b23a | ||
|
|
ce9b44ec82 | ||
|
|
608bea72f6 | ||
|
|
1aadffb78a | ||
|
|
25f73ea745 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -18,7 +18,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# Add additional Xcode versions here if necessary.
|
||||
xcode: ["Xcode_26.4"]
|
||||
xcode: ["Xcode_26.5"]
|
||||
|
||||
steps:
|
||||
- name: Set Xcode version
|
||||
|
||||
2
.github/workflows/precommit.yml
vendored
2
.github/workflows/precommit.yml
vendored
@ -34,7 +34,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
# v0.60.1
|
||||
swiftformat-ref: c8e50ff2cfc2eab46246c072a9ae25ab656c6ec3
|
||||
|
||||
|
||||
2
.github/workflows/protobuf-check.yml
vendored
2
.github/workflows/protobuf-check.yml
vendored
@ -28,7 +28,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
# v1.36.1
|
||||
swift-protobuf-ref: a008af1a102ff3dd6cc3764bb69bf63226d0f5f6
|
||||
|
||||
|
||||
2
.github/workflows/translation-check.yml
vendored
2
.github/workflows/translation-check.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
|
||||
jobs:
|
||||
check-strings:
|
||||
|
||||
2
.github/workflows/translation-tool.yml
vendored
2
.github/workflows/translation-tool.yml
vendored
@ -17,7 +17,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
2
.github/workflows/translation-validator.yml
vendored
2
.github/workflows/translation-validator.yml
vendored
@ -17,7 +17,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.4.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.5.app
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -36,6 +36,9 @@ Index/
|
||||
*.sdsjson
|
||||
Scripts/sds_codegen/sds-includes/*
|
||||
|
||||
# Logs
|
||||
debuglogs/
|
||||
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
Xcode 26.4.1
|
||||
Xcode 26.5
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -7,6 +7,16 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
|
||||
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
|
||||
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */; };
|
||||
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
|
||||
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */; };
|
||||
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */; };
|
||||
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
|
||||
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */; };
|
||||
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
|
||||
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
|
||||
04127D912F23B3B000B4E95B /* CVCapsuleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */; };
|
||||
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */; };
|
||||
041C24ED2DF782AF0065B685 /* OutgoingGroupUpdateMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */; };
|
||||
@ -67,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 */; };
|
||||
@ -83,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 */; };
|
||||
@ -525,7 +533,6 @@
|
||||
45A1684D2A1C308800C2432D /* AudioPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A1684C2A1C308800C2432D /* AudioPresentation.swift */; };
|
||||
45A2F005204473A3002E978A /* NewMessage.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45A2F004204473A3002E978A /* NewMessage.aifc */; };
|
||||
45A3579827DAAC6A0051CE8B /* UserProfileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */; };
|
||||
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */; };
|
||||
45B3680B2A1D75DF0067D05A /* AudioAllMediaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */; };
|
||||
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */; };
|
||||
45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */; };
|
||||
@ -592,7 +599,6 @@
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; };
|
||||
4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MessageActionsToolbar.swift */; };
|
||||
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
|
||||
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBBFE492306F5D300B37450 /* LogViewController.swift */; };
|
||||
4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
4CD675BE22E7BE35008010D2 /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */; };
|
||||
4CD675C522E7CF22008010D2 /* ConversationViewController+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */; };
|
||||
@ -718,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 */; };
|
||||
@ -850,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 */; };
|
||||
@ -1016,8 +1023,7 @@
|
||||
6646573F2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */; };
|
||||
664657412AC4FB720099DE1C /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657402AC4FB720099DE1C /* NotificationPresenter.swift */; };
|
||||
664657472ACB66630099DE1C /* TSAccountManagerObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */; };
|
||||
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */; };
|
||||
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */; };
|
||||
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */; };
|
||||
66485EB92CD17D6400B8613F /* DbRollbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */; };
|
||||
6649651C2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */; };
|
||||
6649651E2BDF169F00E2DE98 /* UIImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */; };
|
||||
@ -1057,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 */; };
|
||||
@ -1073,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 */; };
|
||||
@ -1157,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 */; };
|
||||
@ -1279,6 +1283,7 @@
|
||||
66D31DAD2BC48E0100EAF735 /* OWSContactAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */; };
|
||||
66D31DAF2BC48E3A00EAF735 /* OWSContactName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */; };
|
||||
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */; };
|
||||
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */; };
|
||||
66D709E928E3999400B5013A /* StoryContextAssociatedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */; };
|
||||
66D7B8FF2B9287F00005C98B /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */; };
|
||||
66D7B9012B92889E0005C98B /* AttachmentManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */; };
|
||||
@ -1446,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 */; };
|
||||
@ -1618,7 +1621,6 @@
|
||||
8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */; };
|
||||
8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */; };
|
||||
8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */; };
|
||||
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A088287F4514000E74A5 /* NewStorySheet.swift */; };
|
||||
8868A08A287F4551000E74A5 /* InteractiveSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880C2E01262A19DE006650B6 /* InteractiveSheetViewController.swift */; };
|
||||
8868A08C287F4F81000E74A5 /* OWSTableSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */; };
|
||||
886BB3D225BA0C9D00079781 /* PreviewWallpaperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ABAB8E25B8BE3F0008C78A /* PreviewWallpaperViewController.swift */; };
|
||||
@ -1671,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 */; };
|
||||
@ -2699,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 */; };
|
||||
@ -2739,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 */; };
|
||||
@ -2878,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 */; };
|
||||
@ -2894,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 */; };
|
||||
@ -3172,6 +3177,7 @@
|
||||
D9AE0AD929187F850063488B /* MessageSenderJobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */; };
|
||||
D9AE0ADD2918B2960063488B /* JobRecord+Columns.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */; };
|
||||
D9B0AC7429EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */; };
|
||||
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */; };
|
||||
D9B2E1182E748E1900A823E4 /* OWSByteCountFormatStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */; };
|
||||
D9B8541229137C150058F97B /* JobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B8541129137C150058F97B /* JobRecord.swift */; };
|
||||
D9B95A9629E6830B00D7CB95 /* JobRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */; };
|
||||
@ -3818,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 */; };
|
||||
@ -3957,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 */; };
|
||||
@ -4154,9 +4159,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManager.swift; sourceTree = "<group>"; };
|
||||
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetcher.swift; sourceTree = "<group>"; };
|
||||
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementModel.swift; sourceTree = "<group>"; };
|
||||
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementFetcher.swift; sourceTree = "<group>"; };
|
||||
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesService.swift; sourceTree = "<group>"; };
|
||||
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManagerTests.swift; sourceTree = "<group>"; };
|
||||
04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVCapsuleLabel.swift; sourceTree = "<group>"; };
|
||||
041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementMegaphone.swift; sourceTree = "<group>"; };
|
||||
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementReminderMegaphoneTests.swift; sourceTree = "<group>"; };
|
||||
042223B92EDF30B300158556 /* OutgoingUnpinMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingUnpinMessage.swift; sourceTree = "<group>"; };
|
||||
0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageIconView.swift; sourceTree = "<group>"; };
|
||||
0426758F2EC529F500124C5F /* TSInfoMessage+PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+PinnedMessage.swift"; sourceTree = "<group>"; };
|
||||
@ -4214,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>"; };
|
||||
@ -4740,7 +4749,6 @@
|
||||
45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTest.swift; sourceTree = "<group>"; };
|
||||
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
|
||||
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
|
||||
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalFileBrowserViewController.swift; sourceTree = "<group>"; };
|
||||
45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioAllMediaPresenter.swift; sourceTree = "<group>"; };
|
||||
45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "aurora-quiet.aifc"; sourceTree = "<group>"; };
|
||||
45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "synth-quiet.aifc"; sourceTree = "<group>"; };
|
||||
@ -4823,7 +4831,6 @@
|
||||
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = "<group>"; };
|
||||
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
||||
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
|
||||
4CBBFE492306F5D300B37450 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
|
||||
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = "<group>"; };
|
||||
4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+OWS.swift"; sourceTree = "<group>"; };
|
||||
@ -4983,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>"; };
|
||||
@ -5115,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>"; };
|
||||
@ -5286,8 +5294,7 @@
|
||||
6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRegistrationStateChangeManager.swift; sourceTree = "<group>"; };
|
||||
664657402AC4FB720099DE1C /* NotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = "<group>"; };
|
||||
664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAccountManagerObjcBridge.swift; sourceTree = "<group>"; };
|
||||
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveInternalErrorViewController.swift; sourceTree = "<group>"; };
|
||||
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorPresenter.swift; sourceTree = "<group>"; };
|
||||
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorStore.swift; sourceTree = "<group>"; };
|
||||
66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DbRollbackTests.swift; sourceTree = "<group>"; };
|
||||
6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Attachment.swift"; sourceTree = "<group>"; };
|
||||
6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Attachment.swift"; sourceTree = "<group>"; };
|
||||
@ -5328,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>"; };
|
||||
@ -5344,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>"; };
|
||||
@ -5430,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>"; };
|
||||
@ -5554,6 +5559,7 @@
|
||||
66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactAddress.swift; sourceTree = "<group>"; };
|
||||
66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactName.swift; sourceTree = "<group>"; };
|
||||
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalListMediaViewController.swift; sourceTree = "<group>"; };
|
||||
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBackupSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextAssociatedData.swift; sourceTree = "<group>"; };
|
||||
66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = "<group>"; };
|
||||
66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManagerImpl.swift; sourceTree = "<group>"; };
|
||||
@ -5647,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>"; };
|
||||
@ -5825,7 +5829,6 @@
|
||||
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = "<group>"; };
|
||||
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = "<group>"; };
|
||||
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = "<group>"; };
|
||||
8868A088287F4514000E74A5 /* NewStorySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStorySheet.swift; sourceTree = "<group>"; };
|
||||
8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSTableSheetViewController.swift; sourceTree = "<group>"; };
|
||||
886A58C8276A760600A1099B /* DonationSubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationSubscriptionManager.swift; sourceTree = "<group>"; };
|
||||
886A58C9276A760600A1099B /* DonationReceiptCredentialRedemptionJobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationReceiptCredentialRedemptionJobQueue.swift; sourceTree = "<group>"; };
|
||||
@ -5918,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>"; };
|
||||
@ -6977,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>"; };
|
||||
@ -7020,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>"; };
|
||||
@ -7163,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>"; };
|
||||
@ -7180,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>"; };
|
||||
@ -7461,6 +7467,7 @@
|
||||
D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderJobRecord.swift; sourceTree = "<group>"; };
|
||||
D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JobRecord+Columns.swift"; sourceTree = "<group>"; };
|
||||
D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift"; sourceTree = "<group>"; };
|
||||
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailIfThrowsRecordCursor.swift; sourceTree = "<group>"; };
|
||||
D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSByteCountFormatStyle.swift; sourceTree = "<group>"; };
|
||||
D9B8541129137C150058F97B /* JobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecord.swift; sourceTree = "<group>"; };
|
||||
D9B91D8D2B17E2A600BCB11A /* GroupCallRecordRingUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallRecordRingUpdateDelegate.swift; sourceTree = "<group>"; };
|
||||
@ -8133,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>"; };
|
||||
@ -8273,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>"; };
|
||||
@ -8458,6 +8464,14 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
040507132F80639B0078B769 /* RemoteReleaseNotes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */,
|
||||
);
|
||||
path = RemoteReleaseNotes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0436E4B12E5E2DC80011E125 /* Polls */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -9363,7 +9377,6 @@
|
||||
340FC87A204DAC8C007AEB0F /* AppSettings */,
|
||||
8809CE8822F93C0D00D38867 /* Attachment Keyboard */,
|
||||
883A7FC1269F4BE700841DF9 /* Avatars */,
|
||||
66485EB12CD03F3300B8613F /* BackupArchive */,
|
||||
342FFE6C271EF580000AC89F /* Categories */,
|
||||
F0B872B4269CF01E00D26481 /* ContextMenus */,
|
||||
34D8C0221ED3673300188D7C /* DebugUI */,
|
||||
@ -9887,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 = (
|
||||
@ -10208,15 +10237,6 @@
|
||||
path = DoubleTapToEdit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6640DD612ACDD5CD00CE9A8C /* LocalStorage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
|
||||
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
|
||||
);
|
||||
path = LocalStorage;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6645F30629BF8D1000B58EBD /* AccountAttributes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -10250,14 +10270,6 @@
|
||||
path = RegistrationStateChangeManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66485EB12CD03F3300B8613F /* BackupArchive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */,
|
||||
);
|
||||
path = BackupArchive;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6649651A2BDC6E8D00E2DE98 /* Playback */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -10299,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 = (
|
||||
@ -10355,15 +10353,20 @@
|
||||
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */,
|
||||
6640DD612ACDD5CD00CE9A8C /* LocalStorage */,
|
||||
66C2B13B2A0E9108008DDE72 /* SVR2 */,
|
||||
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
|
||||
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */,
|
||||
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
|
||||
66138FB5298326C7002E0CFE /* SecureValueRecovery.swift */,
|
||||
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
|
||||
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
|
||||
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
|
||||
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
|
||||
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
|
||||
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
|
||||
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
|
||||
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
|
||||
66C2B1372A0DB6A9008DDE72 /* SVRAuthCredential.swift */,
|
||||
6673FF6F2978C40300F96CFD /* SVRAuthCredentialStorage.swift */,
|
||||
6673FF712979B33800F96CFD /* SVRAuthCredentialStorageImpl.swift */,
|
||||
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
|
||||
66C2B1352A0DB02E008DDE72 /* SVRUtil.swift */,
|
||||
);
|
||||
path = SecureValueRecovery;
|
||||
@ -10706,18 +10709,6 @@
|
||||
path = V2;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66C2B13B2A0E9108008DDE72 /* SVR2 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
|
||||
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
|
||||
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
|
||||
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
|
||||
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
|
||||
);
|
||||
path = SVR2;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66C2B1422A12E043008DDE72 /* SecureValueRecovery */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -10727,25 +10718,6 @@
|
||||
path = SecureValueRecovery;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
66C2B14E2A13F0BC008DDE72 /* Mocks */,
|
||||
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
|
||||
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
|
||||
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
|
||||
);
|
||||
path = SgxWebsocketConnection;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66C2B14E2A13F0BC008DDE72 /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66CD25572B0685CF00139E17 /* Archivers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -11429,12 +11401,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
344A761024B366F4009D69A5 /* FlagsViewController.swift */,
|
||||
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */,
|
||||
665229882E218D53002C14A0 /* InternalDiskUsageViewController.swift */,
|
||||
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */,
|
||||
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */,
|
||||
8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */,
|
||||
663883562D4C034F008EA898 /* InternalSQLClientViewController.swift */,
|
||||
4CBBFE492306F5D300B37450 /* LogViewController.swift */,
|
||||
344A761224B36C8C009D69A5 /* TestingViewController.swift */,
|
||||
);
|
||||
path = Internal;
|
||||
@ -11468,7 +11439,10 @@
|
||||
children = (
|
||||
88A505FE23DBAE640005C012 /* UserInterface */,
|
||||
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */,
|
||||
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */,
|
||||
D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */,
|
||||
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */,
|
||||
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */,
|
||||
);
|
||||
path = Megaphones;
|
||||
sourceTree = "<group>";
|
||||
@ -11482,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 */,
|
||||
@ -11639,6 +11613,7 @@
|
||||
88E34F2522F269B600966CC2 /* StorageService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9C5CB12289453B200548EEE /* StorageService.swift */,
|
||||
88E34F2622F269E900966CC2 /* StorageServiceManager.swift */,
|
||||
88E34F2822F26CC100966CC2 /* StorageServiceProto+Sync.swift */,
|
||||
D927372C2CD2DD0D00E15D95 /* StorageServiceRecordIkmMigrator.swift */,
|
||||
@ -11665,6 +11640,7 @@
|
||||
D99ABC712A3D0BAA0034CD3B /* QRCodes */,
|
||||
50791B1B2D037A7800D747F8 /* RecipientPickers */,
|
||||
661278052996BA6700A1D5A1 /* Registration */,
|
||||
040507132F80639B0078B769 /* RemoteReleaseNotes */,
|
||||
4C3EF8002109184A0007EBF7 /* SSKTests */,
|
||||
D97046082E81D5B60034C05D /* Storage */,
|
||||
E75DD3DC2810CD3500E32C36 /* subscriptions */,
|
||||
@ -11814,7 +11790,6 @@
|
||||
88F5D78B2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift */,
|
||||
88F5D7892880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift */,
|
||||
880FB3F228CC161800FA1C10 /* NewStoryHeaderView.swift */,
|
||||
8868A088287F4514000E74A5 /* NewStorySheet.swift */,
|
||||
66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */,
|
||||
B99B155C2A71BA5200E26DAC /* StoryContextViewState.swift */,
|
||||
88B6D67128076F37005D86EC /* StoryMessage+SignalUI.swift */,
|
||||
@ -13368,9 +13343,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */,
|
||||
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */,
|
||||
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */,
|
||||
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */,
|
||||
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */,
|
||||
D98DD85E28EE53B00089333E /* RemoteMegaphoneModel.swift */,
|
||||
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */,
|
||||
);
|
||||
path = Megaphones;
|
||||
sourceTree = "<group>";
|
||||
@ -13545,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 */,
|
||||
@ -13689,7 +13668,7 @@
|
||||
665C0D5F2ADF57D000539A37 /* BackupArchive+Shims.swift */,
|
||||
661429182D35B9EA0043AA22 /* BackupArchive+Timestamp.swift */,
|
||||
04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */,
|
||||
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */,
|
||||
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */,
|
||||
66232AE02CC0271F00AE6A76 /* BackupArchiveFullTextSearchIndexer.swift */,
|
||||
665C0D5B2ADF538100539A37 /* BackupArchiveManager.swift */,
|
||||
665C0D5D2ADF53E200539A37 /* BackupArchiveManagerImpl.swift */,
|
||||
@ -13931,8 +13910,6 @@
|
||||
children = (
|
||||
D90AA32E2CC9616A00021CB0 /* Signal-Message-Backup-Tests */,
|
||||
D90AA6182CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift */,
|
||||
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */,
|
||||
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */,
|
||||
04E66D432E00AB3A0059DBAC /* BackupSettingsStoreTests.swift */,
|
||||
D9A36B922C7FEDA100CEC0E7 /* LineByLineStringDiff.swift */,
|
||||
);
|
||||
@ -14132,15 +14109,6 @@
|
||||
path = DisappearingMessages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
|
||||
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
|
||||
);
|
||||
path = AccountEntropyPool;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D9F6553029D6530B002A330A /* SDSCodableModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -14611,7 +14579,6 @@
|
||||
F900F2DC27F25AB300431E09 /* DonationReceiptViewController.swift */,
|
||||
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */,
|
||||
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */,
|
||||
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */,
|
||||
);
|
||||
path = Donations;
|
||||
sourceTree = "<group>";
|
||||
@ -14623,6 +14590,7 @@
|
||||
D92EFDEB2F68EB7D0031D257 /* AttachmentBackfill */,
|
||||
7255A4C32B98D5A800E95368 /* Attachments */,
|
||||
720547F12B9C8F5E00E2CF2F /* Avatars */,
|
||||
F9C5CA52289453B100548EEE /* Axolotl */,
|
||||
665C0D5A2ADF537000539A37 /* Backups */,
|
||||
F945FE482984795A00C835C7 /* Calls */,
|
||||
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
|
||||
@ -14656,7 +14624,6 @@
|
||||
046092252FBCD28300A8765F /* SafetyTips */,
|
||||
50B791552E8B39230063E71E /* Search */,
|
||||
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
|
||||
F9C5CB98289453B200548EEE /* Security */,
|
||||
F9C5CAB4289453B200548EEE /* Spam */,
|
||||
F9C5CA2F289453B100548EEE /* Storage */,
|
||||
88E34F2522F269B600966CC2 /* StorageService */,
|
||||
@ -14684,6 +14651,7 @@
|
||||
F94261FF289B1B5400460798 /* Account */,
|
||||
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
|
||||
50ED28002F0EDAFB00E57C54 /* Attachments */,
|
||||
50DAF7E12FD87BFD00BE7430 /* Backups */,
|
||||
F945FE4B298481D800C835C7 /* Calls */,
|
||||
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
|
||||
50E0198E2CC2491A0063EA48 /* Concurrency */,
|
||||
@ -14772,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 */,
|
||||
@ -15057,13 +15026,16 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6646572F2AC369EB0099DE1C /* PhoneNumberDiscoverabilityManager */,
|
||||
6659A0242A7C112700066AB7 /* PreKeys */,
|
||||
661170BF2ABA458800A1B16D /* TSAccountManager */,
|
||||
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
|
||||
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
|
||||
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
|
||||
50F401CB2D483BF40094CA56 /* DeviceId.swift */,
|
||||
50D6BDEE2ED6724600CC012E /* DeviceType.swift */,
|
||||
D9F399AC2A95798A001599EC /* IdentityKeyChecker.swift */,
|
||||
D9F399B12A96D65D001599EC /* IdentityKeyMismatchManager.swift */,
|
||||
5033D46629D76BD0007FEADA /* LocalIdentifiers.swift */,
|
||||
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
|
||||
72552EF32C9EF9E7008614AF /* OWSIdentity.swift */,
|
||||
D9CAF74F2A0ACFF20049193A /* PniDistributionParameterBuilder.swift */,
|
||||
C18E3C712A9FF65D003D1CF1 /* PniDistributionSyncMessage.swift */,
|
||||
@ -15192,7 +15164,6 @@
|
||||
F9C5CA2F289453B100548EEE /* Storage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9C5CA52289453B100548EEE /* AxolotlStore */,
|
||||
F9C5CA31289453B100548EEE /* Database */,
|
||||
667DEE562BC7148E00EFF32D /* MediaGallery */,
|
||||
F9C5CA9B289453B100548EEE /* BaseModel.h */,
|
||||
@ -15222,6 +15193,7 @@
|
||||
F9B652C228D8E3DF006914CA /* DatabaseRecovery.swift */,
|
||||
D9FF515B2F03A2A10011982F /* DBUInt64.swift */,
|
||||
F9C5CA48289453B100548EEE /* DeepCopy.swift */,
|
||||
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */,
|
||||
F9C5CA40289453B100548EEE /* GRDBDatabaseStorageAdapter.swift */,
|
||||
F9C5CA47289453B100548EEE /* GRDBSchemaMigrator.swift */,
|
||||
D9B95A9929E8918200D7CB95 /* InMemoryDB.swift */,
|
||||
@ -15260,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 */ = {
|
||||
@ -15332,13 +15305,14 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9C5CAD3289453B200548EEE /* API */,
|
||||
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */,
|
||||
88DF819328E112F600F8BA80 /* SignalProxy */,
|
||||
669E8FE528B4149200043D28 /* BaseOWSURLSessionMock.swift */,
|
||||
727328062CA6CF530080E2C7 /* Certificates.swift */,
|
||||
F9C5CAC4289453B200548EEE /* ChatConnectionManager.swift */,
|
||||
509A8DC12E25817E0024BF14 /* ConnectionLock.swift */,
|
||||
F9C5CAF7289453B200548EEE /* ContentProxy.swift */,
|
||||
F9C5CAF2289453B200548EEE /* HttpHeaders.swift */,
|
||||
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
|
||||
F9C5CAF1289453B200548EEE /* NetworkInterfaceSet.swift */,
|
||||
F9C5CAC8289453B200548EEE /* OutageDetection.swift */,
|
||||
72328C8A2C6C7322000EA728 /* OWSCensorshipConfiguration.swift */,
|
||||
@ -15415,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;
|
||||
@ -15436,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 */,
|
||||
@ -15518,7 +15491,6 @@
|
||||
7634F08C2A21963600BB93D5 /* Sounds.swift */,
|
||||
F9613CDB2981F11400894B55 /* SqliteUtil.swift */,
|
||||
F9C5CB47289453B200548EEE /* SSKPreferences.swift */,
|
||||
F9C5CB12289453B200548EEE /* StorageService.swift */,
|
||||
72DB95AD2C8C7C7B00FD2266 /* String+OWS.swift */,
|
||||
F9C5CB09289453B200548EEE /* String+SSK.swift */,
|
||||
668A010A2C2B602F007B8808 /* StringSanitizer.swift */,
|
||||
@ -15566,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 = (
|
||||
@ -17876,7 +17835,6 @@
|
||||
88F5D78C2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift in Sources */,
|
||||
88F5D78A2880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift in Sources */,
|
||||
880FB3F328CC161800FA1C10 /* NewStoryHeaderView.swift in Sources */,
|
||||
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */,
|
||||
3402AAAC271D9E180084CBAE /* NonContactTableViewCell.swift in Sources */,
|
||||
507C07402F116E9200ECFEFA /* NormalizedImage.swift in Sources */,
|
||||
3402AAAB271D9E180084CBAE /* OWSActionSheets.swift in Sources */,
|
||||
@ -18093,7 +18051,6 @@
|
||||
4C2F454F214C00E1004871FF /* AvatarTableViewCell.swift in Sources */,
|
||||
32C584A825B81C6600256804 /* AvatarViewController.swift in Sources */,
|
||||
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */,
|
||||
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */,
|
||||
D932C0EB2E13AD3F00FEF9C3 /* BackupAttachmentDownloadTracker.swift in Sources */,
|
||||
D93964B62E038C7B00094117 /* BackupAttachmentUploadTracker.swift in Sources */,
|
||||
66A1F4E62E03641D0095DE4B /* BackupBGProcessingTaskRunner.swift in Sources */,
|
||||
@ -18102,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 */,
|
||||
@ -18490,12 +18449,12 @@
|
||||
D9E43C072CC194140001536E /* IndividualCallViewController.swift in Sources */,
|
||||
D97046062E81D4240034C05D /* InfoMessageGroupUpdateMigrator.swift in Sources */,
|
||||
88BCCC8123837B7D00CE5FE6 /* InteractionReactionState.swift in Sources */,
|
||||
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */,
|
||||
665229892E218D5F002C14A0 /* InternalDiskUsageViewController.swift in Sources */,
|
||||
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */,
|
||||
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */,
|
||||
8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */,
|
||||
663883572D4C0360008EA898 /* InternalSQLClientViewController.swift in Sources */,
|
||||
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */,
|
||||
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */,
|
||||
32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */,
|
||||
45C845AD291466C0005F6EA5 /* JournalingOrderedDictionary.swift in Sources */,
|
||||
5045F44229E0DB7100058E5F /* LaunchJobs.swift in Sources */,
|
||||
@ -18515,7 +18474,6 @@
|
||||
4C25768A23AD510800E0398D /* LoadMoreMessagesView.swift in Sources */,
|
||||
D9E43C082CC194140001536E /* LocalVideoView.swift in Sources */,
|
||||
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */,
|
||||
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */,
|
||||
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */,
|
||||
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */,
|
||||
5033D46929D7951F007FEADA /* MainAppContext.swift in Sources */,
|
||||
@ -18611,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 */,
|
||||
@ -18702,9 +18659,12 @@
|
||||
F9E3006C299D76C3000323F8 /* RegistrationVerificationViewController.swift in Sources */,
|
||||
F95D71A3299305C400ED3102 /* RegistrationViewUtil.swift in Sources */,
|
||||
50EA40912E3A899F009CB839 /* RegistrationWebSocketManager.swift in Sources */,
|
||||
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
|
||||
D997FA7628F8E3A2003C7B8B /* RemoteMegaphone.swift in Sources */,
|
||||
509DC8DA2BCED88600375E86 /* RemoteMegaphoneFetcher.swift in Sources */,
|
||||
55B753602D97304100CCC91C /* RemoteMuteToast.swift in Sources */,
|
||||
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
|
||||
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
|
||||
D9E43C0D2CC194140001536E /* RemoteVideoView.swift in Sources */,
|
||||
348433DF243CA94600C7F64A /* ReplaceAdminViewController.swift in Sources */,
|
||||
F952C0A629C8DA5E00D93766 /* RequestAccountDataReportViewController.swift in Sources */,
|
||||
@ -18884,6 +18844,11 @@
|
||||
E16B440E2BBF242C00D2583E /* ReactionsModelTest.swift in Sources */,
|
||||
661278082996BA8900A1D5A1 /* RegistrationCoordinatorTest.swift in Sources */,
|
||||
6612780D2996BD0300A1D5A1 /* RegistrationCoordinatorTestShims.swift in Sources */,
|
||||
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
|
||||
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */,
|
||||
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
|
||||
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
|
||||
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */,
|
||||
F5C80FA22BE3F29F0028F76D /* RTCIceServerFetcherTest.swift in Sources */,
|
||||
F963164B291AE06C00218FB7 /* ScrubbingLogFormatterTest.swift in Sources */,
|
||||
505C2ED92997422D00C23FB2 /* SelfSignedIdentityTest.swift in Sources */,
|
||||
@ -19019,7 +18984,7 @@
|
||||
66F6D69E2C77E4C500EFAF75 /* BackupArchiveContactAttachmentArchiver.swift in Sources */,
|
||||
66CD256E2B06E14F00139E17 /* BackupArchiveContactRecipientArchiver.swift in Sources */,
|
||||
C1CA5F8E2BE2F21C00D733CA /* BackupArchiveDistributionListRecipientArchiver.swift in Sources */,
|
||||
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */,
|
||||
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */,
|
||||
D91D9C8C2C3F06400009E4F7 /* BackupArchiveExpirationTimerChatUpdateArchiver.swift in Sources */,
|
||||
66232AE12CC0272900AE6A76 /* BackupArchiveFullTextSearchIndexer.swift in Sources */,
|
||||
D9A85DC22BE1719C003F7045 /* BackupArchiveGroupCallArchiver.swift in Sources */,
|
||||
@ -19112,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 */,
|
||||
@ -19286,13 +19250,14 @@
|
||||
F9C5CE44289453B400548EEE /* Error+IsRetryable.swift in Sources */,
|
||||
F9C5CE23289453B400548EEE /* Error+SSK.swift in Sources */,
|
||||
D9C7CEB428EB8495001E87B6 /* ExperienceUpgrade.swift in Sources */,
|
||||
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */,
|
||||
D9C7CECB28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift in Sources */,
|
||||
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */,
|
||||
D98BC5332EE387A30052A81F /* ExpirationJob.swift in Sources */,
|
||||
C1FB9B752B16498C00D51A3B /* ExternalPendingDonationStore.swift in Sources */,
|
||||
F9C5CE57289453B400548EEE /* Factories.swift in Sources */,
|
||||
F9C5CC1D289453B300548EEE /* FailedMessagesJob.swift in Sources */,
|
||||
7255A4C82B98DF3E00E95368 /* FailedStorySendDisplayController.swift in Sources */,
|
||||
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */,
|
||||
F9C5CE60289453B400548EEE /* FakeContactsManager.swift in Sources */,
|
||||
F94BFA9528EBB0D800A5F34E /* FakeMessageSender.swift in Sources */,
|
||||
F9C5CE54289453B400548EEE /* FakeStorageServiceManager.swift in Sources */,
|
||||
@ -19416,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 */,
|
||||
@ -19581,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 */,
|
||||
@ -19703,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 */,
|
||||
@ -19781,9 +19745,11 @@
|
||||
6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */,
|
||||
6646573B2AC388C70099DE1C /* RegistrationStateChangeManager.swift in Sources */,
|
||||
6646573D2AC3894D0099DE1C /* RegistrationStateChangeManagerImpl.swift in Sources */,
|
||||
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */,
|
||||
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */,
|
||||
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */,
|
||||
F9C5CE17289453B400548EEE /* RemoteConfigManager.swift in Sources */,
|
||||
D98DD86028EE53B00089333E /* RemoteMegaphoneModel.swift in Sources */,
|
||||
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */,
|
||||
5063B41E2C5432A30041CA51 /* ResolvableValue.swift in Sources */,
|
||||
502C69742B06F0A400012867 /* Result.swift in Sources */,
|
||||
50C0203E2CA4A7A500BDC4EF /* Retry.swift in Sources */,
|
||||
@ -19858,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 */,
|
||||
@ -20129,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 */,
|
||||
@ -20227,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()
|
||||
@ -148,9 +148,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
debugLogger.enableFileLogging(appContext: mainAppContext, canLaunchInBackground: true)
|
||||
DebugLogger.configureSwiftLogging()
|
||||
if DebugFlags.audibleErrorLogging {
|
||||
debugLogger.enableErrorReporting()
|
||||
}
|
||||
|
||||
Logger.warn("Launching…")
|
||||
defer { Logger.info("Launched.") }
|
||||
@ -372,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()
|
||||
}
|
||||
|
||||
@ -400,7 +398,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
let dataMigrationContinuation = globalsContinuation.initGlobals(
|
||||
appContext: launchContext.appContext,
|
||||
appReadiness: appReadiness,
|
||||
backupArchiveErrorPresenterFactory: BackupArchiveErrorPresenterFactoryInternal(),
|
||||
deviceBatteryLevelManager: DeviceBatteryLevelManagerImpl(),
|
||||
deviceSleepManager: launchContext.deviceSleepManager,
|
||||
paymentsEvents: PaymentsEventsMainApp(),
|
||||
@ -646,16 +643,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let remoteMegaphoneFetcher = RemoteMegaphoneFetcher(
|
||||
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
|
||||
signalService: SSKEnvironment.shared.signalServiceRef,
|
||||
let remoteReleaseNotesFetchingManager = RemoteReleaseNotesFetchingManager(
|
||||
db: DependenciesBridge.shared.db,
|
||||
remoteReleaseNotesService: DependenciesBridge.shared.remoteReleaseNotesService,
|
||||
)
|
||||
cron.schedulePeriodically(
|
||||
uniqueKey: .fetchMegaphones,
|
||||
approximateInterval: 3 * .day,
|
||||
mustBeRegistered: false,
|
||||
mustBeConnected: true,
|
||||
operation: { try await remoteMegaphoneFetcher.syncRemoteMegaphones() },
|
||||
operation: { try await remoteReleaseNotesFetchingManager.syncRemoteReleaseNotes() },
|
||||
)
|
||||
|
||||
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
||||
@ -720,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()
|
||||
|
||||
@ -781,6 +779,27 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
operation: { try await blockingManager.syncBlockListIfNecessary(force: false) },
|
||||
)
|
||||
|
||||
let svr = DependenciesBridge.shared.svr
|
||||
|
||||
// We must refresh our SVR2 credentials periodically. We typically do this
|
||||
// when updating to a new version, but we want to refresh it after 14 days
|
||||
// if we haven't upgraded.
|
||||
cron.schedulePeriodically(
|
||||
uniqueKey: .refreshSVRCredentials,
|
||||
approximateInterval: 14 * .day,
|
||||
mustBeRegistered: true,
|
||||
mustBeDeviceType: .primary,
|
||||
mustBeConnected: true,
|
||||
operation: { try await svr.refreshCredentialsIfNecessary() },
|
||||
)
|
||||
|
||||
cron.scheduleFrequently(
|
||||
mustBeRegistered: true,
|
||||
mustBeDeviceType: .primary,
|
||||
mustBeConnected: true,
|
||||
operation: { try await svr.refreshBackupIfNecessary() },
|
||||
)
|
||||
|
||||
// Warm the "available emoji" cache, intentionally off the main thread.
|
||||
Task.detached {
|
||||
Emoji.warmAvailableCache()
|
||||
@ -790,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 {
|
||||
@ -1233,14 +1252,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
switch action {
|
||||
case .submitDebugLogsAndCrash:
|
||||
addSubmitDebugLogsAction {
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
|
||||
DebugLogs(dumper: logDumper).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
) {
|
||||
owsFail("Exiting after submitting debug logs")
|
||||
}
|
||||
}
|
||||
|
||||
case .submitDebugLogsAndLaunchApp(let window, let launchContext):
|
||||
addSubmitDebugLogsAction { [unowned window] in
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
|
||||
DebugLogs(dumper: logDumper).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
) {
|
||||
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
|
||||
}
|
||||
}
|
||||
@ -1367,7 +1392,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
refreshConnection(isAppActive: true, shouldRunCron: true)
|
||||
refreshConnection(isAppActive: true)
|
||||
|
||||
// Every time we become active...
|
||||
if registeredState != nil {
|
||||
@ -1435,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
|
||||
@ -1443,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()
|
||||
@ -1462,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 {
|
||||
@ -1745,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
|
||||
@ -1764,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")
|
||||
@ -1776,6 +1814,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
callService.initiateCall(to: callTarget, isVideo: isVideo)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1790,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.
|
||||
@ -1908,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()
|
||||
@ -1919,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
|
||||
@ -284,11 +287,7 @@ public class AppEnvironment: NSObject {
|
||||
// Things that should run on either the primary or linked devices.
|
||||
if let registeredState, registeredState.isPrimary {
|
||||
Task {
|
||||
do {
|
||||
try await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
|
||||
} catch {
|
||||
Logger.warn("Couldn't perform avatar default color migration: \(error)")
|
||||
}
|
||||
await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
|
||||
}
|
||||
|
||||
Task {
|
||||
@ -329,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)
|
||||
|
||||
@ -128,6 +128,7 @@ public class SignalApp {
|
||||
owsFailDebug("Missing conversationSplitViewController.")
|
||||
return
|
||||
}
|
||||
|
||||
conversationSplitViewController.showAppSettingsWithMode(mode, completion: completion)
|
||||
}
|
||||
|
||||
|
||||
@ -32,8 +32,8 @@ struct AvatarDefaultColorStorageServiceMigrator {
|
||||
self.threadStore = threadStore
|
||||
}
|
||||
|
||||
func performMigrationIfNecessary() async throws {
|
||||
try await db.awaitableWrite { tx in
|
||||
func performMigrationIfNecessary() async {
|
||||
await db.awaitableWrite { tx in
|
||||
if kvStore.hasValue(StoreKeys.hasEnqueuedMigrationKey, transaction: tx) {
|
||||
return
|
||||
}
|
||||
@ -46,15 +46,14 @@ struct AvatarDefaultColorStorageServiceMigrator {
|
||||
}
|
||||
|
||||
var groupV2MasterKeys = [GroupMasterKey]()
|
||||
try threadStore.enumerateGroupThreads(tx: tx) { groupThread in
|
||||
guard
|
||||
threadStore.enumerateGroupThreads(tx: tx) { groupThread in
|
||||
if
|
||||
let groupModelV2 = groupThread.groupModel as? TSGroupModelV2,
|
||||
let groupMasterKey = try? groupModelV2.masterKey()
|
||||
else {
|
||||
return true
|
||||
{
|
||||
groupV2MasterKeys.append(groupMasterKey)
|
||||
}
|
||||
|
||||
groupV2MasterKeys.append(groupMasterKey)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -225,7 +225,6 @@ final class BackupDisablingManager {
|
||||
|
||||
accountEntropyPoolManager.setAccountEntropyPool(
|
||||
newAccountEntropyPool: try! AccountEntropyPool(key: aepBeingRotatedString),
|
||||
disablePIN: false,
|
||||
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
|
||||
@ -120,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
|
||||
@ -179,6 +180,8 @@ class BackupSettingsViewController:
|
||||
presentWelcomeToBackupsSheet()
|
||||
case .automaticallyStartBackup:
|
||||
performManualBackup()
|
||||
case .disableOptimizeLocalStorage:
|
||||
setOptimizeLocalStorage(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -618,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)
|
||||
@ -1018,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: {})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1085,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(
|
||||
@ -1141,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(
|
||||
@ -1158,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
|
||||
@ -1361,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.",
|
||||
@ -1390,7 +1468,6 @@ class BackupSettingsViewController:
|
||||
|
||||
accountEntropyPoolManager.setAccountEntropyPool(
|
||||
newAccountEntropyPool: newCandidateAEP,
|
||||
disablePIN: false,
|
||||
tx: tx,
|
||||
)
|
||||
case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
|
||||
@ -1633,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
|
||||
@ -1915,44 +1997,32 @@ struct BackupSettingsView: View {
|
||||
viewModel: viewModel,
|
||||
)
|
||||
|
||||
if BuildFlags.Backups.showOptimizeMedia {
|
||||
Toggle(
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
|
||||
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
|
||||
),
|
||||
isOn: Binding(
|
||||
get: { viewModel.optimizeLocalStorage },
|
||||
set: { viewModel.setOptimizeLocalStorage($0) },
|
||||
),
|
||||
).disabled(!viewModel.optimizeLocalStorageAvailable)
|
||||
}
|
||||
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 BuildFlags.Backups.showOptimizeMedia {
|
||||
let footerText: String = if
|
||||
viewModel.optimizeLocalStorageAvailable,
|
||||
viewModel.isPaidPlanTester
|
||||
{
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester.",
|
||||
)
|
||||
} else if viewModel.optimizeLocalStorageAvailable {
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
|
||||
)
|
||||
} else {
|
||||
OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
|
||||
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
|
||||
)
|
||||
}
|
||||
|
||||
Text(footerText)
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
.font(.caption)
|
||||
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 {
|
||||
@ -3209,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,
|
||||
|
||||
@ -71,6 +71,7 @@ struct BackupOnboardingIntroView: View {
|
||||
|
||||
Image(.backupsLogo)
|
||||
.frame(width: 80, height: 80)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Spacer().frame(height: 16)
|
||||
HStack {
|
||||
|
||||
@ -192,13 +192,16 @@ class CallQualitySurveyManager {
|
||||
return proto
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
func submit(
|
||||
rating: CallQualitySurvey.Rating,
|
||||
logsToSubmit logs: DebugLogs?,
|
||||
) {
|
||||
var proto = buildProto(rating: rating)
|
||||
|
||||
Task {
|
||||
if shouldSubmitDebugLogs {
|
||||
if let logs {
|
||||
do {
|
||||
let debugLogURL = try await DebugLogs.uploadLogs(dumper: .fromGlobals())
|
||||
let debugLogURL = try await logs.uploadLogs()
|
||||
proto.debugLogURL = debugLogURL.absoluteString
|
||||
} catch {
|
||||
logger.error("Failed to submit debug logs: \(error)")
|
||||
|
||||
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
@ -2302,23 +2302,14 @@ private extension CallsListViewController {
|
||||
}()
|
||||
|
||||
private func makeStartCallButton(viewModel: CallViewModel) -> UIButton {
|
||||
var config = UIButton.Configuration.gray()
|
||||
config.cornerStyle = .capsule
|
||||
config.background.backgroundInsets = .init(margin: 2)
|
||||
config.baseBackgroundColor = UIColor.Signal.tertiaryFill
|
||||
config.baseForegroundColor = UIColor.Signal.label
|
||||
|
||||
let icon: ThemeIcon = switch viewModel.medium {
|
||||
case .audio:
|
||||
.buttonVoiceCall
|
||||
case .video, .link:
|
||||
.buttonVideoCall
|
||||
}
|
||||
|
||||
config.image = Theme.iconImage(icon)
|
||||
|
||||
let button = UIButton(
|
||||
configuration: config,
|
||||
configuration: .roundGray(image: Theme.iconImage(icon)),
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
self?.detailsTapped(viewModel: viewModel)
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -16,10 +16,12 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
private let tableViewController = OWSTableViewController2()
|
||||
|
||||
private var shouldSubmitDebugLogs = false
|
||||
private var logs: DebugLogs
|
||||
|
||||
private let rating: CallQualitySurvey.Rating
|
||||
|
||||
init(rating: CallQualitySurvey.Rating) {
|
||||
self.logs = DebugLogs(dumper: .fromGlobals())
|
||||
self.rating = rating
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
@ -129,7 +131,8 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
let container = UIView()
|
||||
|
||||
let textView = LinkingTextView { [weak self] in
|
||||
self?.showDebugLogPreview()
|
||||
guard let self else { return }
|
||||
self.logs.showPreview(from: self)
|
||||
}
|
||||
textView.attributedText = .composed(of: [
|
||||
OWSLocalizedString(
|
||||
@ -193,12 +196,6 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
private func showDebugLogPreview() {
|
||||
let vc = DebugLogPreviewViewController()
|
||||
let nav = OWSNavigationController(rootViewController: vc)
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
override func customSheetHeight() -> CGFloat? {
|
||||
let headerHeight = headerContainer.height
|
||||
let collectionViewHeight = tableViewController.tableView.contentSize.height + tableViewController.tableView.contentInset.totalHeight
|
||||
@ -209,7 +206,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
private func submit() {
|
||||
sheetNav?.submit(
|
||||
rating: self.rating,
|
||||
shouldSubmitDebugLogs: self.shouldSubmitDebugLogs,
|
||||
logsToSubmit: shouldSubmitDebugLogs ? logs : nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,10 +82,13 @@ final class CallQualitySurveyNavigationController: UINavigationController {
|
||||
pushViewController(vc, animated: false)
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
func submit(
|
||||
rating: CallQualitySurvey.Rating,
|
||||
logsToSubmit: DebugLogs?,
|
||||
) {
|
||||
callQualitySurveyManager.submit(
|
||||
rating: rating,
|
||||
shouldSubmitDebugLogs: shouldSubmitDebugLogs,
|
||||
logsToSubmit: logsToSubmit,
|
||||
)
|
||||
let host = presentingViewController
|
||||
dismiss(animated: true) {
|
||||
|
||||
@ -159,11 +159,10 @@ 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
|
||||
BuildFlags.MemberLabel.display,
|
||||
let groupThread = self?.groupThread,
|
||||
let senderAci = address.aci,
|
||||
let memberLabelString = groupThread.groupModel.groupMembership.memberLabel(for: senderAci)?.labelForRendering(),
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,14 +192,19 @@ class CVAttachmentProgressView: ManualLayoutView {
|
||||
applyState(.tapToDownload, animated: animateStateChange)
|
||||
case .enqueuedOrDownloading:
|
||||
applyState(.unknownProgress, animated: animateStateChange)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(processDownloadNotification(notification:)),
|
||||
name: AttachmentDownloads.attachmentDownloadProgressNotification,
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(processDownloadNotification(notification:)),
|
||||
name: AttachmentDownloads.attachmentDownloadProgressNotification,
|
||||
object: nil,
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(processDownloadStoppedNotification(notification:)),
|
||||
name: AttachmentDownloads.attachmentDownloadStoppedNotification,
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,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()
|
||||
|
||||
@ -65,7 +65,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
let hasWallpaper = conversationStyle.hasWallpaper
|
||||
let wallpaperModeHasChanged = hasWallpaper != componentView.hasWallpaper
|
||||
let isReusing = componentView.rootView.superview != nil
|
||||
&& componentView.label.superview != nil
|
||||
&& componentView.innerStack.superview != nil
|
||||
&& !wallpaperModeHasChanged
|
||||
|
||||
if !isReusing {
|
||||
@ -75,19 +75,32 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
componentView.hasWallpaper = hasWallpaper
|
||||
|
||||
labelConfig.applyForRendering(label: componentView.label)
|
||||
chevronConfig.applyForRendering(label: componentView.chevronLabel)
|
||||
|
||||
if isReusing {
|
||||
componentView.innerStack.configureForReuse(
|
||||
config: innerStackConfig,
|
||||
cellMeasurement: cellMeasurement,
|
||||
measurementKey: Self.measurementKey_innerStack,
|
||||
)
|
||||
componentView.outerStack.configureForReuse(
|
||||
config: outerStackConfig,
|
||||
cellMeasurement: cellMeasurement,
|
||||
measurementKey: Self.measurementKey_outerStack,
|
||||
)
|
||||
} else {
|
||||
componentView.innerStack.configure(
|
||||
config: innerStackConfig,
|
||||
cellMeasurement: cellMeasurement,
|
||||
measurementKey: Self.measurementKey_innerStack,
|
||||
subviews: [componentView.label, componentView.chevronContainer],
|
||||
)
|
||||
|
||||
componentView.outerStack.configure(
|
||||
config: outerStackConfig,
|
||||
cellMeasurement: cellMeasurement,
|
||||
measurementKey: Self.measurementKey_outerStack,
|
||||
subviews: [componentView.label],
|
||||
subviews: [componentView.innerStack],
|
||||
)
|
||||
|
||||
let bubbleView: UIView
|
||||
@ -110,13 +123,20 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
}
|
||||
componentView.outerStack.addSubview(bubbleView)
|
||||
componentView.outerStack.sendSubviewToBack(bubbleView)
|
||||
// This seemed easier than adding an entirely new ManualStackView
|
||||
// just to constrain the label and background to
|
||||
componentView.outerStack.addLayoutBlock { [label = componentView.label] _ in
|
||||
bubbleView.frame = label.frame.inset(by: Self.backgroundLayoutInsets)
|
||||
componentView.outerStack.addLayoutBlock { [innerStack = componentView.innerStack] _ in
|
||||
bubbleView.frame = innerStack.frame.inset(by: Self.backgroundLayoutInsets)
|
||||
}
|
||||
componentView.innerStack.addLayoutBlock { [chevronContainer = componentView.chevronContainer, chevronLabel = componentView.chevronLabel] _ in
|
||||
chevronLabel.bounds.size = chevronContainer.bounds.size
|
||||
chevronLabel.center = CGPoint(x: chevronContainer.bounds.midX, y: chevronContainer.bounds.midY)
|
||||
}
|
||||
}
|
||||
|
||||
componentView.isShowingExpanded = collapseSet.isExpanded
|
||||
componentView.chevronLabel.transform = collapseSet.isExpanded
|
||||
? CGAffineTransform(rotationAngle: -.pi)
|
||||
: .identity
|
||||
|
||||
if
|
||||
hasWallpaper,
|
||||
let wallpaperBlurView = componentView.wallpaperBlurView
|
||||
@ -128,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
|
||||
@ -147,6 +159,35 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
componentView: CVComponentView,
|
||||
renderItem: CVRenderItem,
|
||||
) -> Bool {
|
||||
if let componentView = componentView as? CVComponentViewCollapseSet {
|
||||
let wasExpanded = componentView.isShowingExpanded
|
||||
let willBeExpanded = !wasExpanded
|
||||
let expandedRotation: CGFloat = -.pi
|
||||
let isRTL = componentView.chevronLabel.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
||||
|
||||
let fromAngle: CGFloat
|
||||
let toAngle: CGFloat
|
||||
if willBeExpanded {
|
||||
fromAngle = 0
|
||||
toAngle = isRTL ? CGFloat.pi : -CGFloat.pi
|
||||
} else {
|
||||
fromAngle = expandedRotation
|
||||
toAngle = isRTL ? -2 * CGFloat.pi : 0
|
||||
}
|
||||
|
||||
componentView.isShowingExpanded = willBeExpanded
|
||||
componentView.chevronLabel.transform = willBeExpanded
|
||||
? CGAffineTransform(rotationAngle: expandedRotation)
|
||||
: .identity
|
||||
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: willBeExpanded)
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
animation.fromValue = fromAngle
|
||||
animation.toValue = toAngle
|
||||
animation.duration = 0.2
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
componentView.chevronLabel.layer.add(animation, forKey: "chevronRotation")
|
||||
}
|
||||
componentDelegate.didTapCollapseSet(collapseSetId: interaction.uniqueId)
|
||||
return true
|
||||
}
|
||||
@ -154,6 +195,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
// MARK: - Measurement
|
||||
|
||||
fileprivate static let measurementKey_outerStack = "CVComponentCollapseSet.outerStack"
|
||||
fileprivate static let measurementKey_innerStack = "CVComponentCollapseSet.innerStack"
|
||||
|
||||
func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
|
||||
owsAssertDebug(maxWidth > 0)
|
||||
@ -161,18 +203,38 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
0,
|
||||
maxWidth - outerStackConfig.layoutMargins.totalWidth,
|
||||
)
|
||||
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: availableWidth)
|
||||
let chevronSize = CVText.measureLabel(config: chevronConfig, maxWidth: availableWidth)
|
||||
let labelMaxWidth = max(0, availableWidth - chevronSize.width - innerStackConfig.spacing)
|
||||
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: labelMaxWidth)
|
||||
let innerMeasurement = ManualStackView.measure(
|
||||
config: innerStackConfig,
|
||||
measurementBuilder: measurementBuilder,
|
||||
measurementKey: Self.measurementKey_innerStack,
|
||||
subviewInfos: [
|
||||
labelSize.asManualSubviewInfo(hasFixedWidth: true),
|
||||
chevronSize.asManualSubviewInfo(hasFixedSize: true),
|
||||
],
|
||||
)
|
||||
let outerMeasurement = ManualStackView.measure(
|
||||
config: outerStackConfig,
|
||||
measurementBuilder: measurementBuilder,
|
||||
measurementKey: Self.measurementKey_outerStack,
|
||||
subviewInfos: [labelSize.asManualSubviewInfo(hasFixedWidth: true)],
|
||||
subviewInfos: [innerMeasurement.measuredSize.asManualSubviewInfo(hasFixedWidth: true)],
|
||||
)
|
||||
return outerMeasurement.measuredSize
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private var innerStackConfig: CVStackViewConfig {
|
||||
CVStackViewConfig(
|
||||
axis: .horizontal,
|
||||
alignment: .center,
|
||||
spacing: 4,
|
||||
layoutMargins: .zero,
|
||||
)
|
||||
}
|
||||
|
||||
private var outerStackConfig: CVStackViewConfig {
|
||||
CVStackViewConfig(
|
||||
axis: .vertical,
|
||||
@ -233,7 +295,6 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
)
|
||||
|
||||
let nbsp = SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue
|
||||
let chevron: SignalSymbol = collapseSet.isExpanded ? .chevronUp : .chevronDown
|
||||
|
||||
let result = NSMutableAttributedString()
|
||||
result.append(leadingIcon.attributedString(
|
||||
@ -242,18 +303,33 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
attributes: [.foregroundColor: UIColor.Signal.label],
|
||||
))
|
||||
result.append(NSAttributedString(
|
||||
string: "\(nbsp)\(labelText)\(nbsp)",
|
||||
string: "\(nbsp)\(labelText)",
|
||||
attributes: [
|
||||
.font: labelFont,
|
||||
.foregroundColor: UIColor.Signal.label,
|
||||
],
|
||||
))
|
||||
result.append(chevron.attributedString(
|
||||
return result
|
||||
}
|
||||
|
||||
private var chevronConfig: CVLabelConfig {
|
||||
CVLabelConfig(
|
||||
text: .attributedText(chevronAttributedString),
|
||||
displayConfig: .forUnstyledText(font: labelFont, textColor: .Signal.label),
|
||||
font: labelFont,
|
||||
textColor: .Signal.label,
|
||||
numberOfLines: 1,
|
||||
lineBreakMode: .byClipping,
|
||||
textAlignment: .center,
|
||||
)
|
||||
}
|
||||
|
||||
private var chevronAttributedString: NSAttributedString {
|
||||
SignalSymbol.chevronDown.attributedString(
|
||||
for: .footnote,
|
||||
clamped: false,
|
||||
attributes: [.foregroundColor: UIColor.Signal.label],
|
||||
))
|
||||
return result
|
||||
)
|
||||
}
|
||||
|
||||
private var titleString: String {
|
||||
@ -264,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,
|
||||
@ -322,9 +410,14 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
class CVComponentViewCollapseSet: NSObject, CVComponentView {
|
||||
|
||||
fileprivate let outerStack = ManualStackView(name: "collapseSet.outerStack")
|
||||
fileprivate let innerStack = ManualStackView(name: "collapseSet.innerStack")
|
||||
fileprivate let label = CVLabel()
|
||||
fileprivate let chevronContainer = UIView()
|
||||
fileprivate let chevronLabel = CVLabel()
|
||||
fileprivate let solidBackgroundView = UIView()
|
||||
|
||||
fileprivate var isShowingExpanded = false
|
||||
|
||||
fileprivate var wallpaperBlurView: CVWallpaperBlurView?
|
||||
fileprivate func ensureWallpaperBlurView() -> CVWallpaperBlurView {
|
||||
if let wallpaperBlurView = self.wallpaperBlurView {
|
||||
@ -341,14 +434,24 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
|
||||
|
||||
var rootView: UIView { outerStack }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
chevronContainer.addSubview(chevronLabel)
|
||||
}
|
||||
|
||||
func setIsCellVisible(_ isCellVisible: Bool) {}
|
||||
|
||||
func reset() {
|
||||
label.reset()
|
||||
chevronLabel.reset()
|
||||
chevronLabel.transform = .identity
|
||||
chevronLabel.layer.removeAnimation(forKey: "chevronRotation")
|
||||
isShowingExpanded = false
|
||||
solidBackgroundView.backgroundColor = nil
|
||||
wallpaperBlurView?.removeFromSuperview()
|
||||
wallpaperBlurView = nil
|
||||
hasWallpaper = false
|
||||
innerStack.reset()
|
||||
outerStack.reset()
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,6 +150,7 @@ private class CVQuotedMessageViewAdapter: CVQuotedMessageViewDelegate {
|
||||
DependenciesBridge.shared.attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
|
||||
message,
|
||||
priority: .userInitiated,
|
||||
useThumbnails: false,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
|
||||
@ -493,6 +493,9 @@ public struct CVComponentState: Equatable {
|
||||
let detailsText: NSAttributedString?
|
||||
/// For mutual groups, lack thereof and note-to-self description.
|
||||
let mutualGroupsText: NSAttributedString?
|
||||
/// Populated if `mutualGroupsText` is not suitable for a11y, for
|
||||
/// example if it embeds an image.
|
||||
let mutualGroupsAccessibilityText: String?
|
||||
let threadType: SafetyTipsType
|
||||
let shouldShowSafetyTipsButton: Bool
|
||||
let isOfficialChat: Bool
|
||||
@ -525,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
|
||||
}
|
||||
}
|
||||
@ -1194,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()
|
||||
@ -1361,7 +1363,33 @@ private extension CVComponentState.Builder {
|
||||
case .failed:
|
||||
mediaAlbumHasFailedAttachment = true
|
||||
case .none:
|
||||
mediaAlbumHasSkippedAttachment = true
|
||||
// If optimize local storage is enabled, and the user has auto-downloads
|
||||
// disabled, show the 'skipped attachment' download indicator. Otherwise
|
||||
// render the attachment as normal, using the backup thumbnail for display.
|
||||
let backupPlan = DependenciesBridge.shared.backupPlanManager.backupPlan(tx: transaction)
|
||||
switch backupPlan {
|
||||
case
|
||||
.paid(let optimizeLocalStorage),
|
||||
.paidAsTester(let optimizeLocalStorage),
|
||||
.paidExpiringSoon(let optimizeLocalStorage):
|
||||
if
|
||||
optimizeLocalStorage,
|
||||
canAutoDownloadAttachment(referencedAttachment: attachment),
|
||||
attachment.attachment.localRelativeFilePathThumbnail != nil
|
||||
{
|
||||
// If optimize storage is enabled, auto-downloads are enabled,
|
||||
// and the backup thumbnail is present, show the backup thumbnail
|
||||
// as a true attachment (don't show the download icon overlay).
|
||||
mediaAlbumHasSkippedAttachment = false
|
||||
} else {
|
||||
mediaAlbumHasSkippedAttachment = true
|
||||
}
|
||||
case
|
||||
.free,
|
||||
.disabled,
|
||||
.disabling:
|
||||
mediaAlbumHasSkippedAttachment = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1397,6 +1425,28 @@ private extension CVComponentState.Builder {
|
||||
return result
|
||||
}
|
||||
|
||||
private func canAutoDownloadAttachment(referencedAttachment: ReferencedAttachment) -> Bool {
|
||||
let mediaBandwidthPreferenceStore = DependenciesBridge.shared.mediaBandwidthPreferenceStore
|
||||
let autoDownloadableMediaTypes = mediaBandwidthPreferenceStore.autoDownloadableMediaTypes(tx: transaction)
|
||||
let mimeType = referencedAttachment.attachment.mimeType
|
||||
if MimeTypeUtil.isSupportedImageMimeType(mimeType) {
|
||||
return autoDownloadableMediaTypes.contains(.photo)
|
||||
}
|
||||
if MimeTypeUtil.isSupportedVideoMimeType(mimeType) {
|
||||
return autoDownloadableMediaTypes.contains(.video)
|
||||
}
|
||||
if MimeTypeUtil.isSupportedAudioMimeType(mimeType) {
|
||||
if
|
||||
autoDownloadableMediaTypes.contains(.audio),
|
||||
referencedAttachment.reference.renderingFlag != .voiceMessage
|
||||
{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return autoDownloadableMediaTypes.contains(.document)
|
||||
}
|
||||
|
||||
mutating func buildThreadDetails() -> ThreadDetails {
|
||||
owsAssertDebug(interaction is ThreadDetailsInteraction)
|
||||
|
||||
@ -1631,7 +1681,6 @@ private extension CVComponentState.Builder {
|
||||
} else if let quotedMessage = message.quotedMessage {
|
||||
var memberLabel: String?
|
||||
if
|
||||
BuildFlags.MemberLabel.display,
|
||||
let groupThread = thread as? TSGroupThread,
|
||||
!threadViewModel.hasPendingMessageRequest,
|
||||
let originalMessageAuthor = quotedMessage.authorAddress.aci
|
||||
@ -1714,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))
|
||||
|
||||
@ -249,23 +255,30 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
innerViews.append(UIView.spacer(withHeight: vSpacingSafetySectionDefault))
|
||||
let mutualGroupsLabelConfig = mutualGroupsLabelConfig(attributedText: mutualGroupsText)
|
||||
mutualGroupsLabelConfig.applyForRendering(label: mutualGroupsLabel)
|
||||
mutualGroupsLabel.accessibilityLabel = safetySection.mutualGroupsAccessibilityText
|
||||
innerViews.append(mutualGroupsLabel)
|
||||
}
|
||||
|
||||
if safetySection.shouldShowSafetyTipsButton {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -448,7 +461,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
)
|
||||
}
|
||||
|
||||
private var safetyTipsButtonLabelConfig: CVLabelConfig {
|
||||
private func safetyTipsButtonLabelConfig() -> CVLabelConfig {
|
||||
CVLabelConfig.unstyledText(
|
||||
OWSLocalizedString(
|
||||
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
|
||||
@ -642,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
|
||||
|
||||
@ -688,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -737,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)
|
||||
@ -786,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
|
||||
@ -803,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
|
||||
@ -838,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,
|
||||
@ -906,6 +919,7 @@ extension CVComponentThreadDetails {
|
||||
shouldShowProfileNamesEducation: false,
|
||||
detailsText: NSAttributedString(string: OWSLocalizedString("RELEASE_NOTES_DETAILS", comment: "Details text for the thread details view of the release notes channel")),
|
||||
mutualGroupsText: nil,
|
||||
mutualGroupsAccessibilityText: nil,
|
||||
threadType: .contact,
|
||||
shouldShowSafetyTipsButton: false,
|
||||
isOfficialChat: true,
|
||||
@ -1038,6 +1052,7 @@ extension CVComponentThreadDetails {
|
||||
shouldShowProfileNamesEducation: shouldShowUnknownThreadWarning,
|
||||
detailsText: membersAttributedText,
|
||||
mutualGroupsText: nil,
|
||||
mutualGroupsAccessibilityText: nil,
|
||||
threadType: .group,
|
||||
shouldShowSafetyTipsButton: shouldShowUnknownThreadWarning && groupThread.hasPendingMessageRequest(transaction: tx),
|
||||
isOfficialChat: false,
|
||||
@ -1070,6 +1085,7 @@ extension CVComponentThreadDetails {
|
||||
with: .font(.dynamicTypeSubheadline),
|
||||
.color(UIColor.Signal.label),
|
||||
),
|
||||
mutualGroupsAccessibilityText: nil,
|
||||
threadType: .contact,
|
||||
shouldShowSafetyTipsButton: false,
|
||||
isOfficialChat: false,
|
||||
@ -1174,6 +1190,7 @@ extension CVComponentThreadDetails {
|
||||
" ",
|
||||
formattedString,
|
||||
]),
|
||||
mutualGroupsAccessibilityText: formattedString,
|
||||
threadType: .contact,
|
||||
shouldShowSafetyTipsButton: isMessageRequest,
|
||||
isOfficialChat: false,
|
||||
|
||||
@ -59,7 +59,7 @@ extension TSInfoMessage.PersistableGroupUpdateItem {
|
||||
)
|
||||
{
|
||||
owsAssertDebug(
|
||||
isTail.negated,
|
||||
!isTail,
|
||||
"Collapsed item with a following request shouldn't be a tail!",
|
||||
)
|
||||
return nextItemAction
|
||||
|
||||
@ -179,6 +179,23 @@ class ConversationHeaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Spinning Title
|
||||
|
||||
func updateTitleSpinning() {
|
||||
let key = "spin"
|
||||
if InMemorySettings.spinningConversationTitle {
|
||||
guard layer.animation(forKey: key) == nil else { return }
|
||||
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
animation.toValue = NSNumber(value: Double.pi * 2)
|
||||
animation.duration = 1
|
||||
animation.isCumulative = true
|
||||
animation.repeatCount = .greatestFiniteMagnitude
|
||||
layer.add(animation, forKey: key)
|
||||
} else {
|
||||
layer.removeAnimation(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate Methods
|
||||
|
||||
@objc
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -249,6 +249,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
button.configuration?.baseForegroundColor = Style.buttonTintColor
|
||||
button.accessibilityLabel = accessibilityLabel
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.addConstraints([
|
||||
button.widthAnchor.constraint(equalToConstant: LayoutMetrics.initialTextBoxHeight),
|
||||
@ -345,6 +346,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
button.configuration?.cornerStyle = .capsule
|
||||
button.accessibilityLabel = MessageStrings.sendButton
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.addConstraints([
|
||||
button.widthAnchor.constraint(equalToConstant: buttonSize),
|
||||
@ -378,6 +380,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
comment: "Accessibility hint describing what you can do with the attachment button",
|
||||
)
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.addConstraints([
|
||||
button.widthAnchor.constraint(equalToConstant: buttonSize),
|
||||
@ -403,6 +406,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
button.configuration?.baseForegroundColor = .white
|
||||
button.configuration?.cornerStyle = .capsule
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.addConstraints([
|
||||
button.widthAnchor.constraint(equalToConstant: buttonSize),
|
||||
@ -1215,7 +1219,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
lazy var sendButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.accessibilityLabel = MessageStrings.sendButton
|
||||
button.ows_adjustsImageWhenDisabled = true
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "sendButton")
|
||||
button.setImage(UIImage(imageLiteralResourceName: "send-blue-28"), for: .normal)
|
||||
button.bounds.size = CGSize(width: 48, height: LayoutMetrics.initialToolbarHeight)
|
||||
@ -1225,6 +1229,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
lazy var cameraButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.tintColor = Style.buttonTintColor
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.accessibilityLabel = OWSLocalizedString(
|
||||
"CAMERA_BUTTON_LABEL",
|
||||
comment: "Accessibility label for camera button.",
|
||||
@ -1242,6 +1247,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
lazy var voiceMemoButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.tintColor = Style.buttonTintColor
|
||||
button.isPointerInteractionEnabled = true
|
||||
button.accessibilityLabel = OWSLocalizedString(
|
||||
"INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_LABEL",
|
||||
comment: "accessibility label for the button which records voice memos",
|
||||
@ -1367,6 +1373,8 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
|
||||
override private init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
isPointerInteractionEnabled = true
|
||||
|
||||
addSubview(roundedCornersBackground)
|
||||
roundedCornersBackground.autoCenterInSuperview()
|
||||
roundedCornersBackground.autoSetDimensions(to: CGSize(square: 28))
|
||||
|
||||
@ -545,6 +545,14 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if
|
||||
scrollAction.action == .none,
|
||||
update.loadRequest.preferredScrollContinuityAnchorInteractionId != nil,
|
||||
isScrolledToBottom
|
||||
{
|
||||
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
|
||||
}
|
||||
|
||||
if .loadOlder == renderState.loadType {
|
||||
scrollAction = .none
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ extension ConversationViewController {
|
||||
} else {
|
||||
headerView.titleLabel.text = title
|
||||
}
|
||||
|
||||
headerView.updateTitleSpinning()
|
||||
}
|
||||
|
||||
public func createHeaderViews() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -423,7 +423,6 @@ struct CVItemModelBuilder: CVItemBuilding {
|
||||
|
||||
var memberLabel: String?
|
||||
if
|
||||
BuildFlags.MemberLabel.display,
|
||||
let groupThread = thread as? TSGroupThread,
|
||||
!threadViewModel.hasPendingMessageRequest,
|
||||
let senderAci = incomingSenderAddress.aci
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,11 +47,12 @@ enum ContactSupportActionSheet {
|
||||
let submitWithLogAction = ActionSheetAction(title: submitWithLogTitle, style: .default) { [weak fromViewController] _ in
|
||||
guard let fromViewController else { return }
|
||||
|
||||
let logs = DebugLogs(dumper: logDumper)
|
||||
let emailRequest = SupportEmailModel(
|
||||
userDescription: nil,
|
||||
emojiMood: nil,
|
||||
supportFilter: emailFilter.asString,
|
||||
debugLogPolicy: .requireUpload(logDumper),
|
||||
debugLogPolicy: .requireUpload(logs),
|
||||
hasRecentChallenge: logDumper.challengeReceivedRecently(),
|
||||
)
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import SignalServiceKit
|
||||
import SignalUI
|
||||
import zlib
|
||||
|
||||
public struct DebugLogDumper {
|
||||
struct DebugLogDumper {
|
||||
fileprivate var accountManager: (any TSAccountManager)?
|
||||
fileprivate var appVersion: any AppVersion
|
||||
fileprivate var db: (any DB)?
|
||||
@ -25,7 +25,7 @@ public struct DebugLogDumper {
|
||||
)
|
||||
}
|
||||
|
||||
public func challengeReceivedRecently() -> Bool {
|
||||
func challengeReceivedRecently() -> Bool {
|
||||
guard let db else {
|
||||
return false
|
||||
}
|
||||
@ -57,34 +57,134 @@ public struct DebugLogDumper {
|
||||
}
|
||||
}
|
||||
|
||||
enum DebugLogs {
|
||||
final class DebugLogs {
|
||||
private let dumper: DebugLogDumper
|
||||
private var logsDirPath: String?
|
||||
|
||||
init(dumper: DebugLogDumper) {
|
||||
self.dumper = dumper
|
||||
self.logsDirPath = DebugLogs.collectAndFlushLogs(dumper: dumper)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let logsDirPath {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
func showPreview(
|
||||
from viewController: UIViewController,
|
||||
onSubmit: (() -> Void)? = nil,
|
||||
onCancel: (() -> Void)? = nil,
|
||||
) {
|
||||
guard let logsDirPath else {
|
||||
Logger.error("No logs path found for preview")
|
||||
handleError(error: .noLogs, viewController: viewController)
|
||||
onCancel?()
|
||||
return
|
||||
}
|
||||
let logFilePaths = ((try? FileManager.default.contentsOfDirectory(atPath: logsDirPath)) ?? []).map {
|
||||
URL(fileURLWithPath: logsDirPath).appendingPathComponent($0).path
|
||||
}
|
||||
let previewVC = DebugLogPreviewViewController(logFilePaths: logFilePaths, onSubmit: onSubmit, onCancel: onCancel)
|
||||
let nav = OWSNavigationController(rootViewController: previewVC)
|
||||
viewController.present(nav, animated: true)
|
||||
}
|
||||
|
||||
/// Presents a log preview with an option to submit. Completion is only
|
||||
/// called if the user submits, after the submission is completed.
|
||||
@MainActor
|
||||
static func submitLogs(supportTag: String? = nil, dumper: DebugLogDumper, completion: (() -> Void)? = nil) {
|
||||
let submitLogsCompletion = {
|
||||
if let completion {
|
||||
// Wait a moment. If the user opens a URL, it needs a moment to complete.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
func promptToSubmitLogs(
|
||||
from viewController: UIViewController,
|
||||
supportTag: String? = nil,
|
||||
completion: (() -> Void)? = nil,
|
||||
) {
|
||||
showPreview(from: viewController, onSubmit: {
|
||||
Task {
|
||||
await viewController.awaitableDismiss(animated: true)
|
||||
await self.submitLogs(supportTag: supportTag)
|
||||
if let completion {
|
||||
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func promptToSubmitLogs(
|
||||
from viewController: UIViewController,
|
||||
supportTag: String? = nil,
|
||||
) async {
|
||||
let didSubmit = await withCheckedContinuation { continuation in
|
||||
showPreview(
|
||||
from: viewController,
|
||||
onSubmit: {
|
||||
continuation.resume(returning: true)
|
||||
},
|
||||
onCancel: {
|
||||
continuation.resume(returning: false)
|
||||
},
|
||||
)
|
||||
}
|
||||
if didSubmit {
|
||||
await viewController.awaitableDismiss(animated: true)
|
||||
await submitLogs(supportTag: supportTag)
|
||||
}
|
||||
}
|
||||
|
||||
enum DebugLogsError: LocalizedError {
|
||||
case noLogs
|
||||
case couldNotPackageLogs
|
||||
case uploadError(zipFilePath: String)
|
||||
|
||||
var errorDescription: String? { localizedErrorMessage }
|
||||
var localizedErrorMessage: String {
|
||||
switch self {
|
||||
case .noLogs:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_NO_LOGS",
|
||||
comment: "Error indicating that no debug logs could be found.",
|
||||
)
|
||||
case .couldNotPackageLogs:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
|
||||
comment: "Error indicating that the debug logs could not be packaged.",
|
||||
)
|
||||
case .uploadError:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
|
||||
comment: "Error indicating that a debug log could not be uploaded.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func submitLogs(supportTag: String?) async {
|
||||
var supportFilter = "Signal - iOS Debug Log"
|
||||
if let supportTag {
|
||||
supportFilter += " - \(supportTag)"
|
||||
}
|
||||
|
||||
guard let frontmostViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
submitLogsCompletion()
|
||||
return
|
||||
}
|
||||
uploadLogsUsingViewController(frontmostViewController, dumper: dumper) { url in
|
||||
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
submitLogsCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let url: URL?
|
||||
do {
|
||||
url = try await uploadLogsWithUI(from: frontmostViewController)
|
||||
} catch {
|
||||
self.handleError(error: error, viewController: frontmostViewController)
|
||||
return
|
||||
}
|
||||
guard let url else { return }
|
||||
|
||||
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
return
|
||||
}
|
||||
|
||||
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
||||
let alert = ActionSheetController(
|
||||
title: NSLocalizedString("DEBUG_LOG_ALERT_TITLE", comment: "Title of the debug log alert."),
|
||||
message: NSLocalizedString("DEBUG_LOG_ALERT_MESSAGE", comment: "Message of the debug log alert."),
|
||||
@ -102,10 +202,10 @@ enum DebugLogs {
|
||||
await ComposeSupportEmailOperation.sendEmailWithDefaultErrorHandling(
|
||||
supportFilter: supportFilter,
|
||||
logUrl: url,
|
||||
hasRecentChallenge: dumper.challengeReceivedRecently(),
|
||||
hasRecentChallenge: self.dumper.challengeReceivedRecently(),
|
||||
)
|
||||
}
|
||||
submitLogsCompletion()
|
||||
continuation.resume()
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -118,7 +218,7 @@ enum DebugLogs {
|
||||
handler: { _ in
|
||||
UIPasteboard.general.string = url.absoluteString
|
||||
presentingViewController.presentToast(text: CommonStrings.copiedToClipboardToast, image: .copy)
|
||||
submitLogsCompletion()
|
||||
continuation.resume()
|
||||
},
|
||||
))
|
||||
alert.addAction(ActionSheetAction(
|
||||
@ -131,67 +231,39 @@ enum DebugLogs {
|
||||
AttachmentSharing.showShareUI(
|
||||
for: url.absoluteString,
|
||||
sender: nil,
|
||||
completion: submitLogsCompletion,
|
||||
completion: { continuation.resume() },
|
||||
)
|
||||
},
|
||||
))
|
||||
alert.addAction(ActionSheetAction(
|
||||
title: CommonStrings.cancelButton,
|
||||
style: .cancel,
|
||||
handler: { _ in submitLogsCompletion() },
|
||||
handler: { _ in continuation.resume() },
|
||||
))
|
||||
presentingViewController.presentActionSheet(alert)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func uploadLogsUsingViewController(_ viewController: UIViewController, dumper: DebugLogDumper, completion: @escaping (URL) -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: viewController,
|
||||
private func uploadLogsWithUI(from viewController: UIViewController) async throws(DebugLogsError) -> URL? {
|
||||
return try await ModalActivityIndicatorViewController.presentAndPropagateResult(
|
||||
from: viewController,
|
||||
canCancel: true,
|
||||
asyncBlock: { await _uploadLogs(dumper: dumper, modalActivityIndicator: $0, completion: completion) },
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func _uploadLogs(dumper: DebugLogDumper, modalActivityIndicator: ModalActivityIndicatorViewController, completion: @escaping (URL) -> Void) async {
|
||||
do {
|
||||
let url = try await uploadLogs(dumper: dumper)
|
||||
guard !modalActivityIndicator.wasCancelled else { return }
|
||||
modalActivityIndicator.dismiss {
|
||||
completion(url)
|
||||
}
|
||||
} catch {
|
||||
guard !modalActivityIndicator.wasCancelled else {
|
||||
if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath {
|
||||
OWSFileSystem.deleteFile(logArchiveOrDirectoryPath)
|
||||
) { () throws(DebugLogsError) -> URL? in
|
||||
do throws(DebugLogsError) {
|
||||
return try await self.uploadLogs()
|
||||
} catch {
|
||||
if Task.isCancelled {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
modalActivityIndicator.dismiss {
|
||||
DebugLogs.showFailureAlert(
|
||||
with: error.localizedErrorMessage,
|
||||
logArchiveOrDirectoryPath: error.logArchiveOrDirectoryPath,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Collecting & uploading
|
||||
|
||||
private struct NoLogsError: Error {
|
||||
var errorString: String {
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_NO_LOGS",
|
||||
comment: "Error indicating that no debug logs could be found.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func collectLogs() -> Result<String, NoLogsError> {
|
||||
private static func collectLogs() -> String? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy.MM.dd hh.mm.ss"
|
||||
let dateString = dateFormatter.string(from: Date())
|
||||
@ -203,7 +275,7 @@ enum DebugLogs {
|
||||
|
||||
let logFilePaths = DebugLogger.shared.allLogFilePaths
|
||||
if logFilePaths.isEmpty {
|
||||
return .failure(NoLogsError())
|
||||
return nil
|
||||
}
|
||||
|
||||
for logFilePath in logFilePaths {
|
||||
@ -219,50 +291,44 @@ enum DebugLogs {
|
||||
OWSFileSystem.protectFileOrFolder(atPath: copyFilePath)
|
||||
}
|
||||
|
||||
return .success(zipDirPath)
|
||||
return zipDirPath
|
||||
}
|
||||
|
||||
static func exportLogs() {
|
||||
func exportLogs(viewController: UIViewController) {
|
||||
AssertIsOnMainThread()
|
||||
switch collectLogs() {
|
||||
case let .success(logsDirPath):
|
||||
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
case let .failure(error):
|
||||
Self.showFailureAlert(with: error.errorString, logArchiveOrDirectoryPath: nil)
|
||||
return
|
||||
guard let logsDirPath else {
|
||||
return handleError(
|
||||
error: .noLogs,
|
||||
viewController: viewController,
|
||||
)
|
||||
}
|
||||
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
struct UploadDebugLogError: Error {
|
||||
var localizedErrorMessage: String
|
||||
var logArchiveOrDirectoryPath: String?
|
||||
}
|
||||
|
||||
/// - Note: Various dependencies might not be initialized yet when this
|
||||
/// method is called from the database recovery flow. Notably, the database
|
||||
/// isn't available in that flow.
|
||||
static func uploadLogs(dumper: DebugLogDumper) async throws(UploadDebugLogError) -> URL {
|
||||
// Phase 1: Dump any additional details that are relevant.
|
||||
private static func collectAndFlushLogs(
|
||||
dumper: DebugLogDumper,
|
||||
) -> String? {
|
||||
// Dump any additional details that are relevant.
|
||||
dumper.dump()
|
||||
Logger.info("About to zip debug logs")
|
||||
|
||||
// Phase 2: Flush pending logs to disk.
|
||||
// Flush pending logs to disk.
|
||||
Logger.flush()
|
||||
|
||||
// Phase 3: Make a local copy of all of the log files.
|
||||
let zipDirPath: String
|
||||
switch collectLogs() {
|
||||
case let .success(logsDirPath):
|
||||
zipDirPath = logsDirPath
|
||||
case let .failure(error):
|
||||
throw UploadDebugLogError(localizedErrorMessage: error.errorString)
|
||||
// Make a local copy of all of the log files.
|
||||
return collectLogs()
|
||||
}
|
||||
|
||||
func uploadLogs() async throws(DebugLogsError) -> URL {
|
||||
guard let logsDirPath else {
|
||||
throw DebugLogsError.noLogs
|
||||
}
|
||||
|
||||
// Phase 4: Zip up the log files.
|
||||
let zipDirUrl = URL(fileURLWithPath: zipDirPath)
|
||||
let zipFileUrl = URL(fileURLWithPath: (zipDirPath as NSString).appendingPathExtension("zip")!)
|
||||
// Zip up the log files.
|
||||
let zipDirUrl = URL(fileURLWithPath: logsDirPath)
|
||||
let zipFileUrl = URL(fileURLWithPath: (logsDirPath as NSString).appendingPathExtension("zip")!)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
var zipError: NSError?
|
||||
fileCoordinator.coordinate(readingItemAt: zipDirUrl, options: [.forUploading], error: &zipError) { temporaryFileUrl in
|
||||
@ -273,38 +339,44 @@ enum DebugLogs {
|
||||
}
|
||||
}
|
||||
if zipError != nil || !OWSFileSystem.fileOrFolderExists(url: zipFileUrl) {
|
||||
let errorMessage = OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
|
||||
comment: "Error indicating that the debug logs could not be packaged.",
|
||||
)
|
||||
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipDirPath)
|
||||
throw DebugLogsError.couldNotPackageLogs
|
||||
}
|
||||
|
||||
OWSFileSystem.protectFileOrFolder(atPath: zipFileUrl.path)
|
||||
OWSFileSystem.deleteFile(zipDirPath)
|
||||
|
||||
// Phase 5: Upload the log files.
|
||||
// Upload the log files.
|
||||
do {
|
||||
let url = try await DebugLogUploader.uploadFile(fileUrl: zipFileUrl, mimeType: MimeType.applicationZip.rawValue)
|
||||
try OWSFileSystem.deleteFile(url: zipFileUrl)
|
||||
return url
|
||||
} catch {
|
||||
let errorMessage = OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
|
||||
comment: "Error indicating that a debug log could not be uploaded.",
|
||||
)
|
||||
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipFileUrl.path)
|
||||
throw DebugLogsError.uploadError(zipFilePath: zipFileUrl.path)
|
||||
}
|
||||
}
|
||||
|
||||
private static func showFailureAlert(with message: String, logArchiveOrDirectoryPath: String?) {
|
||||
let deleteArchive: (String) -> Void = { filePath in
|
||||
OWSFileSystem.deleteFile(filePath)
|
||||
private func handleError(
|
||||
error: DebugLogsError,
|
||||
viewController: UIViewController,
|
||||
) {
|
||||
let logsPath: String?
|
||||
let completion: (() -> Void)?
|
||||
switch error {
|
||||
case .noLogs:
|
||||
logsPath = nil
|
||||
completion = nil
|
||||
case .couldNotPackageLogs:
|
||||
logsPath = self.logsDirPath
|
||||
completion = nil
|
||||
case .uploadError(let zipFilePath):
|
||||
logsPath = zipFilePath
|
||||
completion = {
|
||||
OWSFileSystem.deleteFile(zipFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
let alert = ActionSheetController(title: nil, message: message)
|
||||
let alert = ActionSheetController(message: error.localizedErrorMessage)
|
||||
|
||||
if let logArchiveOrDirectoryPath {
|
||||
if let logsPath {
|
||||
alert.addAction(.init(
|
||||
title: OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_OPTION_EXPORT_LOG_ARCHIVE",
|
||||
@ -312,23 +384,18 @@ enum DebugLogs {
|
||||
),
|
||||
) { _ in
|
||||
AttachmentSharing.showShareUI(
|
||||
for: URL(fileURLWithPath: logArchiveOrDirectoryPath),
|
||||
for: URL(fileURLWithPath: logsPath),
|
||||
sender: nil,
|
||||
completion: {
|
||||
deleteArchive(logArchiveOrDirectoryPath)
|
||||
},
|
||||
completion: completion,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
alert.addAction(.init(title: CommonStrings.okButton) { _ in
|
||||
if let logArchiveOrDirectoryPath {
|
||||
deleteArchive(logArchiveOrDirectoryPath)
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
|
||||
let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts
|
||||
presentingViewController?.presentActionSheet(alert)
|
||||
viewController.presentActionSheet(alert)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
52
Signal/Megaphones/RemoteAnnouncementFetcher.swift
Normal file
52
Signal/Megaphones/RemoteAnnouncementFetcher.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
public import SignalServiceKit
|
||||
|
||||
/// Handles fetching and parsing remote announcements.
|
||||
public class RemoteAnnouncementFetcher: RemoteReleaseNotesFetcher<RemoteAnnouncementModel.Manifest, RemoteAnnouncementModel.Translation> {
|
||||
override func updatePersistedData(
|
||||
withFetchedData fetchedTranslations: [(RemoteAnnouncementModel.Manifest, RemoteAnnouncementModel.Translation)],
|
||||
transaction: DBWriteTransaction,
|
||||
) {
|
||||
// TODO: [KC] implement!
|
||||
}
|
||||
|
||||
override func fetchTranslationAndImage(
|
||||
forManifest manifest: RemoteAnnouncementModel.Manifest,
|
||||
withLocaleString localeString: String,
|
||||
) async throws -> RemoteAnnouncementModel.Translation {
|
||||
return try await Retry.performWithBackoff(
|
||||
maxAttempts: 3,
|
||||
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
||||
block: {
|
||||
guard
|
||||
let translationUrlPath: String = .translationUrlPath(
|
||||
forManifestId: manifest.id,
|
||||
withLocaleString: localeString,
|
||||
)
|
||||
else {
|
||||
throw OWSAssertionError("Failed to create translation URL path for manifest \(manifest.id)")
|
||||
}
|
||||
let translationParser = try await remoteReleaseNotesService.fetchTranslationParser(translationUrlPath: translationUrlPath)
|
||||
let translation = try RemoteAnnouncementModel.Translation.parseFrom(parser: translationParser)
|
||||
|
||||
// TODO: [KC] May want to store whether we've downloaded media
|
||||
let _ = try await self.downloadMediaIfNecessary(
|
||||
mediaRemoteUrlPath: translation.mediaRemoteUrlPath,
|
||||
mediaFileDirectory: RemoteAnnouncementModel.mediaDirectory,
|
||||
translationId: translation.id,
|
||||
)
|
||||
if manifest.id != translation.id {
|
||||
// We shouldn't fail here, but this scenario is
|
||||
// unexpected so let's keep an eye out for it.
|
||||
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
|
||||
}
|
||||
return translation
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -4,63 +4,40 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalServiceKit
|
||||
public import SignalServiceKit
|
||||
|
||||
/// Handles fetching and parsing remote megaphones.
|
||||
class RemoteMegaphoneFetcher {
|
||||
private let databaseStorage: SDSDatabaseStorage
|
||||
private let signalService: any OWSSignalServiceProtocol
|
||||
public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation> {
|
||||
private let experienceUpgradeStore: ExperienceUpgradeStore
|
||||
|
||||
init(
|
||||
databaseStorage: SDSDatabaseStorage,
|
||||
signalService: any OWSSignalServiceProtocol,
|
||||
override init(
|
||||
db: DB,
|
||||
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
|
||||
) {
|
||||
self.databaseStorage = databaseStorage
|
||||
self.signalService = signalService
|
||||
self.experienceUpgradeStore = ExperienceUpgradeStore()
|
||||
|
||||
super.init(
|
||||
db: db,
|
||||
remoteReleaseNotesService: remoteReleaseNotesService,
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch all remote megaphones currently on the service and persist them
|
||||
/// locally. Removes any locally-persisted remote megaphones that are no
|
||||
/// longer available remotely.
|
||||
func syncRemoteMegaphones() async throws {
|
||||
Logger.info("Beginning remote megaphone fetch.")
|
||||
|
||||
let megaphones: [RemoteMegaphoneModel]
|
||||
do {
|
||||
megaphones = try await fetchRemoteMegaphones()
|
||||
} catch {
|
||||
Logger.warn("\(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
Logger.info("Syncing \(megaphones.count) fetched remote megaphones with local state.")
|
||||
|
||||
await self.databaseStorage.awaitableWrite { transaction in
|
||||
self.updatePersistedMegaphones(
|
||||
withFetchedMegaphones: megaphones,
|
||||
transaction: transaction,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Persisted megaphones
|
||||
|
||||
private extension RemoteMegaphoneFetcher {
|
||||
/// Update our local persisted megaphone state with freshly-fetched
|
||||
/// megaphones from the service. Updates existing megaphones if present,
|
||||
/// and creates new ones if necessary. Removes any locally-persisted
|
||||
/// megaphones that no longer exist on the service.
|
||||
func updatePersistedMegaphones(
|
||||
withFetchedMegaphones serviceMegaphones: [RemoteMegaphoneModel],
|
||||
transaction: DBWriteTransaction,
|
||||
override func updatePersistedData(
|
||||
withFetchedData fetchedTranslations: [(RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation)],
|
||||
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
|
||||
@ -68,107 +45,37 @@ private extension RemoteMegaphoneFetcher {
|
||||
// if anything has changed about the megaphone we have the latest state.
|
||||
// For example, if the user's locale has changed we may have updated
|
||||
// translations.
|
||||
for serviceMegaphone in serviceMegaphones {
|
||||
if let existingLocalMegaphone = localRemoteMegaphones[serviceMegaphone.id] {
|
||||
existingLocalMegaphone.updateManifestRemoteMegaphone(withRefetchedMegaphone: serviceMegaphone)
|
||||
existingLocalMegaphone.anyUpsert(transaction: transaction)
|
||||
|
||||
localRemoteMegaphones.removeValue(forKey: serviceMegaphone.id)
|
||||
for (manifest, translation) in fetchedTranslations {
|
||||
let remoteMegaphoneModel = RemoteMegaphoneModel(manifest: manifest, translation: translation)
|
||||
let experienceUpgrade: ExperienceUpgrade
|
||||
if let persisted = experienceUpgradesByMegaphoneId.removeValue(forKey: manifest.id) {
|
||||
experienceUpgrade = persisted
|
||||
} else {
|
||||
ExperienceUpgrade
|
||||
.makeNew(withManifest: .remoteMegaphone(megaphone: 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetching
|
||||
|
||||
private extension RemoteMegaphoneFetcher {
|
||||
func fetchRemoteMegaphones() async throws -> [RemoteMegaphoneModel] {
|
||||
let manifests = try await fetchManifests()
|
||||
return try await withThrowingTaskGroup(of: RemoteMegaphoneModel.self) { taskGroup in
|
||||
for manifest in manifests {
|
||||
taskGroup.addTask {
|
||||
let translation = try await self.fetchTranslation(forMegaphoneManifest: manifest)
|
||||
if manifest.id != translation.id {
|
||||
// We shouldn't fail here, but this scenario is
|
||||
// unexpected so let's keep an eye out for it.
|
||||
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
|
||||
}
|
||||
|
||||
return RemoteMegaphoneModel(manifest: manifest, translation: translation)
|
||||
}
|
||||
}
|
||||
return try await taskGroup.reduce(into: [], { $0.append($1) })
|
||||
}
|
||||
}
|
||||
|
||||
private func getUrlSession() -> OWSURLSessionProtocol {
|
||||
signalService.urlSessionForUpdates2()
|
||||
}
|
||||
|
||||
/// Fetch the manifests for the currently-active remote megaphones.
|
||||
/// Manifests contain metadata about a megaphone, such as when it should be
|
||||
/// shown and what actions it should expose. They do not contain any
|
||||
/// user-visible content, such as strings.
|
||||
private func fetchManifests() async throws -> [RemoteMegaphoneModel.Manifest] {
|
||||
return try await Retry.performWithBackoff(
|
||||
maxAttempts: 3,
|
||||
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
||||
block: {
|
||||
Logger.info("Fetching remote megaphone manifests")
|
||||
let response = try await getUrlSession().performRequest(
|
||||
.manifestUrlPath,
|
||||
method: .get,
|
||||
)
|
||||
|
||||
guard let parser = response.responseBodyParamParser else {
|
||||
throw OWSAssertionError("Missing or invalid body JSON for manifest!")
|
||||
}
|
||||
|
||||
return try RemoteMegaphoneModel.Manifest.parseFrom(parser: parser)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch user-displayable localized strings for the given manifest. Will
|
||||
/// attempt to fetch a translation matching the user's current locale,
|
||||
/// falling back to English otherwise.
|
||||
private func fetchTranslation(
|
||||
forMegaphoneManifest manifest: RemoteMegaphoneModel.Manifest,
|
||||
) async throws -> RemoteMegaphoneModel.Translation {
|
||||
let localeStrings: [String] = .possibleTranslationLocaleStrings
|
||||
|
||||
for (index, localeString) in localeStrings.enumerated() {
|
||||
do {
|
||||
var translation = try await fetchTranslation(forMegaphoneManifest: manifest, withLocaleString: localeString)
|
||||
translation.setHasImage(try await self.downloadImageIfNecessary(forTranslation: translation))
|
||||
return translation
|
||||
} catch let error as OWSHTTPError where error.responseStatusCode == 404 && (index + 1) != localeStrings.endIndex {
|
||||
// If this isn't the last locale & it's not found, try the next one.
|
||||
continue
|
||||
}
|
||||
// If we hit a non-404 error, propagate it out immediately.
|
||||
}
|
||||
|
||||
// We either return a value or throw an error in the loop as long as there
|
||||
// is at least one locale.
|
||||
throw OWSAssertionError("Unexpectedly found no locale strings!")
|
||||
}
|
||||
|
||||
/// Fetch a translation for the given manifest, using the given locale
|
||||
/// string. Retries automatically on network failure, if possible. May
|
||||
/// fail with a 404, if no translation exists for the given locale string.
|
||||
private func fetchTranslation(
|
||||
forMegaphoneManifest manifest: RemoteMegaphoneModel.Manifest,
|
||||
override func fetchTranslationAndImage(
|
||||
forManifest manifest: RemoteMegaphoneModel.Manifest,
|
||||
withLocaleString localeString: String,
|
||||
) async throws -> RemoteMegaphoneModel.Translation {
|
||||
return try await Retry.performWithBackoff(
|
||||
@ -177,236 +84,26 @@ private extension RemoteMegaphoneFetcher {
|
||||
block: {
|
||||
guard
|
||||
let translationUrlPath: String = .translationUrlPath(
|
||||
forManifest: manifest,
|
||||
forManifestId: manifest.id,
|
||||
withLocaleString: localeString,
|
||||
)
|
||||
else {
|
||||
throw OWSAssertionError("Failed to create translation URL path for manifest \(manifest.id)")
|
||||
}
|
||||
Logger.info("Fetching remote megaphone translation")
|
||||
let response = try await getUrlSession().performRequest(translationUrlPath, method: .get)
|
||||
guard let parser = response.responseBodyParamParser else {
|
||||
throw OWSAssertionError("Missing or invalid body JSON for translation!")
|
||||
}
|
||||
return try RemoteMegaphoneModel.Translation.parseFrom(parser: parser)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Downloads the image if necessary.
|
||||
///
|
||||
/// Doesn't perform any network requests if the image has already been
|
||||
/// downloaded.
|
||||
///
|
||||
/// - Throws: If the image should be downloaded but can't be downloaded.
|
||||
/// - Returns: Whether or not `translation` has an image.
|
||||
private func downloadImageIfNecessary(
|
||||
forTranslation translation: RemoteMegaphoneModel.Translation,
|
||||
) async throws -> Bool {
|
||||
guard let imageRemoteUrlPath = translation.imageRemoteUrlPath else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let imageFileUrl: URL = .imageFilePath(forFetchedTranslation: translation) else {
|
||||
throw OWSAssertionError("Failed to get image file path for translation with ID \(translation.id)")
|
||||
}
|
||||
|
||||
return try await Retry.performWithBackoff(
|
||||
maxAttempts: 3,
|
||||
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
||||
block: {
|
||||
do {
|
||||
if !FileManager.default.fileExists(atPath: imageFileUrl.path) {
|
||||
Logger.info("Fetching remote megaphone image")
|
||||
let response = try await getUrlSession().performDownload(
|
||||
imageRemoteUrlPath,
|
||||
method: .get,
|
||||
)
|
||||
|
||||
do {
|
||||
try FileManager.default.moveItem(
|
||||
at: response.downloadUrl,
|
||||
to: imageFileUrl,
|
||||
)
|
||||
} catch let error {
|
||||
throw OWSAssertionError("Failed to move downloaded image! \(error)")
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch where error.httpStatusCode == 404 {
|
||||
owsFailDebug("Unexpectedly got 404 while fetching remote megaphone image for ID \(translation.id)!")
|
||||
return false
|
||||
} catch let error as OWSHTTPError {
|
||||
owsFailDebug("Unexpectedly got error status code \(error.responseStatusCode) while fetching remote megaphone image for ID \(translation.id)!")
|
||||
throw error
|
||||
let translationParser = try await remoteReleaseNotesService.fetchTranslationParser(translationUrlPath: translationUrlPath)
|
||||
var translation = try RemoteMegaphoneModel.Translation.parseFrom(parser: translationParser)
|
||||
translation.setHasImage(try await self.downloadMediaIfNecessary(
|
||||
mediaRemoteUrlPath: translation.imageRemoteUrlPath,
|
||||
mediaFileDirectory: RemoteMegaphoneModel.imagesDirectory,
|
||||
translationId: translation.id,
|
||||
))
|
||||
if manifest.id != translation.id {
|
||||
// We shouldn't fail here, but this scenario is
|
||||
// unexpected so let's keep an eye out for it.
|
||||
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
|
||||
}
|
||||
return translation
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: URLs
|
||||
|
||||
private extension URL {
|
||||
static func imageFilePath(forFetchedTranslation translation: RemoteMegaphoneModel.Translation) -> URL? {
|
||||
let dirUrl = RemoteMegaphoneModel.imagesDirectory
|
||||
|
||||
guard OWSFileSystem.ensureDirectoryExists(dirUrl.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return dirUrl.appendingPathComponent(translation.imageLocalRelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array<String> {
|
||||
/// A list of possible locale strings for which a translation may be
|
||||
/// available, based on the user's current locale. Includes a fallback to
|
||||
/// English.
|
||||
static var possibleTranslationLocaleStrings: [String] {
|
||||
var locales: [String] = []
|
||||
|
||||
if let langCode = Locale.current.languageCode {
|
||||
locales.append(langCode)
|
||||
|
||||
if let regionCode = Locale.current.regionCode {
|
||||
locales.append("\(langCode)_\(regionCode)")
|
||||
}
|
||||
}
|
||||
|
||||
// Always include English at the end, as a fallback. This translation
|
||||
// should always exist.
|
||||
return locales + ["en"]
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// The path at which remote megaphone manifests are listed.
|
||||
static let manifestUrlPath = "dynamic/release-notes/release-notes-v2.json"
|
||||
|
||||
/// The path at which a translation may be found, for the given manifest
|
||||
/// and locale string.
|
||||
static func translationUrlPath(
|
||||
forManifest manifest: RemoteMegaphoneModel.Manifest,
|
||||
withLocaleString localeString: String,
|
||||
) -> String? {
|
||||
"static/release-notes/\(manifest.id)/\(localeString).json"
|
||||
.percentEncodedAsUrlPath
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parsing manifests
|
||||
|
||||
private extension RemoteMegaphoneModel.Manifest {
|
||||
private static let megaphonesKey = "megaphones"
|
||||
private static let uuidKey = "uuid"
|
||||
private static let priorityKey = "priority"
|
||||
private static let iosMinVersionKey = "iosMinVersion"
|
||||
private static let countriesKey = "countries"
|
||||
private static let dontShowBeforeEpochSecondsKey = "dontShowBeforeEpochSeconds"
|
||||
private static let dontShowAfterEpochSecondsKey = "dontShowAfterEpochSeconds"
|
||||
private static let showForNumberOfDaysKey = "showForNumberOfDays"
|
||||
private static let conditionalIdKey = "conditionalId"
|
||||
private static let primaryCtaIdKey = "primaryCtaId"
|
||||
private static let primaryCtaDataKey = "primaryCtaData"
|
||||
private static let secondaryCtaIdKey = "secondaryCtaId"
|
||||
private static let secondaryCtaDataKey = "secondaryCtaData"
|
||||
|
||||
static func parseFrom(parser megaphonesArrayParser: ParamParser) throws -> [Self] {
|
||||
let individualMegaphones: [[String: Any]] = try megaphonesArrayParser.required(key: Self.megaphonesKey)
|
||||
|
||||
return try individualMegaphones.compactMap { megaphoneObject throws -> Self? in
|
||||
let megaphoneParser = ParamParser(megaphoneObject)
|
||||
|
||||
guard let iosMinVersion: String = try megaphoneParser.optional(key: Self.iosMinVersionKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let uuid: String = try megaphoneParser.required(key: Self.uuidKey)
|
||||
let priority: Int = try megaphoneParser.required(key: Self.priorityKey)
|
||||
let countries: String = try megaphoneParser.required(key: Self.countriesKey)
|
||||
let dontShowBeforeEpochSeconds: UInt64 = try megaphoneParser.required(key: Self.dontShowBeforeEpochSecondsKey)
|
||||
let dontShowAfterEpochSeconds: UInt64 = try megaphoneParser.required(key: Self.dontShowAfterEpochSecondsKey)
|
||||
let showForNumberOfDays: Int = try megaphoneParser.required(key: Self.showForNumberOfDaysKey)
|
||||
|
||||
let conditionalId: String? = try megaphoneParser.optional(key: Self.conditionalIdKey)
|
||||
let primaryCtaId: String? = try megaphoneParser.optional(key: Self.primaryCtaIdKey)
|
||||
let primaryCtaDataJson: [String: Any]? = try megaphoneParser.optional(key: Self.primaryCtaDataKey)
|
||||
let secondaryCtaId: String? = try megaphoneParser.optional(key: Self.secondaryCtaIdKey)
|
||||
let secondaryCtaDataJson: [String: Any]? = try megaphoneParser.optional(key: Self.secondaryCtaDataKey)
|
||||
|
||||
var conditionalCheck: ConditionalCheck?
|
||||
if let conditionalId {
|
||||
conditionalCheck = ConditionalCheck(fromConditionalId: conditionalId)
|
||||
}
|
||||
|
||||
var primaryAction: Action?
|
||||
if let primaryCtaId {
|
||||
primaryAction = Action(fromActionId: primaryCtaId)
|
||||
}
|
||||
|
||||
var primaryActionData: ActionData?
|
||||
if let primaryCtaDataJson {
|
||||
primaryActionData = try ActionData.parse(fromJson: primaryCtaDataJson)
|
||||
}
|
||||
|
||||
var secondaryAction: Action?
|
||||
if let secondaryCtaId {
|
||||
secondaryAction = Action(fromActionId: secondaryCtaId)
|
||||
}
|
||||
|
||||
var secondaryActionData: ActionData?
|
||||
if let secondaryCtaDataJson {
|
||||
secondaryActionData = try ActionData.parse(fromJson: secondaryCtaDataJson)
|
||||
}
|
||||
|
||||
return RemoteMegaphoneModel.Manifest(
|
||||
id: uuid,
|
||||
priority: priority,
|
||||
minAppVersion: iosMinVersion,
|
||||
countries: countries,
|
||||
dontShowBefore: dontShowBeforeEpochSeconds,
|
||||
dontShowAfter: dontShowAfterEpochSeconds,
|
||||
showForNumberOfDays: showForNumberOfDays,
|
||||
conditionalCheck: conditionalCheck,
|
||||
primaryAction: primaryAction,
|
||||
primaryActionData: primaryActionData,
|
||||
secondaryAction: secondaryAction,
|
||||
secondaryActionData: secondaryActionData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parsing translations
|
||||
|
||||
private extension RemoteMegaphoneModel.Translation {
|
||||
private static let uuidKey = "uuid"
|
||||
private static let imageUrlKey = "image"
|
||||
private static let titleKey = "title"
|
||||
private static let bodyKey = "body"
|
||||
private static let primaryCtaTextKey = "primaryCtaText"
|
||||
private static let secondaryCtaTextKey = "secondaryCtaText"
|
||||
|
||||
static func parseFrom(parser: ParamParser) throws -> Self {
|
||||
let uuid: String = try parser.required(key: Self.uuidKey)
|
||||
let imageUrl: String? = try parser.optional(key: Self.imageUrlKey)
|
||||
let title: String = try parser.required(key: Self.titleKey)
|
||||
let body: String = try parser.required(key: Self.bodyKey)
|
||||
let primaryCtaText: String? = try parser.optional(key: Self.primaryCtaTextKey)
|
||||
let secondaryCtaText: String? = try parser.optional(key: Self.secondaryCtaTextKey)
|
||||
|
||||
guard uuid.isPermissibleAsFilename else {
|
||||
throw OWSAssertionError("Translation had UUID that is illegal filename: \(uuid)")
|
||||
}
|
||||
|
||||
return RemoteMegaphoneModel.Translation.makeWithoutLocalImage(
|
||||
id: uuid,
|
||||
title: title,
|
||||
body: body,
|
||||
imageRemoteUrlPath: imageUrl,
|
||||
primaryActionText: primaryCtaText,
|
||||
secondaryActionText: secondaryCtaText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
149
Signal/Megaphones/RemoteReleaseNotesFetcher.swift
Normal file
149
Signal/Megaphones/RemoteReleaseNotesFetcher.swift
Normal file
@ -0,0 +1,149 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalServiceKit
|
||||
|
||||
private extension Array<String> {
|
||||
/// A list of possible locale strings for which a translation may be
|
||||
/// available, based on the user's current locale. Includes a fallback to
|
||||
/// English.
|
||||
static var possibleTranslationLocaleStrings: [String] {
|
||||
var locales: [String] = []
|
||||
|
||||
if let langCode = Locale.current.languageCode {
|
||||
locales.append(langCode)
|
||||
|
||||
if let regionCode = Locale.current.regionCode {
|
||||
locales.append("\(langCode)_\(regionCode)")
|
||||
}
|
||||
}
|
||||
|
||||
// Always include English at the end, as a fallback. This translation
|
||||
// should always exist.
|
||||
return locales + ["en"]
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// The path at which a translation may be found, for the given manifest
|
||||
/// and locale string.
|
||||
static func translationUrlPath(
|
||||
forManifestId manifestId: String,
|
||||
withLocaleString localeString: String,
|
||||
) -> String? {
|
||||
"static/release-notes/\(manifestId)/\(localeString).json"
|
||||
.percentEncodedAsUrlPath
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: URLs
|
||||
|
||||
extension URL {
|
||||
static func mediaFilePath(dirUrl: URL, mediaLocalRelativePath: String) -> URL? {
|
||||
guard OWSFileSystem.ensureDirectoryExists(dirUrl.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return dirUrl.appendingPathComponent(mediaLocalRelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
public class RemoteReleaseNotesFetcher<ManifestType, TranslationType> {
|
||||
let db: DB
|
||||
let remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol
|
||||
var fetchedTranslations: [(ManifestType, TranslationType)] = []
|
||||
|
||||
init(
|
||||
db: DB,
|
||||
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
|
||||
) {
|
||||
self.db = db
|
||||
self.remoteReleaseNotesService = remoteReleaseNotesService
|
||||
}
|
||||
|
||||
func run(manifests: [ManifestType]) async throws {
|
||||
fetchedTranslations = try await withThrowingTaskGroup(of: (ManifestType, TranslationType).self) { taskGroup in
|
||||
for manifest in manifests {
|
||||
taskGroup.addTask {
|
||||
let translation = try await self.fetchTranslation(forManifest: manifest)
|
||||
return (manifest, translation)
|
||||
}
|
||||
}
|
||||
return try await taskGroup.reduce(into: [], { $0.append($1) })
|
||||
}
|
||||
|
||||
await db.awaitableWrite { tx in
|
||||
updatePersistedData(withFetchedData: fetchedTranslations, transaction: tx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch user-displayable localized strings for the given manifest. Will
|
||||
/// attempt to fetch a translation matching the user's current locale,
|
||||
/// falling back to English otherwise.
|
||||
private func fetchTranslation(
|
||||
forManifest manifest: ManifestType,
|
||||
) async throws -> TranslationType {
|
||||
let localeStrings: [String] = .possibleTranslationLocaleStrings
|
||||
|
||||
for (index, localeString) in localeStrings.enumerated() {
|
||||
do {
|
||||
return try await fetchTranslationAndImage(forManifest: manifest, withLocaleString: localeString)
|
||||
} catch let error as OWSHTTPError where error.responseStatusCode == 404 && (index + 1) != localeStrings.endIndex {
|
||||
// If this isn't the last locale & it's not found, try the next one.
|
||||
continue
|
||||
}
|
||||
// If we hit a non-404 error, propagate it out immediately.
|
||||
}
|
||||
|
||||
// We either return a value or throw an error in the loop as long as there
|
||||
// is at least one locale.
|
||||
throw OWSAssertionError("Unexpectedly found no locale strings!")
|
||||
}
|
||||
|
||||
/// Downloads the image if necessary.
|
||||
///
|
||||
/// Doesn't perform any network requests if the image has already been
|
||||
/// downloaded.
|
||||
///
|
||||
/// - Throws: If the image should be downloaded but can't be downloaded.
|
||||
/// - Returns: Whether or not `translation` has an image.
|
||||
func downloadMediaIfNecessary(
|
||||
mediaRemoteUrlPath: String?,
|
||||
mediaFileDirectory: URL,
|
||||
translationId: String,
|
||||
) async throws -> Bool {
|
||||
guard let mediaRemoteUrlPath else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let mediaFileUrl: URL = .mediaFilePath(dirUrl: mediaFileDirectory, mediaLocalRelativePath: translationId) else {
|
||||
throw OWSAssertionError("Failed to get image file path for translation with ID \(translationId)")
|
||||
}
|
||||
|
||||
return try await Retry.performWithBackoff(
|
||||
maxAttempts: 3,
|
||||
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
||||
block: {
|
||||
try await remoteReleaseNotesService.downloadMedia(
|
||||
mediaRemoteUrlPath: mediaRemoteUrlPath,
|
||||
mediaFileUrl: mediaFileUrl,
|
||||
translationId: translationId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func fetchTranslationAndImage(
|
||||
forManifest manifest: ManifestType,
|
||||
withLocaleString localeString: String,
|
||||
) async throws -> TranslationType {
|
||||
owsFail("Must override fetch")
|
||||
}
|
||||
|
||||
func updatePersistedData(withFetchedData fetchedTranslations: [(ManifestType, TranslationType)], transaction: DBWriteTransaction) {
|
||||
owsFail("Must override fetch")
|
||||
}
|
||||
}
|
||||
71
Signal/Megaphones/RemoteReleaseNotesFetchingManager.swift
Normal file
71
Signal/Megaphones/RemoteReleaseNotesFetchingManager.swift
Normal file
@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalServiceKit
|
||||
|
||||
/// Handles fetching and parsing remote megaphones and release notes.
|
||||
public class RemoteReleaseNotesFetchingManager {
|
||||
private let remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol
|
||||
private let remoteMegaphoneFetcher: RemoteMegaphoneFetcher
|
||||
private let remoteAnnouncementFetcher: RemoteAnnouncementFetcher
|
||||
|
||||
init(
|
||||
db: DB,
|
||||
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
|
||||
) {
|
||||
self.remoteReleaseNotesService = remoteReleaseNotesService
|
||||
|
||||
self.remoteMegaphoneFetcher = RemoteMegaphoneFetcher(
|
||||
db: db,
|
||||
remoteReleaseNotesService: remoteReleaseNotesService,
|
||||
)
|
||||
self.remoteAnnouncementFetcher = RemoteAnnouncementFetcher(
|
||||
db: db,
|
||||
remoteReleaseNotesService: remoteReleaseNotesService,
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch all remote release notes currently on the service and persist them
|
||||
/// locally. Removes any locally-persisted remote release notes that are no
|
||||
/// longer available remotely.
|
||||
func syncRemoteReleaseNotes() async throws {
|
||||
Logger.info("Beginning remote release notes fetch.")
|
||||
|
||||
let (megaphoneManifests, announcementManifests) = try await fetchManifests()
|
||||
|
||||
let megaphoneResult = await Result {
|
||||
try await remoteMegaphoneFetcher.run(manifests: megaphoneManifests)
|
||||
}
|
||||
|
||||
if case .failure(let error) = megaphoneResult {
|
||||
Logger.error("megaphone fetch failed: \(error)")
|
||||
}
|
||||
|
||||
if BuildFlags.ReleaseNotesChannel.announcementFetch {
|
||||
let announcementResult = await Result {
|
||||
try await remoteAnnouncementFetcher.run(manifests: announcementManifests)
|
||||
}
|
||||
if case .failure(let error) = announcementResult {
|
||||
Logger.error("announcement fetch failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the manifests for the currently-active remote megaphones.
|
||||
/// Manifests contain metadata about a megaphone, such as when it should be
|
||||
/// shown and what actions it should expose. They do not contain any
|
||||
/// user-visible content, such as strings.
|
||||
private func fetchManifests() async throws -> ([RemoteMegaphoneModel.Manifest], [RemoteAnnouncementModel.Manifest]) {
|
||||
return try await Retry.performWithBackoff(
|
||||
maxAttempts: 3,
|
||||
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
||||
block: {
|
||||
Logger.info("Fetching remote release notes manifests")
|
||||
return try await remoteReleaseNotesService.fetchManifests()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -366,11 +368,13 @@ public class NotificationActionHandler {
|
||||
|
||||
@MainActor
|
||||
private class func submitDebugLogs(supportTag: String?) async {
|
||||
await withCheckedContinuation { continuation in
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: .fromGlobals()) {
|
||||
continuation.resume()
|
||||
}
|
||||
guard let viewController = CurrentAppContext().frontmostViewController() else {
|
||||
return
|
||||
}
|
||||
await DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -25,6 +25,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
private let signalService: OWSSignalServiceProtocol
|
||||
private let storageServiceManager: StorageServiceManager
|
||||
private let svr: SecureValueRecovery
|
||||
private let svrLocalStorage: SVRLocalStorage
|
||||
private let syncManager: SyncManagerProtocol
|
||||
private let threadStore: ThreadStore
|
||||
private let tsAccountManager: TSAccountManager
|
||||
@ -47,6 +48,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
signalService: OWSSignalServiceProtocol,
|
||||
storageServiceManager: StorageServiceManager,
|
||||
svr: SecureValueRecovery,
|
||||
svrLocalStorage: SVRLocalStorage,
|
||||
syncManager: SyncManagerProtocol,
|
||||
threadStore: ThreadStore,
|
||||
tsAccountManager: TSAccountManager,
|
||||
@ -68,6 +70,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
self.signalService = signalService
|
||||
self.storageServiceManager = storageServiceManager
|
||||
self.svr = svr
|
||||
self.svrLocalStorage = svrLocalStorage
|
||||
self.syncManager = syncManager
|
||||
self.threadStore = threadStore
|
||||
self.tsAccountManager = tsAccountManager
|
||||
@ -359,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,
|
||||
@ -402,7 +393,6 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
userProfileWriter: .linking,
|
||||
transaction: tx,
|
||||
)
|
||||
self.svr.clearKeys(transaction: tx)
|
||||
|
||||
// reset to default (false)
|
||||
self.receiptManager.setAreReadReceiptsEnabled(
|
||||
@ -480,7 +470,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
didLinkNSync: Bool,
|
||||
) async throws(CompleteProvisioningError) {
|
||||
let hasBackedUpMasterKey = self.db.read { tx in
|
||||
self.svr.hasBackedUpMasterKey(transaction: tx)
|
||||
self.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
|
||||
}
|
||||
let capabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
|
||||
do {
|
||||
@ -703,7 +693,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
|
||||
|
||||
let phoneNumberDiscoverability = tsAccountManager.phoneNumberDiscoverability(tx: tx)
|
||||
|
||||
let hasSVRBackups = svr.hasBackedUpMasterKey(transaction: tx)
|
||||
let hasSVRBackups = svrLocalStorage.isMasterKeyBackedUp(tx: tx)
|
||||
|
||||
return AccountAttributes(
|
||||
isManualMessageFetchEnabled: isManualMessageFetchEnabled,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -51,6 +51,7 @@ class ProvisioningController: NSObject {
|
||||
signalService: SSKEnvironment.shared.signalServiceRef,
|
||||
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
|
||||
svr: DependenciesBridge.shared.svr,
|
||||
svrLocalStorage: DependenciesBridge.shared.svrLocalStorage,
|
||||
syncManager: SSKEnvironment.shared.syncManagerRef,
|
||||
threadStore: ThreadStoreImpl(),
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
@ -147,7 +148,11 @@ class ProvisioningController: NSObject {
|
||||
@objc
|
||||
@MainActor
|
||||
private func submitLogs() {
|
||||
DebugLogs.submitLogs(supportTag: "Onboarding", dumper: .fromGlobals())
|
||||
guard let viewController = CurrentAppContext().frontmostViewController() else {
|
||||
return
|
||||
}
|
||||
let logs = DebugLogs(dumper: .fromGlobals())
|
||||
logs.promptToSubmitLogs(from: viewController, supportTag: "Onboarding")
|
||||
}
|
||||
|
||||
// MARK: - Transitions
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -41,6 +41,7 @@ public struct RegistrationCoordinatorDependencies {
|
||||
public let signalService: OWSSignalServiceProtocol
|
||||
public let storageServiceManager: RegistrationCoordinatorImpl.Shims.StorageServiceManager
|
||||
public let svr: SecureValueRecovery
|
||||
public let svrLocalStorage: SVRLocalStorage
|
||||
public let svrAuthCredentialStore: SVRAuthCredentialStorage
|
||||
public let timeoutProvider: RegistrationCoordinatorImpl.Shims.TimeoutProvider
|
||||
public let tsAccountManager: TSAccountManager
|
||||
@ -88,6 +89,7 @@ public struct RegistrationCoordinatorDependencies {
|
||||
signalService: SSKEnvironment.shared.signalServiceRef,
|
||||
storageServiceManager: RegistrationCoordinatorImpl.Wrappers.StorageServiceManager(SSKEnvironment.shared.storageServiceManagerRef),
|
||||
svr: DependenciesBridge.shared.svr,
|
||||
svrLocalStorage: DependenciesBridge.shared.svrLocalStorage,
|
||||
svrAuthCredentialStore: DependenciesBridge.shared.svrCredentialStorage,
|
||||
timeoutProvider: RegistrationCoordinatorImpl.Wrappers.TimeoutProvider(),
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
|
||||
@ -424,7 +424,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
case .changingNumber:
|
||||
break
|
||||
case .registering, .reRegistering:
|
||||
deps.svr.clearKeys(transaction: tx)
|
||||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||||
}
|
||||
}
|
||||
@ -485,9 +484,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
case .changingNumber:
|
||||
break
|
||||
case .registering, .reRegistering:
|
||||
// Whenever we do this, wipe the keys we've got.
|
||||
// We don't want to have them and use then implicitly later.
|
||||
deps.svr.clearKeys(transaction: tx)
|
||||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||||
}
|
||||
}
|
||||
@ -524,9 +520,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
case .changingNumber:
|
||||
break
|
||||
case .registering, .reRegistering:
|
||||
// Whenever we do this, wipe the keys we've got.
|
||||
// We don't want to have them and use them implicitly later.
|
||||
deps.svr.clearKeys(transaction: tx)
|
||||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||||
}
|
||||
}
|
||||
@ -888,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?
|
||||
@ -1276,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()
|
||||
@ -1438,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.
|
||||
@ -2043,9 +2026,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
// Its possible we tried svr2 and kbs has the right info, or vice versa, but this is all
|
||||
// best effort anyway; just fall back to session-based registration.
|
||||
deps.svrAuthCredentialStore.removeSVR2CredentialsForCurrentUser(tx)
|
||||
// Clear the SVR master key locally; we failed reglock so we know its wrong
|
||||
// and useless anyway.
|
||||
deps.svr.clearKeys(transaction: tx)
|
||||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||||
self.updatePersistedState(tx) {
|
||||
$0.e164WithKnownReglockEnabled = e164
|
||||
@ -2185,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
|
||||
}
|
||||
}
|
||||
@ -2323,7 +2303,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
// If we have a local master key, theres no need to restore after registration.
|
||||
// (we will still back up though)
|
||||
inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = localMasterKey == nil
|
||||
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svr.hasBackedUpMasterKey(transaction: tx)
|
||||
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
|
||||
}
|
||||
|
||||
// MARK: - SVR Auth Credential Candidates Pathway
|
||||
@ -2740,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.
|
||||
@ -3544,13 +3524,13 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
let accountEntropyPool = getOrGenerateAccountEntropyPool()
|
||||
|
||||
if
|
||||
let backupStepGuarantee = await performSVRBackupStepsIfNeeded(
|
||||
let nextStep = await performSVRBackupStepsIfNeeded(
|
||||
resetPINReminderInterval: false,
|
||||
accountEntropyPool: accountEntropyPool,
|
||||
accountIdentity: accountIdentity,
|
||||
)
|
||||
{
|
||||
return backupStepGuarantee
|
||||
return nextStep
|
||||
}
|
||||
|
||||
return await exportAndWipeState(
|
||||
@ -3918,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 {
|
||||
@ -4037,9 +4017,10 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
|
||||
let masterKey = accountEntropyPool.getMasterKey()
|
||||
do {
|
||||
let backedUpMasterKey = try await deps.svr.backupMasterKey(
|
||||
try await deps.svr.backupMasterKey(
|
||||
pin: pin,
|
||||
masterKey: masterKey,
|
||||
force: true,
|
||||
authMethod: authMethod,
|
||||
)
|
||||
|
||||
@ -4047,7 +4028,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
await db.awaitableWrite { tx in
|
||||
logger.info("Setting pin code after SVR backup")
|
||||
updateMasterKeyAndLocalState(
|
||||
masterKey: backedUpMasterKey,
|
||||
masterKey: masterKey,
|
||||
tx: tx,
|
||||
)
|
||||
deps.ows2FAManager.markPinEnabled(
|
||||
@ -4389,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)
|
||||
@ -4780,10 +4761,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||||
|
||||
private func reglockToken(for e164: E164) -> String? {
|
||||
if
|
||||
|
||||
inMemoryState.wasReglockEnabledBeforeStarting
|
||||
|| persistedState.e164WithKnownReglockEnabled == e164
|
||||
,
|
||||
inMemoryState.wasReglockEnabledBeforeStarting || persistedState.e164WithKnownReglockEnabled == e164,
|
||||
let reglockToken = inMemoryState.reglockToken
|
||||
{
|
||||
return reglockToken
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,9 @@ public class RegistrationNavigationController: OWSNavigationController {
|
||||
if #available(iOS 26.0, *) {
|
||||
interactiveContentPopGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
if #unavailable(iOS 26) {
|
||||
navigationBar.tintColor = .Signal.accent
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
@ -62,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
|
||||
|
||||
@ -497,7 +500,8 @@ public class RegistrationNavigationController: OWSNavigationController {
|
||||
))
|
||||
self.present(navVc, animated: true)
|
||||
} else {
|
||||
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
|
||||
let logs = DebugLogs(dumper: .fromGlobals())
|
||||
logs.promptToSubmitLogs(from: self, supportTag: "Registration")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -620,17 +624,6 @@ extension RegistrationNavigationController: RegistrationPinPresenter {
|
||||
func submitWithCreateNewPinInstead() {
|
||||
pushNextController(coordinator.skipAndCreateNewPINCode())
|
||||
}
|
||||
|
||||
func enterRecoveryKey() {
|
||||
pushNextController(
|
||||
.value(.enterRecoveryKey(
|
||||
RegistrationEnterAccountEntropyPoolState(
|
||||
canShowBackButton: true,
|
||||
canShowNoKeyHelpButton: false,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension RegistrationNavigationController: RegistrationPinAttemptsExhaustedAndMustCreateNewPinPresenter {
|
||||
|
||||
@ -129,16 +129,6 @@ class RegistrationPhoneNumberViewController: OWSViewController {
|
||||
|
||||
// MARK: UI
|
||||
|
||||
private lazy var contextButton: ContextMenuButton = {
|
||||
let result = ContextMenuButton(empty: ())
|
||||
result.setImage(Theme.iconImage(.buttonMore), for: .normal)
|
||||
if #unavailable(iOS 26) {
|
||||
result.tintColor = .Signal.accent
|
||||
}
|
||||
result.autoSetDimensions(to: .square(40))
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel.titleLabelForRegistration(text: OWSLocalizedString(
|
||||
"REGISTRATION_PHONE_NUMBER_TITLE",
|
||||
@ -178,21 +168,13 @@ class RegistrationPhoneNumberViewController: OWSViewController {
|
||||
|
||||
view.backgroundColor = .Signal.background
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
customView: contextButton,
|
||||
accessibilityIdentifier: "registration.verificationCode.contextButton",
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: CommonStrings.nextButton,
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(didTapNext),
|
||||
accessibilityIdentifier: "registration.phonenumber.nextButton",
|
||||
)
|
||||
navigationItem.rightBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(
|
||||
title: CommonStrings.nextButton,
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(didTapNext),
|
||||
accessibilityIdentifier: "registration.phonenumber.nextButton",
|
||||
)
|
||||
barButtonItem.tintColor = .Signal.accent
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let stackView = addStaticContentStackView(
|
||||
arrangedSubviews: [
|
||||
@ -276,7 +258,8 @@ class RegistrationPhoneNumberViewController: OWSViewController {
|
||||
},
|
||||
))
|
||||
}
|
||||
contextButton.setActions(actions: actions)
|
||||
|
||||
navigationItem.leftBarButtonItem = .contextMenuButton(actions: actions)
|
||||
|
||||
let now = Date()
|
||||
|
||||
|
||||
@ -89,8 +89,6 @@ protocol RegistrationPinPresenter: AnyObject {
|
||||
func submitWithCreateNewPinInstead()
|
||||
|
||||
func exitRegistration()
|
||||
|
||||
func enterRecoveryKey()
|
||||
}
|
||||
|
||||
// MARK: - RegistrationPinViewController
|
||||
@ -146,21 +144,6 @@ class RegistrationPinViewController: OWSViewController {
|
||||
|
||||
// MARK: Rendering
|
||||
|
||||
private lazy var moreButton: ContextMenuButton = {
|
||||
let result = ContextMenuButton(empty: ())
|
||||
result.setImage(Theme.iconImage(.buttonMore), for: .normal)
|
||||
if #unavailable(iOS 26) {
|
||||
result.tintColor = .Signal.accent
|
||||
}
|
||||
result.autoSetDimensions(to: .square(40))
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var moreBarButton = UIBarButtonItem(
|
||||
customView: moreButton,
|
||||
accessibilityIdentifier: "registration.pin.disablePinButton",
|
||||
)
|
||||
|
||||
private lazy var backButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.setTemplateImage(
|
||||
@ -330,19 +313,15 @@ class RegistrationPinViewController: OWSViewController {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .Signal.background
|
||||
navigationItem.rightBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(
|
||||
title: CommonStrings.nextButton,
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(didTapNext),
|
||||
accessibilityIdentifier: "registration.pin.nextButton",
|
||||
)
|
||||
barButtonItem.tintColor = .Signal.accent
|
||||
return barButtonItem
|
||||
}()
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: CommonStrings.nextButton,
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(didTapNext),
|
||||
accessibilityIdentifier: "registration.pin.nextButton",
|
||||
)
|
||||
|
||||
self.stackView = addStaticContentStackView(
|
||||
stackView = addStaticContentStackView(
|
||||
arrangedSubviews: [titleLabel, explanationView, pinTextField],
|
||||
isScrollable: true,
|
||||
shouldAvoidKeyboard: true,
|
||||
@ -399,9 +378,7 @@ class RegistrationPinViewController: OWSViewController {
|
||||
}
|
||||
|
||||
private func configureUIForCreatingNewPin() {
|
||||
navigationItem.leftBarButtonItem = moreBarButton
|
||||
|
||||
moreButton.setActions(actions: [
|
||||
navigationItem.leftBarButtonItem = .contextMenuButton(actions: [
|
||||
UIAction(
|
||||
title: OWSLocalizedString(
|
||||
"PIN_CREATION_LEARN_MORE",
|
||||
@ -462,9 +439,7 @@ class RegistrationPinViewController: OWSViewController {
|
||||
skippability: RegistrationPinState.Skippability,
|
||||
remainingAttempts: UInt?,
|
||||
) {
|
||||
navigationItem.leftBarButtonItem = moreBarButton
|
||||
var actions = [UIMenuElement]()
|
||||
|
||||
var actions = [UIAction]()
|
||||
if skippability.canSkip {
|
||||
actions.append(UIAction(
|
||||
title: OWSLocalizedString(
|
||||
@ -477,23 +452,11 @@ 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)
|
||||
}
|
||||
|
||||
moreButton.setActions(actions: actions)
|
||||
navigationItem.leftBarButtonItem = .contextMenuButton(actions: actions)
|
||||
|
||||
showAttemptWarningIfNecessary(
|
||||
remainingAttempts: remainingAttempts,
|
||||
@ -705,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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user