Compare commits

...

116 Commits

Author SHA1 Message Date
Kate
972078533b Update translations
Some checks failed
CI / Build and Test (Xcode_26.5) (push) Has been cancelled
CI / Check if strings file is outdated (push) Has been cancelled
protobuf-check / check protobufs (push) Has been cancelled
2026-06-10 15:47:47 -04:00
Kate
1eb8b48bb4 Update release notes 2026-06-10 15:47:10 -04:00
Max Radermacher
221043a998
Compute mediaName dynamically 2026-06-10 13:51:02 -05:00
Max Radermacher
3914c811be
Debug when we request reviews 2026-06-10 13:26:53 -05:00
Elaine
c4b6b61da7
Disable notification actions with screen lock enabled 2026-06-10 14:08:15 -04:00
Sasha Weiss
aa353b3f59
Remove remote-config gate for Optimize Storage 2026-06-10 10:56:11 -07:00
Sasha Weiss
ee158d4c4c
Remove redundant startCron argument to AppDelegate/refreshConnection 2026-06-10 10:35:24 -07:00
Sasha Weiss
459643fd14
Default Optimize Storage to off for new Backup signups 2026-06-10 10:09:02 -07:00
Elaine
3b1636e179
Fix stories links 2026-06-09 19:30:28 -04:00
Sasha Weiss
cad4022e68
Backfill missing errorType property for OWSRecoverableDecryptionPlaceholder(s) 2026-06-09 16:26:43 -07:00
Sasha Weiss
d12641a3a2
Inline some OWSRecoverablePlaceholder logic 2026-06-09 16:10:25 -07:00
Elaine
f995e51b28
Revert "Improved media viewer layout on iPad." 2026-06-09 18:03:11 -04:00
Pete Walters
213cbcd9ad
Remove option to enter recovery key from PIN entry screen. 2026-06-09 16:53:12 -05:00
Sasha Weiss
f5892e0aad
Warn when attempting to paste AEP into ConversationInputTextView 2026-06-09 12:56:31 -07:00
Max Radermacher
b139b7c7b9
Update to LibSignal v0.95.0 2026-06-09 14:29:20 -05:00
Pete Walters
3e6dc35321
Disable optimizeLocalStorage when restoring during provisioning 2026-06-09 08:21:44 -05:00
Sasha Weiss
30431a6354
Hide "sensitive" views from the App Switcher 2026-06-08 20:47:21 -07:00
Max Radermacher
6be8862bdb
Don’t start expiration timer unless specified 2026-06-08 21:01:03 -05:00
Max Radermacher
2182e5952a
Make some expiration timer fields nonnull 2026-06-08 20:59:03 -05:00
Elaine
800a5cc0bc
Improve handling of many large collapse sets 2026-06-08 20:17:12 -04:00
Pete Walters
82ce1ead86
Prioritize backup thumbnail downloads for the opened conversation 2026-06-08 17:45:33 -05:00
Pete Walters
d10259eae1
Fix AudioCell layout for undownloaded items 2026-06-08 17:45:07 -05:00
sashaweiss-signal
1a9a0dbdd9 Tweak strings for Recovery Key copying warning sheet 2026-06-08 15:33:32 -07:00
Sasha Weiss
1b8e0a0c93
Don't require SessionRecord to copy flawlessly in DatabaseRecovery 2026-06-08 12:53:13 -07:00
Max Radermacher
cef72e5a5a
Wait for sync message after storage service error 2026-06-08 14:17:53 -05:00
Sasha Weiss
eb533a72a2
Return PENDING, not TERMINAL, error when attempting to backfill offloaded media 2026-06-08 11:35:45 -07:00
kate-signal
08a3e32943
fix release notes wallpaper 2026-06-08 09:04:22 -04:00
Max Radermacher
b957357516
Clarify regeneration vs. duplicates comment 2026-06-05 12:12:24 -05:00
Max Radermacher
c38b1309dd
Remove unused provisioning/sync message fields 2026-06-05 12:10:55 -05:00
Max Radermacher
926432d03a Fix typo: seconday → secondary 2026-06-05 12:07:19 -05:00
Max Radermacher
6628b9f6fc
Remove unused MOB code 2026-06-05 11:52:31 -05:00
Pete Walters
1954342a36
De-singleton RemoteAttestation 2026-06-05 08:15:59 -05:00
Max Radermacher
f40bc944ae
Remove throws from method that doesn’t throw 2026-06-04 19:38:23 -05:00
Elaine
feeb1303e5 Bump version to 8.16 2026-06-04 15:48:48 -04:00
Elaine
a6e8eda73c Update translations
Some checks failed
CI / Build and Test (Xcode_26.5) (push) Has been cancelled
CI / Check if strings file is outdated (push) Has been cancelled
2026-06-04 15:48:45 -04:00
Elaine
a661667b24 Update release notes 2026-06-04 15:48:06 -04:00
Sasha Weiss
a978b4cc8b
Add warning sheet when copying Recovery Key 2026-06-04 11:29:34 -07:00
Sasha Weiss
15ada8dcf0
Expose BackupPlanOptionView for reuse 2026-06-03 21:28:07 -07:00
Pete Walters
15f9d3dc96
Fix a layout issue when reloading a gif in the media gallery 2026-06-03 16:31:30 -05:00
Pete Walters
fc5102cd54
Label video + shouldLoop as GIF in media gallery 2026-06-03 16:31:06 -05:00
Sasha Weiss
16c179115e
Convert Decryption Placeholder expiration to an ExpirationJob 2026-06-03 13:58:46 -07:00
Elaine
d28e29fa21
Reload story rows when nicknames change 2026-06-03 15:31:51 -04:00
andrew-signal
265757716a
Update to libsignal v0.94.4. 2026-06-03 13:23:57 -04:00
Max Radermacher
0f0c3e6fc6
Use failIfThrows in place of forced unwraps 2026-06-03 12:12:08 -05:00
Pete Walters
8a53464a41
Allow non-restore backup tier downloads over cellular 2026-06-02 21:38:48 -05:00
sashaweiss-signal
79bbd556a4 Tweak Optimize Storage warning strings 2026-06-02 16:51:31 -07:00
Max Radermacher
cfb22a38b3 Fix typo: attachmenr → attachment 2026-06-02 17:33:38 -05:00
Elaine
808f3218db
Only insert one group update per info message 2026-06-02 17:03:28 -04:00
Pete Walters
280fc1f244
Bump webP encoder quality up to default quality (4) 2026-06-02 15:24:53 -05:00
Pete Walters
78130adac7
Add internal setting to force regeneration of backup thumbnails 2026-06-02 14:00:50 -05:00
sashaweiss-signal
feba86dbfb String change for Optimize Storage explanation footer 2026-06-02 11:59:38 -07:00
Pete Walters
202d8a1f07
Add two separate modes to the Safety Tip screen
Co-authored-by: Max Radermacher <max@signal.org>
2026-06-02 13:00:37 -05:00
Ehren Kret
c6492caae7 Negate negated 2026-06-02 12:38:07 -05:00
Sasha Weiss
a82216e06c
Add warning sheets for undownloaded media with an expiring IAP subscription 2026-06-02 09:51:04 -07:00
sashaweiss-signal
a173d4599a Tweak .paidExpiringSoon preview in BackupSettingsVC 2026-06-02 08:25:46 -07:00
sashaweiss-signal
6f5bc03b96 Finalize new AEP after popping to BackupSettingsVC
In the "disabling Backups to rotate my AEP" case, we may end up
presenting action sheets. To that end, make sure we're at Backup
Settings before we get there.
2026-06-02 08:25:46 -07:00
Sasha Weiss
ba15734132
Add extensive warning before skipping downloads of offloaded media 2026-06-01 16:41:31 -07:00
Pete Walters
5a57831b26
Only offload attachments related to link previews and messages 2026-06-01 16:49:43 -05:00
Pete Walters
31867c8d06
When CVC isn't visible, don't complain about starting the read timer 2026-06-01 16:45:33 -05:00
Sasha Weiss
08ae6b3e07
Make Megaphone construction one step instead of two 2026-06-01 13:24:08 -07:00
Sasha Weiss
28e9247793
Split Megaphone, MegaphoneView 2026-06-01 12:56:00 -07:00
Sasha Weiss
8b1379149c
Don't show a megaphone for 1d after dismissing previous 2026-06-01 12:51:35 -07:00
Sasha Weiss
0cc18a5285
Consolidate MegaphoneView, ExperienceUpgradeManager code 2026-06-01 12:45:54 -07:00
Sasha Weiss
f64e718ba2
Never allow My Story to be deleted 2026-06-01 12:42:17 -07:00
Sasha Weiss
185035784c
Treat images with image/gif MIME types as "GIFs" in the Media Gallery 2026-06-01 12:09:38 -07:00
Sasha Weiss
dcf02125a0
Simplify PIN creation error handling 2026-06-01 11:04:13 -07:00
Pete Walters
44e6c6cb43
Show the download icon overlay if no thumbnail present 2026-06-01 12:57:01 -05:00
Pete Walters
dc3a819024
Use backup thumbnails for link previews 2026-06-01 12:56:46 -05:00
Pete Walters
c0cedd0026
Fall back to backup thumbnail for quoted message attachment 2026-06-01 12:56:28 -05:00
Pete Walters
27439824e7
Consolidate MediaGallery-related CVAttachmentProgressView creation 2026-06-01 12:55:56 -05:00
Max Radermacher
c7005df406
Don’t send reactive profile keys for groups 2026-06-01 12:50:30 -05:00
Max Radermacher
4caec2f2d3
Stop decoding/validating most recordTypes 2026-06-01 12:50:08 -05:00
Max Radermacher
7dded9229a
Prune old record types 2026-06-01 12:49:00 -05:00
Max Radermacher
aa7bced824
Don’t use SDSRecordType for TSThread 2026-06-01 12:47:16 -05:00
Elaine
8663b50018
Require screen unlock before starting calls 2026-06-01 11:44:33 -04:00
Igor Solomennikov
2259a151d9
Delete unused OWSButton methods / properties. 2026-05-31 10:12:11 -07:00
Igor Solomennikov
08bf2bb9e5
Remove OWSRoundedButton. 2026-05-31 10:11:44 -07:00
Sasha Weiss
0206e8c487
Add "Enable Optimize Storage" toggle to "Welcome to Backups" sheet 2026-05-29 15:49:10 -07:00
andrew-signal
39780d4bc7
Bump to libsignal v0.94.2 2026-05-29 15:27:16 -07:00
Pete Walters
49311ef328
Add ability to download attchment via the media gallery list 2026-05-29 17:21:27 -05:00
Pete Walters
08371f4c50
Tap to download an undownloaded gallery tile attachments 2026-05-29 17:21:05 -05:00
Pete Walters
0d76c69ec1
Fix settings -> media gallery transition when missing image 2026-05-29 17:20:30 -05:00
Pete Walters
e80b3d8bdb
Allow cancelling audio/file media gallery downloads 2026-05-29 17:19:51 -05:00
Pete Walters
79122a2301
Notify when an attachment stops downloading due to exception 2026-05-29 17:19:02 -05:00
Max Radermacher
8f60728454
Don’t wrap PreKeyManager mocks in Tasks 2026-05-29 13:29:34 -05:00
Sasha Weiss
65f577efed
Stop caching SVRB auth credentials 2026-05-29 11:18:26 -07:00
Max Radermacher
8f0c315ad7
Remove rotatePreKeysOnUpgradeIfNecessary 2026-05-29 12:52:14 -05:00
gram-signal
7fd03d6bd2
SPQR: add requirePqRatio to remote configs, enforce it in MessageSender. 2026-05-29 10:07:04 -07:00
Sasha Weiss
102b164f89
De-SDS-ify ExperienceUpgrade 2026-05-29 09:42:50 -07:00
Sasha Weiss
c57f731c67
Revert "Reapply "Display AEPs with 0/O, accept 0/O/=/#"" 2026-05-29 09:42:15 -07:00
Igor Solomennikov
8b613f8bc1
Refactoring of PhotoCaptureVC and SendMediaNavController.
Restructure the code to move protocol conformance declarations into the class deceleration and stop doing class extensions.
2026-05-29 06:24:19 -07:00
Igor Solomennikov
a178545e1e
Update MessageReactionPicker.
• do not use OWSFlatButton.
• document layout constants.
• some renames for clarity.
2026-05-29 06:23:43 -07:00
Igor Solomennikov
7dde1505d7
Modernize appearance of "Choose Photo" button in QR code scanner. 2026-05-29 06:23:17 -07:00
Igor Solomennikov
6d78ec66e1
Updates to scan QR code button in Find by Username screen.
• use shared "smallSecondary" button configuration.
• use Symbols font to show qr code icon.
2026-05-29 06:22:42 -07:00
Igor Solomennikov
832f2b06eb
Modernize configuration of "Names not Verified" button.
• use UIButton.Configuration (shared with Safety Tips button now).
• do not use OWSRoundedButton anymore.
2026-05-29 06:22:09 -07:00
Igor Solomennikov
2260eb9b8f
Convert ContactCellConfiguration and ContactCellAccessoryView to structs. 2026-05-29 06:21:35 -07:00
Sasha Weiss
d535e8bfe2
Significantly slim SVRBError 2026-05-28 21:55:25 -07:00
Igor Solomennikov
5ba733f275
Updates to QR code button in top row of App Settings screen.
• use shared "roundGray" button configuration - gives us simpler code and standardized size and colors.
• do not downscale QR code icon - button grew from 36 dp to 40 dp.
2026-05-28 10:28:50 -07:00
Igor Solomennikov
3fafbe67bc
Put all buttons of different styles into one preview VC. 2026-05-28 10:28:07 -07:00
Igor Solomennikov
52f5b28db3
Remove PaypalButton - configure via UIButton.Configuration instead. 2026-05-28 10:27:27 -07:00
Igor Solomennikov
0bc63e4b57
Modernize Reset button in username qr code view.
Use UIButton.Configuration - shared "smallSecondary" style.
2026-05-28 10:26:41 -07:00
Max Radermacher
344081c1b6
Pare down InternalDiskUsageViewController 2026-05-28 12:06:44 -05:00
Max Radermacher
9e4e2976c6
Remove MasterKeySyncManager 2026-05-28 12:06:30 -05:00
Max Radermacher
892b51221a
Fix empty WAL file transfer during device transfer
Co-authored-by: Sasha Weiss <sasha@signal.org>
2026-05-28 11:29:51 -05:00
Max Radermacher
fa6876eefa
Consolidate various Signal Protocol-related files 2026-05-28 11:03:43 -05:00
Max Radermacher
79fc5037a3
Rename PreKey → PreKeyRecord 2026-05-28 11:02:25 -05:00
Max Radermacher
0088304b35
Remove BuildFlags.decodeDeprecatedPreKeys 2026-05-28 11:01:45 -05:00
Pete Walters
e4b9550f31
Support downloading of file/audio attachments within the media gallery 2026-05-28 09:25:31 -05:00
Pete Walters
7856a0f0e1
Disable the 'dont show undownloaded' flag if optimize media available 2026-05-27 22:12:33 -05:00
Pete Walters
e0b88ecf86
Remove unnecessary dispatch during progress view layout 2026-05-27 22:12:06 -05:00
Pete Walters
507959b305
Only cancel non-cancelled, non-nil download tasks 2026-05-27 22:11:43 -05:00
Igor Solomennikov
aa059ff975 Revert "Modernize appearance of "Choose Photo" button in QR code scanner."
This reverts commit 3ac71942a7.
2026-05-27 19:47:14 -07:00
Igor Solomennikov
3ac71942a7 Modernize appearance of "Choose Photo" button in QR code scanner. 2026-05-27 19:46:56 -07:00
Igor Solomennikov
28b9200637
Use CGFloat.hairlineWidth for separator height constraint.
Instead of 0.3
2026-05-27 15:33:06 -07:00
Igor Solomennikov
1c5dfb2e71
Tiny updates to Group Story Settings screen.
• use UIColor.Signal colors.
• remove unnecessary updates to UIViewController.navigationItem.
• Remove > from Remove Group Story button.
2026-05-27 15:32:47 -07:00
sashaweiss-signal
2cd69719c7 Bump version to 8.15 2026-05-27 15:12:33 -07:00
444 changed files with 10921 additions and 8226 deletions

View File

@ -11,8 +11,8 @@ source 'https://cdn.cocoapods.org/'
pod 'blurhash', podspec: './ThirdParty/blurhash.podspec'
pod 'SwiftProtobuf', "1.36.1"
ENV['LIBSIGNAL_FFI_PREBUILD_CHECKSUM'] = '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'

View File

@ -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

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

View File

@ -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
}

View File

@ -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.

View File

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

View File

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

View File

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

View 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) },
)
}
}

View 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)
}
}

View File

@ -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]],

View File

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

View File

@ -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 {

View File

@ -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
}
},

View File

@ -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,

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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

View File

@ -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()

View File

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

View File

@ -5,7 +5,7 @@
import SignalServiceKit
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,

View File

@ -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()

View File

@ -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(

View File

@ -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: [],
)
}
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
}

View File

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

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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!")

View File

@ -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!")

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)

View File

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

View File

@ -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,

View File

@ -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(

View File

@ -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)
}

View File

@ -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.",

View File

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

View File

@ -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>(

View File

@ -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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,6 +50,7 @@ enum SafetyTipsSheet {
),
handler: { [weak fromViewController] _ in
let safetyTipsVC = SafetyTipsViewController(
mode: .smsRequest,
primaryButton: SafetyTipsViewController.Button(
title: OWSLocalizedString(
"SETTINGS_ACCOUNT_BUTTON",

View File

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

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