Compare commits

..

194 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
sashaweiss-signal
cdb96e1029 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-05-27 15:09:56 -07:00
sashaweiss-signal
961936b0ca Update release notes 2026-05-27 15:09:11 -07:00
Igor Solomennikov
46445edfe7
Use modern UIButton configuration for Approve / Reject group join request buttons.
Made avatar and button a bit larger too.
2026-05-27 14:57:59 -07:00
Elaine
ab454da687
Add debug log preview 2026-05-27 16:22:31 -04:00
Igor Solomennikov
025a9ff9be
New design for wallpaper preview view. 2026-05-27 12:31:41 -07:00
Max Radermacher
c91c15ec7f
Fix estimated length when removing characters 2026-05-27 14:30:16 -05:00
Igor Solomennikov
975834e1f6
A couple improvements for the Contact Us screen.
• show "Reason" selection UI as a popup menu instead of an action sheet.
• fix "Include debug log" not wrapping to multiple lines.
2026-05-27 12:28:53 -07:00
Igor Solomennikov
77bc1008ad
Layout tweaks for username qr code view.
• use modern UIButton configuration for Copy Username button.
• better vertical alignment for the copy button.
2026-05-27 12:28:23 -07:00
Igor Solomennikov
ed9f3615ba
Remove obsolete UIButton extension methods. 2026-05-27 12:27:54 -07:00
Elaine
aad90b9f5b
Add chevron animation to collapse events 2026-05-27 14:46:08 -04:00
Elaine
dc3827ed5f
Stay scrolled at bottom when expanding collapse sets 2026-05-27 11:39:07 -07:00
Max Radermacher
f5806db594 Use Swift Testing for StringSanitizerTests 2026-05-27 13:16:03 -05:00
Max Radermacher
18871a45bd Use escape sequences for StringSanitizerTests 2026-05-27 13:16:03 -05:00
Igor Solomennikov
ec69b9425f
Present "New Story" creation options via a popup menu.
Instead of a customized action sheet.
2026-05-26 16:45:15 -07:00
Igor Solomennikov
732d375c82
Improve code for creating ••• bar button that shows a popup menu.
Old: create a ContextMenuButton (which is an UIButton) and put it into a UIBarButtonItem (as customView).

New: Create UIBarButtonItem with ••• icon and a set of UIActions to show in a popup menu.
2026-05-26 16:43:10 -07:00
Igor Solomennikov
ce442092f3
Do not tint Next button in Contact Us flow in blue.
To match all other Settings screens.
2026-05-26 16:42:42 -07:00
Igor Solomennikov
2faeff7589
Improved layout in SAEFailedViewController.
• fix issue where text was made white and wasn't visible unless in dark mode.
• use UIColor.Signal colors.
• better vertical alignment.
2026-05-26 16:42:01 -07:00
Igor Solomennikov
8384fab6a1
Update BlurredToolbarContainer.
• remove forceDarkMode parameter.
• simplify logic.
2026-05-26 16:40:03 -07:00
Max Radermacher
3a3ffde3dd
Remove indirection for some methods 2026-05-26 18:24:40 -05:00
Max Radermacher
6e45f851f2
Move some account/SVR files 2026-05-26 17:48:41 -05:00
Max Radermacher
a6476a9e79
Remove BuildFlags.AttachmentBackfill 2026-05-26 17:29:34 -05:00
Max Radermacher
276d778e22
Remove BuildFlags.MemberLabel 2026-05-26 17:29:24 -05:00
Pete Walters
92b54a1ceb
Fix transitions from a gallery item w/o image 2026-05-26 16:06:17 -05:00
kate-signal
ea190d9ae0
Pop to chat list root in showAppSettings 2026-05-26 14:16:16 -04:00
Pete Walters
507d23b760
Always listen for progress in CVAttachmentProgressView 2026-05-26 12:28:44 -05:00
Pete Walters
e39fb58e06
Fix display of download indicator when optimize media is disabled 2026-05-26 08:21:34 -05:00
Sasha Weiss
d655b7b7a4
Don't allow addresses with a label but nothing else in Backup exports.
Uses OWSContactAddress.isValid to decide if we should even try and archive it; notably, isValid did not return true if only the label was present.

Also uses strippedOrNil instead of just nilIfEmpty – I don't think we'll want to export a field if it's got just spaces in it.
2026-05-25 15:01:20 -07:00
Igor Solomennikov
29b863abf8
Improved media viewer layout on iPad.
Previous layout code was assuming that a view would have "regular" horizontal size class only when vertical size class is "compact". This is only correct on iPhones. On iPad views most often has both vertical and horizontal size classes as "regular".

This commit fixes an issue where on iPad media viewer layout would assume "compact" width and would not use screen real estate efficiently.
2026-05-25 14:17:37 -07:00
kate-signal
7beb7330a0
show verification code requested sheet in CVC 2026-05-25 08:44:48 -04:00
Max Radermacher
e14f223e79
Remove unused SpamChallengeResolvedError 2026-05-22 21:10:33 -05:00
Max Radermacher
334f6b9888
Remove obsolete Storage Service migrations 2026-05-22 20:43:35 -05:00
Max Radermacher
5f7cbb5f66
Don’t send stories multiple times 2026-05-22 20:33:19 -05:00
Sasha Weiss
0ca83a1b50
Clean up Internal Settings 2026-05-22 15:46:13 -07:00
Max Radermacher
665fda1f2a
Simplify & fix checkpointing 2026-05-22 17:36:39 -05:00
Pete Walters
ab097068a8
Add ios.optimizeStorageEnabled remote config 2026-05-22 16:08:09 -05:00
Pete Walters
ebd1292f61
Update prod SVR2 enclaves
Co-authored-by: Max Radermacher <max@signal.org>
2026-05-22 15:40:58 -05:00
Max Radermacher
118e6289ab
Fix isRetryable for network failure SignalErrors 2026-05-22 15:39:21 -05:00
Elaine
73e5108c1e
Add internal setting 2026-05-22 15:44:58 -04:00
Max Radermacher
fa65a9e9b5
Add support for simultaneous SVR2 enclaves 2026-05-22 14:17:06 -05:00
Max Radermacher
1ea6b1b1d9
Migrate current enclave to potential enclaves 2026-05-22 14:16:23 -05:00
Max Radermacher
b8a90daaf0
Stop retrying if expose fails 2026-05-22 14:13:37 -05:00
Max Radermacher
e7a0d760ca
Simplify SVR2 2026-05-22 14:12:57 -05:00
Pierre-Yves Lapersonne
e541922e02 Use dynamic type font for GroupTableViewCell subtitle
Addresses PR 6272.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
7166c115af Add .button accessibility trait to ProfileDetailsLabel, if necessary
Addresses PR 6270.
2026-05-21 17:21:59 -07:00
sashaweiss-signal
5897252015 Remove unnecessary ProfileDetailsLabel longPressAction 2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
7771cf159d Hide mutual-groups images from VoiceOver
Addresses PR 6268.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
7f55a610d8 Add .link accessibility trait to Donor FAQ button
Addresses PR 6266.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
c859d83b1d Add accessibility label to Share button in FingerprintViewController
Addresses PR 6264.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
c23f22445a Add accessibility labels to two MOB balance buttons
Addresses PR 6262.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
3cbd9ece13 Mark Payments helper cards as embedded hyperlinks
Addresses PR 6259.
2026-05-21 17:21:59 -07:00
Pierre-Yves Lapersonne
140a572d85 Hide Backups onboarding image from VoiceOver
Addresses PR 6257.
2026-05-21 17:21:59 -07:00
Sasha Weiss
a6387b9bfd
Use failIfThrows in Backup archiving enumerations 2026-05-21 16:16:33 -07:00
Pete Walters
d3b8a06e00
Display thumbnails when optimize media & auto-downloads enabled 2026-05-21 17:59:48 -05:00
Max Radermacher
48fb1065c6
Convert SVRUtil to enum 2026-05-21 17:59:07 -05:00
Pete Walters
87eb0382b3
Update WebP thumbnail quality settings 2026-05-21 17:58:13 -05:00
sashaweiss-signal
30c949e930 Snooze the Recovery Key reminder for 7d, not 2d 2026-05-21 15:13:07 -07:00
Sasha Weiss
60554470e6
Add debuglogs/ to the gitignore 2026-05-21 13:46:08 -07:00
Pete Walters
4ab7d1d12d
Fix layout of downloaded looping media 2026-05-21 13:20:56 -05:00
Igor Solomennikov
a7eb78f46c
Enable isPointerInteractionEnabled on some glass buttons.
Round buttons in chat input bar and media viewer.

With this property enabled iPadOS will highlight the button what trackpad cursor is over it.
2026-05-21 11:19:01 -07:00
Max Radermacher
153efb2d45
Defer pruning of old, unknown enclaves 2026-05-21 12:30:01 -05:00
kate-signal
ba2b662d37
Fetch remote announcements 2026-05-21 11:37:50 -04:00
Pete Walters
543085bd26
Migrate ChangeNumber prompt from HeroSheet -> ActionSheet 2026-05-21 10:07:16 -05:00
Pete Walters
a65ec79c04
Move the gallery item progress view to the root view vs media view 2026-05-20 20:45:09 -05:00
Pete Walters
2ea672abb3
Better handle deduped attachment download in media gallery 2026-05-20 20:44:47 -05:00
Pete Walters
e7d209ee66
Disable share/forward/save for undownloaded attachments 2026-05-20 20:39:58 -05:00
Pete Walters
df2e8557e9
Add MediaGallery support for downloading fullsize media 2026-05-20 20:31:18 -05:00
Max Radermacher
46b3f825a2
Back up again when MasterKey changes 2026-05-20 18:04:43 -05:00
Max Radermacher
7b7727287c
Update to Xcode 26.5 2026-05-20 17:38:30 -05:00
Max Radermacher
cf47211efe
Use Cron for SVR2 2026-05-20 17:32:18 -05:00
Max Radermacher
3cb8044133
Simplify InProgressBackup handling 2026-05-20 17:29:58 -05:00
Sasha Weiss
8ddef58df8
Stop logging Backup frames on error 2026-05-20 15:04:17 -07:00
Sasha Weiss
7f240db8a4
Use single-element TimeGatedBatch in BackupOversizeTextCache 2026-05-20 14:59:37 -07:00
Max Radermacher
d5747a432a Remove unnecessary do { … } block 2026-05-20 16:54:01 -05:00
Max Radermacher
c1c292b23a Clean up name and some formatting in registration 2026-05-20 16:53:46 -05:00
Max Radermacher
ce9b44ec82
Remove obsolete SVRLocalStorage methods 2026-05-20 16:51:17 -05:00
Sasha Weiss
608bea72f6
Update two KT sheet strings 2026-05-20 14:49:17 -07:00
Max Radermacher
1aadffb78a
Remove getPinType/setPinType 2026-05-20 15:19:09 -05:00
Max Radermacher
25f73ea745 Bump version to 8.14 2026-05-20 14:49:35 -05:00
573 changed files with 15660 additions and 13351 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

@ -1 +1 @@
Xcode 26.4.1
Xcode 26.5

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

@ -7,6 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */; };
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */; };
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */; };
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */; };
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
04127D912F23B3B000B4E95B /* CVCapsuleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */; };
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */; };
041C24ED2DF782AF0065B685 /* OutgoingGroupUpdateMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */; };
@ -67,7 +77,6 @@
046092262FBCD2DA00A8765F /* SafetyTipsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046092232FBCC7E700A8765F /* SafetyTipsManager.swift */; };
046926092E8EBAAE00B1FC74 /* TSInfoMessage+Polls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */; };
0477BE322FA4FC41002F9B47 /* TSReleaseNotesThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */; };
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
0480F0002E57C51A006CBB29 /* BackupsEnabledNotificationMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */; };
0484CECE2F44B7BE009AB2CB /* AdminDeleteRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */; };
0484CED02F44BD00009AB2CB /* AdminDeleteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */; };
@ -83,7 +92,6 @@
04A573702E4D4BD50019651F /* OWSPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A5736F2E4D4BD30019651F /* OWSPoll.swift */; };
04A573722E53A3BF0019651F /* SupportKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A573712E53A3B40019651F /* SupportKeyValueStore.swift */; };
04A573762E75B00B0019651F /* DebugLogPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A573752E75B00A0019651F /* DebugLogPreviewViewController.swift */; };
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */; };
04AB61C62E5E37A800405699 /* PollRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C52E5E37A400405699 /* PollRecord.swift */; };
04AB61C82E5E399700405699 /* PollOptionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C72E5E399400405699 /* PollOptionRecord.swift */; };
04AB61CA2E5E449100405699 /* PollManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C92E5E448A00405699 /* PollManagerTest.swift */; };
@ -525,7 +533,6 @@
45A1684D2A1C308800C2432D /* AudioPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A1684C2A1C308800C2432D /* AudioPresentation.swift */; };
45A2F005204473A3002E978A /* NewMessage.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45A2F004204473A3002E978A /* NewMessage.aifc */; };
45A3579827DAAC6A0051CE8B /* UserProfileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */; };
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */; };
45B3680B2A1D75DF0067D05A /* AudioAllMediaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */; };
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */; };
45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */; };
@ -592,7 +599,6 @@
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; };
4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MessageActionsToolbar.swift */; };
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBBFE492306F5D300B37450 /* LogViewController.swift */; };
4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
4CD675BE22E7BE35008010D2 /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */; };
4CD675C522E7CF22008010D2 /* ConversationViewController+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */; };
@ -718,7 +724,7 @@
50552C2C2BAB8E8500815474 /* AuthCredentialStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50552C2B2BAB8E8500815474 /* AuthCredentialStore.swift */; };
5056B3BF2DEED72800F55320 /* MonotonicDateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5056B3BE2DEED72800F55320 /* MonotonicDateTest.swift */; };
50589CDE2E8C44D5003EF42A /* PreKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */; };
50589CE02E8C4AD5003EF42A /* PreKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKey.swift */; };
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */; };
50597BBA2B97C38C004681E1 /* SignalAccountStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BB92B97C38C004681E1 /* SignalAccountStore.swift */; };
50597BBC2B97C449004681E1 /* UsernameLookupRecordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */; };
50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */; };
@ -850,6 +856,7 @@
50D839512F916A3700EE009A /* MessageRequestDecliner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D839502F916A3700EE009A /* MessageRequestDecliner.swift */; };
50D8796A2A16D2C20031345D /* MessageLoaderBatchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */; };
50D9CD8D2C52D78000273D6C /* StoryRecipientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */; };
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */; };
50DCCBFA2F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */; };
50DCCBFC2F181A790024D124 /* ProfileKeyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */; };
50DCCBFE2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */; };
@ -1016,8 +1023,7 @@
6646573F2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */; };
664657412AC4FB720099DE1C /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657402AC4FB720099DE1C /* NotificationPresenter.swift */; };
664657472ACB66630099DE1C /* TSAccountManagerObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */; };
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */; };
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */; };
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */; };
66485EB92CD17D6400B8613F /* DbRollbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */; };
6649651C2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */; };
6649651E2BDF169F00E2DE98 /* UIImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */; };
@ -1057,7 +1063,7 @@
66586D3829005A1B00DDA9B9 /* story_viewer_onboarding_1.json in Resources */ = {isa = PBXBuildFile; fileRef = 66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */; };
66586D3929005A1B00DDA9B9 /* story_viewer_onboarding_3.json in Resources */ = {isa = PBXBuildFile; fileRef = 66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */; };
66586D4129009C0000DDA9B9 /* TextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66586D4029009C0000DDA9B9 /* TextAttachment.swift */; };
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */; };
6659A0262A7C11A800066AB7 /* PreKeyManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */; };
6659A0282A7C11ED00066AB7 /* MockPreKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */; };
6659A0312A7C5B9700066AB7 /* PreKeyUploadBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */; };
6659A0392A81933B00066AB7 /* ProvisioningPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */; };
@ -1073,7 +1079,6 @@
665FAE8C2A02C0D400FA298D /* SpoilerRevealState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */; };
6660725E2BAB36960084B3D2 /* AttachmentDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */; };
6664B9AB2A314EBD008EF74B /* SpoilerRevealStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */; };
666654212AD0B03F00B23B32 /* MasterKeySyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */; };
66681CDF2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */; };
6671DC872CD44CA8002620EF /* LastVisibleInteractionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6671DC862CD44C9B002620EF /* LastVisibleInteractionStore.swift */; };
66734F012CA1ED3F00558494 /* BackupAttachmentUploadScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66734F002CA1ED3A00558494 /* BackupAttachmentUploadScheduler.swift */; };
@ -1157,7 +1162,6 @@
668B5BFC2C7E46D30018CF36 /* PaletteChatColor+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668B5BFB2C7E46D30018CF36 /* PaletteChatColor+Constants.swift */; };
668CAB3E289983520085A2C3 /* AudioMessagePlaybackRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */; };
668E403C2BE43752004B6730 /* SDAnimatedImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E403B2BE43752004B6730 /* SDAnimatedImage+Attachment.swift */; };
668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09A28B923A4008B9071 /* Bool+SSK.swift */; };
668FE09F28B947ED008B9071 /* StoryContextMenuGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09E28B947ED008B9071 /* StoryContextMenuGenerator.swift */; };
6691E7EF2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */; };
6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */; };
@ -1279,6 +1283,7 @@
66D31DAD2BC48E0100EAF735 /* OWSContactAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */; };
66D31DAF2BC48E3A00EAF735 /* OWSContactName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */; };
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */; };
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */; };
66D709E928E3999400B5013A /* StoryContextAssociatedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */; };
66D7B8FF2B9287F00005C98B /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */; };
66D7B9012B92889E0005C98B /* AttachmentManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */; };
@ -1446,8 +1451,6 @@
729E0B0A2CA4AEB0002EC961 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729E0B082CA4ADE2002EC961 /* Threading.swift */; };
72A132A52CA210C7000ACED6 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A132A42CA210C2000ACED6 /* DarwinNotificationCenter.swift */; };
72A132A72CA25EF0000ACED6 /* SDSCrossProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A132A62CA25EE9000ACED6 /* SDSCrossProcess.swift */; };
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */; };
72B0C2422C9EED0E00B57DAD /* SignedPreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */; };
72B4819D2BD60FDF008B8BA1 /* OWSMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B4819C2BD60FDF008B8BA1 /* OWSMath.swift */; };
72B994DB2BE950DB000CBBFD /* TestAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B994DA2BE950DB000CBBFD /* TestAppContext.swift */; };
72C905892B9A28BF00E586B8 /* Sounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7634F08C2A21963600BB93D5 /* Sounds.swift */; };
@ -1618,7 +1621,6 @@
8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */; };
8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */; };
8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */; };
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A088287F4514000E74A5 /* NewStorySheet.swift */; };
8868A08A287F4551000E74A5 /* InteractiveSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880C2E01262A19DE006650B6 /* InteractiveSheetViewController.swift */; };
8868A08C287F4F81000E74A5 /* OWSTableSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */; };
886BB3D225BA0C9D00079781 /* PreviewWallpaperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ABAB8E25B8BE3F0008C78A /* PreviewWallpaperViewController.swift */; };
@ -1671,7 +1673,7 @@
88A357B923639384009D6B9A /* MemberActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A357B823639384009D6B9A /* MemberActionSheet.swift */; };
88A4CC10246CE2760082211F /* TransferProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A4CC0F246CE2760082211F /* TransferProgressView.swift */; };
88A505F423DA16E10005C012 /* ExperienceUpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINs.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */; };
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A941982409A391000E9700 /* LottieToggleButton.swift */; };
88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */; };
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729322FB4D02004B4FBF /* LocationPicker.swift */; };
@ -2699,6 +2701,7 @@
D92812E22FA95C1400667DCF /* DisplayableAccountEntropyPoolTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92812E12FA95C0D00667DCF /* DisplayableAccountEntropyPoolTest.swift */; };
D92A1CDA2E314BD400C91E21 /* DebugUIPrompts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92A1CD92E314BD000C91E21 /* DebugUIPrompts.swift */; };
D92AB7D829E3BEE30081CA7D /* OWSDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92AB7D729E3BEE30081CA7D /* OWSDeviceManager.swift */; };
D92B55EF2FD0D9210083B070 /* BackupPlanOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */; };
D92C57552A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92C57542A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift */; };
D92CA9EF2F500EA500FDE32D /* LeaveGroupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92CA9EE2F500EA500FDE32D /* LeaveGroupCoordinator.swift */; };
D92CB5562F030F8300537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92CB5552F030F7A00537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift */; };
@ -2739,6 +2742,7 @@
D943F3EF2892F89B008C0C8B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D943F3EE2892F89B008C0C8B /* NSELogger.swift */; };
D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441302D559567005B2A54 /* UUIDv7.swift */; };
D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441322D559C6B005B2A54 /* UUIDv7Test.swift */; };
D944D2FD2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */; };
D945319E2CE53CEB004DAB30 /* SubscriptionRedemptionNecessityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */; };
D94852272F6A224000B130B2 /* GroupCallVideoContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94852262F6A223500B130B2 /* GroupCallVideoContextMenuConfiguration.swift */; };
D9495A6D2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */; };
@ -2878,8 +2882,8 @@
D96869452E1065F5005451E4 /* SeriallyAccessedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96869442E1065F1005451E4 /* SeriallyAccessedState.swift */; };
D968B4982C9E1AD1006B14E1 /* SmsLockIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D968B4972C9E1AC3006B14E1 /* SmsLockIconView.swift */; };
D968F71E2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D968F71D2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift */; };
D9697C162FD78FE400119F72 /* BackupNeverShareRecoveryKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */; };
D96A94A72954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96A94A62954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift */; };
D96BE42E292EF04200E4FE1A /* PaypalButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96BE42D292EF04200E4FE1A /* PaypalButton.swift */; };
D97046062E81D4240034C05D /* InfoMessageGroupUpdateMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97046052E81D41F0034C05D /* InfoMessageGroupUpdateMigrator.swift */; };
D970460A2E81D5C00034C05D /* InfoMessageGroupUpdateMigratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97046092E81D5BB0034C05D /* InfoMessageGroupUpdateMigratorTest.swift */; };
D970541A2CFE49E400AC7954 /* SubscriptionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97054192CFE49E200AC7954 /* SubscriptionFetcher.swift */; };
@ -2894,6 +2898,7 @@
D9791BC42EAADF010016AA5A /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9791BC32EAADEFD0016AA5A /* HTTPResponse.swift */; };
D97992A12D9E55F20080A4F5 /* CurrencyFormatterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992A02D9E55E10080A4F5 /* CurrencyFormatterTest.swift */; };
D97992A32D9E55FB0080A4F5 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992A22D9E55F80080A4F5 /* CurrencyFormatter.swift */; };
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */; };
D979CC262AD3933B006AAC49 /* IndividualCallRecordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC1F2AD3933B006AAC49 /* IndividualCallRecordManager.swift */; };
D979CC292AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC222AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift */; };
D979CC2B2AD3933B006AAC49 /* InteractionStore+CallRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC242AD3933B006AAC49 /* InteractionStore+CallRecord.swift */; };
@ -3172,6 +3177,7 @@
D9AE0AD929187F850063488B /* MessageSenderJobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */; };
D9AE0ADD2918B2960063488B /* JobRecord+Columns.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */; };
D9B0AC7429EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */; };
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */; };
D9B2E1182E748E1900A823E4 /* OWSByteCountFormatStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */; };
D9B8541229137C150058F97B /* JobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B8541129137C150058F97B /* JobRecord.swift */; };
D9B95A9629E6830B00D7CB95 /* JobRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */; };
@ -3818,7 +3824,7 @@
F9C5CCA3289453B300548EEE /* StorageService.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9B8289453B100548EEE /* StorageService.pb.swift */; };
F9C5CCA4289453B300548EEE /* SSKProto+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9B9289453B100548EEE /* SSKProto+OWS.swift */; };
F9C5CCAC289453B300548EEE /* PreKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C2289453B100548EEE /* PreKeyManager.swift */; };
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */; };
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */; };
F9C5CCC0289453B300548EEE /* ContactDiscoveryTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9D9289453B100548EEE /* ContactDiscoveryTask.swift */; };
F9C5CCC3289453B300548EEE /* ContactDiscoveryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9DC289453B100548EEE /* ContactDiscoveryError.swift */; };
F9C5CCC5289453B300548EEE /* SignalAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9DE289453B100548EEE /* SignalAccount.swift */; };
@ -3957,7 +3963,6 @@
F9C5CE29289453B400548EEE /* ModelReadCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB57289453B200548EEE /* ModelReadCache.swift */; };
F9C5CE2A289453B400548EEE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB58289453B200548EEE /* Platform.swift */; };
F9C5CE2B289453B400548EEE /* BuildFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB59289453B200548EEE /* BuildFlags.swift */; };
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */; };
F9C5CE2F289453B400548EEE /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB5D289453B200548EEE /* SwiftSingletons.swift */; };
F9C5CE33289453B400548EEE /* LocalDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB61289453B200548EEE /* LocalDevice.swift */; };
F9C5CE34289453B400548EEE /* AudioWaveformManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB62289453B200548EEE /* AudioWaveformManagerImpl.swift */; };
@ -4154,9 +4159,14 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManager.swift; sourceTree = "<group>"; };
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetcher.swift; sourceTree = "<group>"; };
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementModel.swift; sourceTree = "<group>"; };
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementFetcher.swift; sourceTree = "<group>"; };
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesService.swift; sourceTree = "<group>"; };
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManagerTests.swift; sourceTree = "<group>"; };
04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVCapsuleLabel.swift; sourceTree = "<group>"; };
041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementMegaphone.swift; sourceTree = "<group>"; };
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementReminderMegaphoneTests.swift; sourceTree = "<group>"; };
042223B92EDF30B300158556 /* OutgoingUnpinMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingUnpinMessage.swift; sourceTree = "<group>"; };
0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageIconView.swift; sourceTree = "<group>"; };
0426758F2EC529F500124C5F /* TSInfoMessage+PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+PinnedMessage.swift"; sourceTree = "<group>"; };
@ -4214,7 +4224,6 @@
046092232FBCC7E700A8765F /* SafetyTipsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyTipsManager.swift; sourceTree = "<group>"; };
046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+Polls.swift"; sourceTree = "<group>"; };
0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSReleaseNotesThread.swift; sourceTree = "<group>"; };
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsEnabledNotificationMegaphone.swift; sourceTree = "<group>"; };
0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteRecord.swift; sourceTree = "<group>"; };
0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteManager.swift; sourceTree = "<group>"; };
@ -4740,7 +4749,6 @@
45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTest.swift; sourceTree = "<group>"; };
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalFileBrowserViewController.swift; sourceTree = "<group>"; };
45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioAllMediaPresenter.swift; sourceTree = "<group>"; };
45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "aurora-quiet.aifc"; sourceTree = "<group>"; };
45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "synth-quiet.aifc"; sourceTree = "<group>"; };
@ -4823,7 +4831,6 @@
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = "<group>"; };
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
4CBBFE492306F5D300B37450 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = "<group>"; };
4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+OWS.swift"; sourceTree = "<group>"; };
@ -4983,7 +4990,7 @@
50552C302BAC079A00815474 /* CallLinkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallLinkTest.swift; sourceTree = "<group>"; };
5056B3BE2DEED72800F55320 /* MonotonicDateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonotonicDateTest.swift; sourceTree = "<group>"; };
50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyStore.swift; sourceTree = "<group>"; };
50589CDF2E8C4AD5003EF42A /* PreKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKey.swift; sourceTree = "<group>"; };
50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyRecord.swift; sourceTree = "<group>"; };
50597BB92B97C38C004681E1 /* SignalAccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAccountStore.swift; sourceTree = "<group>"; };
50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameLookupRecordStore.swift; sourceTree = "<group>"; };
50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchableNameFinder.swift; sourceTree = "<group>"; };
@ -5115,6 +5122,7 @@
50D839502F916A3700EE009A /* MessageRequestDecliner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestDecliner.swift; sourceTree = "<group>"; };
50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLoaderBatchTest.swift; sourceTree = "<group>"; };
50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryRecipientManager.swift; sourceTree = "<group>"; };
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrphanedBackupAttachmentTest.swift; sourceTree = "<group>"; };
50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesConfigurationMessage.swift; sourceTree = "<group>"; };
50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileKeyMessage.swift; sourceTree = "<group>"; };
50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingSenderKeyDistributionMessage.swift; sourceTree = "<group>"; };
@ -5286,8 +5294,7 @@
6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRegistrationStateChangeManager.swift; sourceTree = "<group>"; };
664657402AC4FB720099DE1C /* NotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = "<group>"; };
664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAccountManagerObjcBridge.swift; sourceTree = "<group>"; };
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveInternalErrorViewController.swift; sourceTree = "<group>"; };
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorPresenter.swift; sourceTree = "<group>"; };
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorStore.swift; sourceTree = "<group>"; };
66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DbRollbackTests.swift; sourceTree = "<group>"; };
6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Attachment.swift"; sourceTree = "<group>"; };
6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Attachment.swift"; sourceTree = "<group>"; };
@ -5328,7 +5335,7 @@
66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = story_viewer_onboarding_1.json; sourceTree = "<group>"; };
66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = story_viewer_onboarding_3.json; sourceTree = "<group>"; };
66586D4029009C0000DDA9B9 /* TextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttachment.swift; sourceTree = "<group>"; };
6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrekeyManagerImpl.swift; sourceTree = "<group>"; };
6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyManagerImpl.swift; sourceTree = "<group>"; };
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPreKeyManager.swift; sourceTree = "<group>"; };
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyUploadBundle.swift; sourceTree = "<group>"; };
6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisioningPermissionsViewController.swift; sourceTree = "<group>"; };
@ -5344,7 +5351,6 @@
665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealState.swift; sourceTree = "<group>"; };
6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataSource.swift; sourceTree = "<group>"; };
6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealStateTests.swift; sourceTree = "<group>"; };
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterKeySyncManager.swift; sourceTree = "<group>"; };
66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentDownloadStoreTests.swift; sourceTree = "<group>"; };
6671DC862CD44C9B002620EF /* LastVisibleInteractionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastVisibleInteractionStore.swift; sourceTree = "<group>"; };
66734F002CA1ED3A00558494 /* BackupAttachmentUploadScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentUploadScheduler.swift; sourceTree = "<group>"; };
@ -5430,7 +5436,6 @@
668B5BFB2C7E46D30018CF36 /* PaletteChatColor+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaletteChatColor+Constants.swift"; sourceTree = "<group>"; };
668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = "<group>"; };
668E403B2BE43752004B6730 /* SDAnimatedImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDAnimatedImage+Attachment.swift"; sourceTree = "<group>"; };
668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = "<group>"; };
668FE09E28B947ED008B9071 /* StoryContextMenuGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextMenuGenerator.swift; sourceTree = "<group>"; };
6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRequestOWSURLSessionMock.swift; sourceTree = "<group>"; };
6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSessionManagerMock.swift; sourceTree = "<group>"; };
@ -5554,6 +5559,7 @@
66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactAddress.swift; sourceTree = "<group>"; };
66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactName.swift; sourceTree = "<group>"; };
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalListMediaViewController.swift; sourceTree = "<group>"; };
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBackupSettingsViewController.swift; sourceTree = "<group>"; };
66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextAssociatedData.swift; sourceTree = "<group>"; };
66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = "<group>"; };
66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManagerImpl.swift; sourceTree = "<group>"; };
@ -5647,8 +5653,6 @@
729E0B082CA4ADE2002EC961 /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
72A132A42CA210C2000ACED6 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = "<group>"; };
72A132A62CA25EE9000ACED6 /* SDSCrossProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDSCrossProcess.swift; sourceTree = "<group>"; };
72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyRecord.swift; sourceTree = "<group>"; };
72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedPreKeyRecord.swift; sourceTree = "<group>"; };
72B4819C2BD60FDF008B8BA1 /* OWSMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSMath.swift; sourceTree = "<group>"; };
72B994DA2BE950DB000CBBFD /* TestAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppContext.swift; sourceTree = "<group>"; };
72DB95AD2C8C7C7B00FD2266 /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = "<group>"; };
@ -5825,7 +5829,6 @@
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = "<group>"; };
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = "<group>"; };
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = "<group>"; };
8868A088287F4514000E74A5 /* NewStorySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStorySheet.swift; sourceTree = "<group>"; };
8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSTableSheetViewController.swift; sourceTree = "<group>"; };
886A58C8276A760600A1099B /* DonationSubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationSubscriptionManager.swift; sourceTree = "<group>"; };
886A58C9276A760600A1099B /* DonationReceiptCredentialRedemptionJobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationReceiptCredentialRedemptionJobQueue.swift; sourceTree = "<group>"; };
@ -5918,7 +5921,7 @@
88A4717228664DE3001A3065 /* BaseMemberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMemberViewController.swift; sourceTree = "<group>"; };
88A4CC0F246CE2760082211F /* TransferProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferProgressView.swift; sourceTree = "<group>"; };
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeManager.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINs.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINsMegaphone.swift; sourceTree = "<group>"; };
88A695BC232C18DF002F7B9B /* AudioWaveformProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioWaveformProgressView.swift; sourceTree = "<group>"; };
88A941982409A391000E9700 /* LottieToggleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieToggleButton.swift; sourceTree = "<group>"; };
88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentFormatPickerView.swift; sourceTree = "<group>"; };
@ -6977,6 +6980,7 @@
D92812E12FA95C0D00667DCF /* DisplayableAccountEntropyPoolTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayableAccountEntropyPoolTest.swift; sourceTree = "<group>"; };
D92A1CD92E314BD000C91E21 /* DebugUIPrompts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugUIPrompts.swift; sourceTree = "<group>"; };
D92AB7D729E3BEE30081CA7D /* OWSDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSDeviceManager.swift; sourceTree = "<group>"; };
D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPlanOptionView.swift; sourceTree = "<group>"; };
D92C57542A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+DisplayableGroupUpdateItemTest.swift"; sourceTree = "<group>"; };
D92CA9EE2F500EA500FDE32D /* LeaveGroupCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveGroupCoordinator.swift; sourceTree = "<group>"; };
D92CB5552F030F7A00537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSubscriptionAlreadyRedeemedSheet.swift; sourceTree = "<group>"; };
@ -7020,6 +7024,7 @@
D943F3EE2892F89B008C0C8B /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
D94441302D559567005B2A54 /* UUIDv7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7.swift; sourceTree = "<group>"; };
D94441322D559C6B005B2A54 /* UUIDv7Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7Test.swift; sourceTree = "<group>"; };
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSDecryptionPlaceholderExpirationJob.swift; sourceTree = "<group>"; };
D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedemptionNecessityChecker.swift; sourceTree = "<group>"; };
D94852262F6A223500B130B2 /* GroupCallVideoContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoContextMenuConfiguration.swift; sourceTree = "<group>"; };
D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientState.swift; sourceTree = "<group>"; };
@ -7163,9 +7168,9 @@
D96869442E1065F1005451E4 /* SeriallyAccessedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriallyAccessedState.swift; sourceTree = "<group>"; };
D968B4972C9E1AC3006B14E1 /* SmsLockIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsLockIconView.swift; sourceTree = "<group>"; };
D968F71D2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveReleaseNotesRecipientArchiver.swift; sourceTree = "<group>"; };
D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupNeverShareRecoveryKeySheet.swift; sourceTree = "<group>"; };
D96A94A62954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DonateViewController+MonthlyPaypalDonation.swift"; sourceTree = "<group>"; };
D96A94A82955270D004EA434 /* Stripe+Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stripe+Subscriptions.swift"; sourceTree = "<group>"; };
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaypalButton.swift; sourceTree = "<group>"; };
D97046052E81D41F0034C05D /* InfoMessageGroupUpdateMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageGroupUpdateMigrator.swift; sourceTree = "<group>"; };
D97046092E81D5BB0034C05D /* InfoMessageGroupUpdateMigratorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageGroupUpdateMigratorTest.swift; sourceTree = "<group>"; };
D97054192CFE49E200AC7954 /* SubscriptionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFetcher.swift; sourceTree = "<group>"; };
@ -7180,6 +7185,7 @@
D9791BC32EAADEFD0016AA5A /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = "<group>"; };
D97992A02D9E55E10080A4F5 /* CurrencyFormatterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatterTest.swift; sourceTree = "<group>"; };
D97992A22D9E55F80080A4F5 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = "<group>"; };
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeStore.swift; sourceTree = "<group>"; };
D979CC1F2AD3933B006AAC49 /* IndividualCallRecordManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndividualCallRecordManager.swift; sourceTree = "<group>"; };
D979CC202AD3933B006AAC49 /* IncomingCallEventSyncMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncomingCallEventSyncMessageManager.swift; sourceTree = "<group>"; };
D979CC222AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingCallEventSyncMessageManager.swift; sourceTree = "<group>"; };
@ -7461,6 +7467,7 @@
D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderJobRecord.swift; sourceTree = "<group>"; };
D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JobRecord+Columns.swift"; sourceTree = "<group>"; };
D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift"; sourceTree = "<group>"; };
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailIfThrowsRecordCursor.swift; sourceTree = "<group>"; };
D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSByteCountFormatStyle.swift; sourceTree = "<group>"; };
D9B8541129137C150058F97B /* JobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecord.swift; sourceTree = "<group>"; };
D9B91D8D2B17E2A600BCB11A /* GroupCallRecordRingUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallRecordRingUpdateDelegate.swift; sourceTree = "<group>"; };
@ -8133,7 +8140,7 @@
F9C5C9B8289453B100548EEE /* StorageService.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageService.pb.swift; sourceTree = "<group>"; };
F9C5C9B9289453B100548EEE /* SSKProto+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SSKProto+OWS.swift"; sourceTree = "<group>"; };
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreKeyManager.swift; sourceTree = "<group>"; };
F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestation.swift; sourceTree = "<group>"; };
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestationAuthFetcher.swift; sourceTree = "<group>"; };
F9C5C9D9289453B100548EEE /* ContactDiscoveryTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDiscoveryTask.swift; sourceTree = "<group>"; };
F9C5C9DC289453B100548EEE /* ContactDiscoveryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDiscoveryError.swift; sourceTree = "<group>"; };
F9C5C9DE289453B100548EEE /* SignalAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAccount.swift; sourceTree = "<group>"; };
@ -8273,7 +8280,6 @@
F9C5CB57289453B200548EEE /* ModelReadCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelReadCache.swift; sourceTree = "<group>"; };
F9C5CB58289453B200548EEE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
F9C5CB59289453B200548EEE /* BuildFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildFlags.swift; sourceTree = "<group>"; };
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeFinder.swift; sourceTree = "<group>"; };
F9C5CB5D289453B200548EEE /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = "<group>"; };
F9C5CB61289453B200548EEE /* LocalDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDevice.swift; sourceTree = "<group>"; };
F9C5CB62289453B200548EEE /* AudioWaveformManagerImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformManagerImpl.swift; sourceTree = "<group>"; };
@ -8458,6 +8464,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
040507132F80639B0078B769 /* RemoteReleaseNotes */ = {
isa = PBXGroup;
children = (
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */,
);
path = RemoteReleaseNotes;
sourceTree = "<group>";
};
0436E4B12E5E2DC80011E125 /* Polls */ = {
isa = PBXGroup;
children = (
@ -9363,7 +9377,6 @@
340FC87A204DAC8C007AEB0F /* AppSettings */,
8809CE8822F93C0D00D38867 /* Attachment Keyboard */,
883A7FC1269F4BE700841DF9 /* Avatars */,
66485EB12CD03F3300B8613F /* BackupArchive */,
342FFE6C271EF580000AC89F /* Categories */,
F0B872B4269CF01E00D26481 /* ContextMenus */,
34D8C0221ED3673300188D7C /* DebugUI */,
@ -9887,6 +9900,22 @@
path = Debugging;
sourceTree = "<group>";
};
50DAF7E12FD87BFD00BE7430 /* Backups */ = {
isa = PBXGroup;
children = (
50DAF7E22FD87C7000BE7430 /* Attachments */,
);
path = Backups;
sourceTree = "<group>";
};
50DAF7E22FD87C7000BE7430 /* Attachments */ = {
isa = PBXGroup;
children = (
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */,
);
path = Attachments;
sourceTree = "<group>";
};
50E0198E2CC2491A0063EA48 /* Concurrency */ = {
isa = PBXGroup;
children = (
@ -10208,15 +10237,6 @@
path = DoubleTapToEdit;
sourceTree = "<group>";
};
6640DD612ACDD5CD00CE9A8C /* LocalStorage */ = {
isa = PBXGroup;
children = (
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
);
path = LocalStorage;
sourceTree = "<group>";
};
6645F30629BF8D1000B58EBD /* AccountAttributes */ = {
isa = PBXGroup;
children = (
@ -10250,14 +10270,6 @@
path = RegistrationStateChangeManager;
sourceTree = "<group>";
};
66485EB12CD03F3300B8613F /* BackupArchive */ = {
isa = PBXGroup;
children = (
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */,
);
path = BackupArchive;
sourceTree = "<group>";
};
6649651A2BDC6E8D00E2DE98 /* Playback */ = {
isa = PBXGroup;
children = (
@ -10299,20 +10311,6 @@
path = WhoAmI;
sourceTree = "<group>";
};
6659A0242A7C112700066AB7 /* PreKeys */ = {
isa = PBXGroup;
children = (
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */,
6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */,
C17345BA2A5E000300C6426D /* PreKeyTarget.swift */,
D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */,
C1ED5CA02A72E3D5009AD3FC /* PreKeyTaskManager.swift */,
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */,
);
path = PreKeys;
sourceTree = "<group>";
};
6659A02D2A7C171900066AB7 /* PreKeys */ = {
isa = PBXGroup;
children = (
@ -10355,15 +10353,20 @@
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */ = {
isa = PBXGroup;
children = (
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */,
6640DD612ACDD5CD00CE9A8C /* LocalStorage */,
66C2B13B2A0E9108008DDE72 /* SVR2 */,
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */,
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
66138FB5298326C7002E0CFE /* SecureValueRecovery.swift */,
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
66C2B1372A0DB6A9008DDE72 /* SVRAuthCredential.swift */,
6673FF6F2978C40300F96CFD /* SVRAuthCredentialStorage.swift */,
6673FF712979B33800F96CFD /* SVRAuthCredentialStorageImpl.swift */,
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
66C2B1352A0DB02E008DDE72 /* SVRUtil.swift */,
);
path = SecureValueRecovery;
@ -10706,18 +10709,6 @@
path = V2;
sourceTree = "<group>";
};
66C2B13B2A0E9108008DDE72 /* SVR2 */ = {
isa = PBXGroup;
children = (
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
);
path = SVR2;
sourceTree = "<group>";
};
66C2B1422A12E043008DDE72 /* SecureValueRecovery */ = {
isa = PBXGroup;
children = (
@ -10727,25 +10718,6 @@
path = SecureValueRecovery;
sourceTree = "<group>";
};
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */ = {
isa = PBXGroup;
children = (
66C2B14E2A13F0BC008DDE72 /* Mocks */,
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
);
path = SgxWebsocketConnection;
sourceTree = "<group>";
};
66C2B14E2A13F0BC008DDE72 /* Mocks */ = {
isa = PBXGroup;
children = (
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
66CD25572B0685CF00139E17 /* Archivers */ = {
isa = PBXGroup;
children = (
@ -11429,12 +11401,11 @@
isa = PBXGroup;
children = (
344A761024B366F4009D69A5 /* FlagsViewController.swift */,
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */,
665229882E218D53002C14A0 /* InternalDiskUsageViewController.swift */,
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */,
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */,
8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */,
663883562D4C034F008EA898 /* InternalSQLClientViewController.swift */,
4CBBFE492306F5D300B37450 /* LogViewController.swift */,
344A761224B36C8C009D69A5 /* TestingViewController.swift */,
);
path = Internal;
@ -11468,7 +11439,10 @@
children = (
88A505FE23DBAE640005C012 /* UserInterface */,
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */,
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */,
D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */,
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */,
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */,
);
path = Megaphones;
sourceTree = "<group>";
@ -11482,7 +11456,7 @@
D9C2D77F299EC11400D79715 /* CreateUsernameMegaphone.swift */,
D9C0AE682BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift */,
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */,
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */,
8837F74023DA0B0F00772A32 /* MegaphoneView.swift */,
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */,
8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */,
@ -11639,6 +11613,7 @@
88E34F2522F269B600966CC2 /* StorageService */ = {
isa = PBXGroup;
children = (
F9C5CB12289453B200548EEE /* StorageService.swift */,
88E34F2622F269E900966CC2 /* StorageServiceManager.swift */,
88E34F2822F26CC100966CC2 /* StorageServiceProto+Sync.swift */,
D927372C2CD2DD0D00E15D95 /* StorageServiceRecordIkmMigrator.swift */,
@ -11665,6 +11640,7 @@
D99ABC712A3D0BAA0034CD3B /* QRCodes */,
50791B1B2D037A7800D747F8 /* RecipientPickers */,
661278052996BA6700A1D5A1 /* Registration */,
040507132F80639B0078B769 /* RemoteReleaseNotes */,
4C3EF8002109184A0007EBF7 /* SSKTests */,
D97046082E81D5B60034C05D /* Storage */,
E75DD3DC2810CD3500E32C36 /* subscriptions */,
@ -11814,7 +11790,6 @@
88F5D78B2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift */,
88F5D7892880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift */,
880FB3F228CC161800FA1C10 /* NewStoryHeaderView.swift */,
8868A088287F4514000E74A5 /* NewStorySheet.swift */,
66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */,
B99B155C2A71BA5200E26DAC /* StoryContextViewState.swift */,
88B6D67128076F37005D86EC /* StoryMessage+SignalUI.swift */,
@ -13368,9 +13343,11 @@
isa = PBXGroup;
children = (
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */,
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */,
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */,
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */,
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */,
D98DD85E28EE53B00089333E /* RemoteMegaphoneModel.swift */,
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */,
);
path = Megaphones;
sourceTree = "<group>";
@ -13545,6 +13522,8 @@
D97C9FF12DD3FB7200191CE2 /* BackupDisablingManager.swift */,
D93FA5BE2DE77E440013879E /* BackupEnablingManager.swift */,
D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */,
D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */,
D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */,
D98CA2B22DF2450E0060370E /* BackupRecordKeyViewController.swift */,
04E66D412DFF3A3E0059DBAC /* BackupRecoveryKeyReminderCoordinator.swift */,
04B975452E43A4AA00E20364 /* BackupRefreshManager.swift */,
@ -13689,7 +13668,7 @@
665C0D5F2ADF57D000539A37 /* BackupArchive+Shims.swift */,
661429182D35B9EA0043AA22 /* BackupArchive+Timestamp.swift */,
04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */,
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */,
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */,
66232AE02CC0271F00AE6A76 /* BackupArchiveFullTextSearchIndexer.swift */,
665C0D5B2ADF538100539A37 /* BackupArchiveManager.swift */,
665C0D5D2ADF53E200539A37 /* BackupArchiveManagerImpl.swift */,
@ -13931,8 +13910,6 @@
children = (
D90AA32E2CC9616A00021CB0 /* Signal-Message-Backup-Tests */,
D90AA6182CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift */,
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */,
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */,
04E66D432E00AB3A0059DBAC /* BackupSettingsStoreTests.swift */,
D9A36B922C7FEDA100CEC0E7 /* LineByLineStringDiff.swift */,
);
@ -14132,15 +14109,6 @@
path = DisappearingMessages;
sourceTree = "<group>";
};
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */ = {
isa = PBXGroup;
children = (
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
);
path = AccountEntropyPool;
sourceTree = "<group>";
};
D9F6553029D6530B002A330A /* SDSCodableModel */ = {
isa = PBXGroup;
children = (
@ -14611,7 +14579,6 @@
F900F2DC27F25AB300431E09 /* DonationReceiptViewController.swift */,
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */,
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */,
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */,
);
path = Donations;
sourceTree = "<group>";
@ -14623,6 +14590,7 @@
D92EFDEB2F68EB7D0031D257 /* AttachmentBackfill */,
7255A4C32B98D5A800E95368 /* Attachments */,
720547F12B9C8F5E00E2CF2F /* Avatars */,
F9C5CA52289453B100548EEE /* Axolotl */,
665C0D5A2ADF537000539A37 /* Backups */,
F945FE482984795A00C835C7 /* Calls */,
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
@ -14656,7 +14624,6 @@
046092252FBCD28300A8765F /* SafetyTips */,
50B791552E8B39230063E71E /* Search */,
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
F9C5CB98289453B200548EEE /* Security */,
F9C5CAB4289453B200548EEE /* Spam */,
F9C5CA2F289453B100548EEE /* Storage */,
88E34F2522F269B600966CC2 /* StorageService */,
@ -14684,6 +14651,7 @@
F94261FF289B1B5400460798 /* Account */,
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
50ED28002F0EDAFB00E57C54 /* Attachments */,
50DAF7E12FD87BFD00BE7430 /* Backups */,
F945FE4B298481D800C835C7 /* Calls */,
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
50E0198E2CC2491A0063EA48 /* Concurrency */,
@ -14772,6 +14740,7 @@
F9C5C950289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage+SDS.swift */,
F9C5C997289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.h */,
F9C5C958289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.m */,
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */,
F9C5C93B289453B100548EEE /* OWSIdentityManager.swift */,
F9C5C983289453B100548EEE /* OWSMessageDecrypter.swift */,
F9C5C973289453B100548EEE /* OWSMessageSend.swift */,
@ -15057,13 +15026,16 @@
isa = PBXGroup;
children = (
6646572F2AC369EB0099DE1C /* PhoneNumberDiscoverabilityManager */,
6659A0242A7C112700066AB7 /* PreKeys */,
661170BF2ABA458800A1B16D /* TSAccountManager */,
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
50F401CB2D483BF40094CA56 /* DeviceId.swift */,
50D6BDEE2ED6724600CC012E /* DeviceType.swift */,
D9F399AC2A95798A001599EC /* IdentityKeyChecker.swift */,
D9F399B12A96D65D001599EC /* IdentityKeyMismatchManager.swift */,
5033D46629D76BD0007FEADA /* LocalIdentifiers.swift */,
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
72552EF32C9EF9E7008614AF /* OWSIdentity.swift */,
D9CAF74F2A0ACFF20049193A /* PniDistributionParameterBuilder.swift */,
C18E3C712A9FF65D003D1CF1 /* PniDistributionSyncMessage.swift */,
@ -15192,7 +15164,6 @@
F9C5CA2F289453B100548EEE /* Storage */ = {
isa = PBXGroup;
children = (
F9C5CA52289453B100548EEE /* AxolotlStore */,
F9C5CA31289453B100548EEE /* Database */,
667DEE562BC7148E00EFF32D /* MediaGallery */,
F9C5CA9B289453B100548EEE /* BaseModel.h */,
@ -15222,6 +15193,7 @@
F9B652C228D8E3DF006914CA /* DatabaseRecovery.swift */,
D9FF515B2F03A2A10011982F /* DBUInt64.swift */,
F9C5CA48289453B100548EEE /* DeepCopy.swift */,
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */,
F9C5CA40289453B100548EEE /* GRDBDatabaseStorageAdapter.swift */,
F9C5CA47289453B100548EEE /* GRDBSchemaMigrator.swift */,
D9B95A9929E8918200D7CB95 /* InMemoryDB.swift */,
@ -15260,32 +15232,33 @@
path = Snapshots;
sourceTree = "<group>";
};
F9C5CA52289453B100548EEE /* AxolotlStore */ = {
F9C5CA52289453B100548EEE /* Axolotl */ = {
isa = PBXGroup;
children = (
F9C5CA5F289453B100548EEE /* Model */,
667664352A43BBCD00716B84 /* CombinedFingerprints.swift */,
50A156C62FA11AA8008FE086 /* Fingerprint.swift */,
C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */,
504F98B02EAFFAC600DF465B /* KyberPreKeyUseRecord.swift */,
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
F9C5CA59289453B100548EEE /* OldSenderKeyStore.swift */,
50589CDF2E8C4AD5003EF42A /* PreKey.swift */,
F9C5CB9F289453B200548EEE /* OWSRecipientIdentity.swift */,
F9C5CB99289453B200548EEE /* OWSVerificationState.h */,
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.swift */,
5050A8782B76E2E100E9BFA4 /* PreKeyId.swift */,
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */,
6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */,
50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */,
50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */,
F9C5CA75289453B100548EEE /* PreKeyStoreImpl.swift */,
C17345BA2A5E000300C6426D /* PreKeyTarget.swift */,
D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */,
C1ED5CA02A72E3D5009AD3FC /* PreKeyTaskManager.swift */,
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */,
501050BA2EB959A4005161CA /* SessionStore.swift */,
F9C5CA56289453B100548EEE /* SignalProtocolStore.swift */,
F9C5CA55289453B100548EEE /* SignedPreKeyStoreImpl.swift */,
);
path = AxolotlStore;
sourceTree = "<group>";
};
F9C5CA5F289453B100548EEE /* Model */ = {
isa = PBXGroup;
children = (
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.swift */,
72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */,
72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */,
);
path = Model;
path = Axolotl;
sourceTree = "<group>";
};
F9C5CA85289453B100548EEE /* JobRecords */ = {
@ -15332,13 +15305,14 @@
isa = PBXGroup;
children = (
F9C5CAD3289453B200548EEE /* API */,
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */,
88DF819328E112F600F8BA80 /* SignalProxy */,
669E8FE528B4149200043D28 /* BaseOWSURLSessionMock.swift */,
727328062CA6CF530080E2C7 /* Certificates.swift */,
F9C5CAC4289453B200548EEE /* ChatConnectionManager.swift */,
509A8DC12E25817E0024BF14 /* ConnectionLock.swift */,
F9C5CAF7289453B200548EEE /* ContentProxy.swift */,
F9C5CAF2289453B200548EEE /* HttpHeaders.swift */,
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
F9C5CAF1289453B200548EEE /* NetworkInterfaceSet.swift */,
F9C5CAC8289453B200548EEE /* OutageDetection.swift */,
72328C8A2C6C7322000EA728 /* OWSCensorshipConfiguration.swift */,
@ -15415,7 +15389,7 @@
F9D5BFCC2979A017001737E5 /* OWSRequestFactory+Spam.swift */,
D95C39E7296DEBFB00A9DA23 /* OWSRequestFactory+Usernames.swift */,
F9C5CAE2289453B200548EEE /* OWSRequestFactory.swift */,
F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */,
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */,
66C2B1302A05D28A008DDE72 /* TSRequest.swift */,
);
path = Requests;
@ -15436,7 +15410,6 @@
058B49922C66804B00307D38 /* AVAssetExportSession+Async.swift */,
F9C5CB64289453B200548EEE /* Batching.swift */,
F9C5CB40289453B200548EEE /* Bench.swift */,
668FE09A28B923A4008B9071 /* Bool+SSK.swift */,
E7D7C93E28B580AC003F043B /* Bundle+OWS.swift */,
88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */,
76387BEF28F4ED73002C7BA5 /* CaseIterable.swift */,
@ -15518,7 +15491,6 @@
7634F08C2A21963600BB93D5 /* Sounds.swift */,
F9613CDB2981F11400894B55 /* SqliteUtil.swift */,
F9C5CB47289453B200548EEE /* SSKPreferences.swift */,
F9C5CB12289453B200548EEE /* StorageService.swift */,
72DB95AD2C8C7C7B00FD2266 /* String+OWS.swift */,
F9C5CB09289453B200548EEE /* String+SSK.swift */,
668A010A2C2B602F007B8808 /* StringSanitizer.swift */,
@ -15566,19 +15538,6 @@
path = TestUtils;
sourceTree = "<group>";
};
F9C5CB98289453B200548EEE /* Security */ = {
isa = PBXGroup;
children = (
727328062CA6CF530080E2C7 /* Certificates.swift */,
667664352A43BBCD00716B84 /* CombinedFingerprints.swift */,
50A156C62FA11AA8008FE086 /* Fingerprint.swift */,
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
F9C5CB9F289453B200548EEE /* OWSRecipientIdentity.swift */,
F9C5CB99289453B200548EEE /* OWSVerificationState.h */,
);
path = Security;
sourceTree = "<group>";
};
F9C5CBA3289453B200548EEE /* Groups */ = {
isa = PBXGroup;
children = (
@ -17876,7 +17835,6 @@
88F5D78C2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift in Sources */,
88F5D78A2880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift in Sources */,
880FB3F328CC161800FA1C10 /* NewStoryHeaderView.swift in Sources */,
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */,
3402AAAC271D9E180084CBAE /* NonContactTableViewCell.swift in Sources */,
507C07402F116E9200ECFEFA /* NormalizedImage.swift in Sources */,
3402AAAB271D9E180084CBAE /* OWSActionSheets.swift in Sources */,
@ -18093,7 +18051,6 @@
4C2F454F214C00E1004871FF /* AvatarTableViewCell.swift in Sources */,
32C584A825B81C6600256804 /* AvatarViewController.swift in Sources */,
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */,
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */,
D932C0EB2E13AD3F00FEF9C3 /* BackupAttachmentDownloadTracker.swift in Sources */,
D93964B62E038C7B00094117 /* BackupAttachmentUploadTracker.swift in Sources */,
66A1F4E62E03641D0095DE4B /* BackupBGProcessingTaskRunner.swift in Sources */,
@ -18102,9 +18059,11 @@
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */,
D9DF21EC2E21BD6600A962B2 /* BackupEnablingManager.swift in Sources */,
D93BDD942E43064500779BD8 /* BackupKeepKeySafeSheet.swift in Sources */,
D9697C162FD78FE400119F72 /* BackupNeverShareRecoveryKeySheet.swift in Sources */,
D999345A2DE97BBC002C9196 /* BackupOnboardingCoordinator.swift in Sources */,
D9DE34FD2DEE7765005099D7 /* BackupOnboardingIntroViewController.swift in Sources */,
D98CA2AD2DF14A890060370E /* BackupOnboardingKeyIntroViewController.swift in Sources */,
D92B55EF2FD0D9210083B070 /* BackupPlanOptionView.swift in Sources */,
D98CA2B32DF245140060370E /* BackupRecordKeyViewController.swift in Sources */,
04E66D422DFF3A4B0059DBAC /* BackupRecoveryKeyReminderCoordinator.swift in Sources */,
50438A8E2ECBBDF600FCB28F /* BackupRefreshManager.swift in Sources */,
@ -18490,12 +18449,12 @@
D9E43C072CC194140001536E /* IndividualCallViewController.swift in Sources */,
D97046062E81D4240034C05D /* InfoMessageGroupUpdateMigrator.swift in Sources */,
88BCCC8123837B7D00CE5FE6 /* InteractionReactionState.swift in Sources */,
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */,
665229892E218D5F002C14A0 /* InternalDiskUsageViewController.swift in Sources */,
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */,
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */,
8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */,
663883572D4C0360008EA898 /* InternalSQLClientViewController.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */,
32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */,
45C845AD291466C0005F6EA5 /* JournalingOrderedDictionary.swift in Sources */,
5045F44229E0DB7100058E5F /* LaunchJobs.swift in Sources */,
@ -18515,7 +18474,6 @@
4C25768A23AD510800E0398D /* LoadMoreMessagesView.swift in Sources */,
D9E43C082CC194140001536E /* LocalVideoView.swift in Sources */,
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */,
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */,
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */,
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */,
5033D46929D7951F007FEADA /* MainAppContext.swift in Sources */,
@ -18611,7 +18569,6 @@
3495FF0525F9091400959D6E /* PaymentsViewPassphraseGridViewController.swift in Sources */,
3495FF0F25F9538900959D6E /* PaymentsViewPassphraseSplashViewController.swift in Sources */,
34FB6A5325D2D10400E599B1 /* PaymentsViewUtils.swift in Sources */,
D96BE42E292EF04200E4FE1A /* PaypalButton.swift in Sources */,
667AF9DE2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift in Sources */,
C176B48A299DA25500B1900D /* PhoneNumberPrivacySettingsViewController.swift in Sources */,
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */,
@ -18702,9 +18659,12 @@
F9E3006C299D76C3000323F8 /* RegistrationVerificationViewController.swift in Sources */,
F95D71A3299305C400ED3102 /* RegistrationViewUtil.swift in Sources */,
50EA40912E3A899F009CB839 /* RegistrationWebSocketManager.swift in Sources */,
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
D997FA7628F8E3A2003C7B8B /* RemoteMegaphone.swift in Sources */,
509DC8DA2BCED88600375E86 /* RemoteMegaphoneFetcher.swift in Sources */,
55B753602D97304100CCC91C /* RemoteMuteToast.swift in Sources */,
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
D9E43C0D2CC194140001536E /* RemoteVideoView.swift in Sources */,
348433DF243CA94600C7F64A /* ReplaceAdminViewController.swift in Sources */,
F952C0A629C8DA5E00D93766 /* RequestAccountDataReportViewController.swift in Sources */,
@ -18884,6 +18844,11 @@
E16B440E2BBF242C00D2583E /* ReactionsModelTest.swift in Sources */,
661278082996BA8900A1D5A1 /* RegistrationCoordinatorTest.swift in Sources */,
6612780D2996BD0300A1D5A1 /* RegistrationCoordinatorTestShims.swift in Sources */,
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */,
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */,
F5C80FA22BE3F29F0028F76D /* RTCIceServerFetcherTest.swift in Sources */,
F963164B291AE06C00218FB7 /* ScrubbingLogFormatterTest.swift in Sources */,
505C2ED92997422D00C23FB2 /* SelfSignedIdentityTest.swift in Sources */,
@ -19019,7 +18984,7 @@
66F6D69E2C77E4C500EFAF75 /* BackupArchiveContactAttachmentArchiver.swift in Sources */,
66CD256E2B06E14F00139E17 /* BackupArchiveContactRecipientArchiver.swift in Sources */,
C1CA5F8E2BE2F21C00D733CA /* BackupArchiveDistributionListRecipientArchiver.swift in Sources */,
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */,
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */,
D91D9C8C2C3F06400009E4F7 /* BackupArchiveExpirationTimerChatUpdateArchiver.swift in Sources */,
66232AE12CC0272900AE6A76 /* BackupArchiveFullTextSearchIndexer.swift in Sources */,
D9A85DC22BE1719C003F7045 /* BackupArchiveGroupCallArchiver.swift in Sources */,
@ -19112,7 +19077,6 @@
50F039C42C6D239500162B99 /* BlockedRecipientStore.swift in Sources */,
F9C5CC31289453B300548EEE /* BlockingManager.swift in Sources */,
F9C5CC74289453B300548EEE /* BlurHash.swift in Sources */,
668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */,
505F76332BC45C0700B1B51C /* BuildFlags+Generated.swift in Sources */,
F9C5CE2B289453B400548EEE /* BuildFlags.swift in Sources */,
D9F9A63B2BFFFCC400EF13EC /* BulkDeleteInteractionJobQueue.swift in Sources */,
@ -19286,13 +19250,14 @@
F9C5CE44289453B400548EEE /* Error+IsRetryable.swift in Sources */,
F9C5CE23289453B400548EEE /* Error+SSK.swift in Sources */,
D9C7CEB428EB8495001E87B6 /* ExperienceUpgrade.swift in Sources */,
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */,
D9C7CECB28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift in Sources */,
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */,
D98BC5332EE387A30052A81F /* ExpirationJob.swift in Sources */,
C1FB9B752B16498C00D51A3B /* ExternalPendingDonationStore.swift in Sources */,
F9C5CE57289453B400548EEE /* Factories.swift in Sources */,
F9C5CC1D289453B300548EEE /* FailedMessagesJob.swift in Sources */,
7255A4C82B98DF3E00E95368 /* FailedStorySendDisplayController.swift in Sources */,
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */,
F9C5CE60289453B400548EEE /* FakeContactsManager.swift in Sources */,
F94BFA9528EBB0D800A5F34E /* FakeMessageSender.swift in Sources */,
F9C5CE54289453B400548EEE /* FakeStorageServiceManager.swift in Sources */,
@ -19416,7 +19381,6 @@
F9C5CDF6289453B400548EEE /* LRUCache.swift in Sources */,
F9C5CDE3289453B400548EEE /* MailtoLink.swift in Sources */,
D94AEB3A2D28837F00B03D7A /* MasterKey.swift in Sources */,
666654212AD0B03F00B23B32 /* MasterKeySyncManager.swift in Sources */,
F9C5CE08289453B400548EEE /* Math+OWS.swift in Sources */,
66BED7E32B9B8FDF00236BAD /* MediaBandwidthPreferenceStore.swift in Sources */,
66BED7E62B9B929600236BAD /* MediaBandwidthPreferenceStoreImpl.swift in Sources */,
@ -19581,6 +19545,7 @@
66D31DA92BC48D7900EAF735 /* OWSContactPhoneNumber.swift in Sources */,
725465192BA00F7500EABFD2 /* OWSContactsManager.swift in Sources */,
72328C892C6C6733000EA728 /* OWSCountryMetadata.swift in Sources */,
D944D2FD2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift in Sources */,
F9C5CCFD289453B300548EEE /* OWSDevice.swift in Sources */,
D92AB7D829E3BEE30081CA7D /* OWSDeviceManager.swift in Sources */,
F9C5CE0C289453B400548EEE /* OWSDeviceNames.swift in Sources */,
@ -19703,12 +19668,11 @@
500876142BF7B32A00D6F615 /* Preconditions.swift in Sources */,
7255A4D42B98E36900E95368 /* Preferences.swift in Sources */,
D95C39EC296E1BC600A9DA23 /* PrefixedLogger.swift in Sources */,
50589CE02E8C4AD5003EF42A /* PreKey.swift in Sources */,
5010B6B42C6BD41E00314CD4 /* PreKeyBundle.swift in Sources */,
5050A8792B76E2E100E9BFA4 /* PreKeyId.swift in Sources */,
F9C5CCAC289453B300548EEE /* PreKeyManager.swift in Sources */,
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */,
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */,
6659A0262A7C11A800066AB7 /* PreKeyManagerImpl.swift in Sources */,
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */,
50589CDE2E8C44D5003EF42A /* PreKeyStore.swift in Sources */,
F9C5CD52289453B300548EEE /* PreKeyStoreImpl.swift in Sources */,
C17345BB2A5E000300C6426D /* PreKeyTarget.swift in Sources */,
@ -19781,9 +19745,11 @@
6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */,
6646573B2AC388C70099DE1C /* RegistrationStateChangeManager.swift in Sources */,
6646573D2AC3894D0099DE1C /* RegistrationStateChangeManagerImpl.swift in Sources */,
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */,
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */,
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */,
F9C5CE17289453B400548EEE /* RemoteConfigManager.swift in Sources */,
D98DD86028EE53B00089333E /* RemoteMegaphoneModel.swift in Sources */,
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */,
5063B41E2C5432A30041CA51 /* ResolvableValue.swift in Sources */,
502C69742B06F0A400012867 /* Result.swift in Sources */,
50C0203E2CA4A7A500BDC4EF /* Retry.swift in Sources */,
@ -19858,7 +19824,6 @@
F9C5CC9A289453B300548EEE /* SignalService.pb.swift in Sources */,
F9C5CCE2289453B300548EEE /* SignalServiceAddress.swift in Sources */,
F9C5CDBB289453B400548EEE /* SignalServiceProfile.swift in Sources */,
72B0C2422C9EED0E00B57DAD /* SignedPreKeyRecord.swift in Sources */,
F9C5CD33289453B300548EEE /* SignedPreKeyStoreImpl.swift in Sources */,
F9C5CC51289453B300548EEE /* SMKError.swift in Sources */,
F9C5CC53289453B300548EEE /* SMKSecretSessionCipher.swift in Sources */,
@ -20129,8 +20094,6 @@
D90AA6192CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift in Sources */,
66681CDF2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift in Sources */,
66C795302C9B83A200C13937 /* BackupAttachmentUploadStoreTests.swift in Sources */,
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */,
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */,
66A1F4EB2E07CEA50095DE4B /* BackupListMediaManagerTests.swift in Sources */,
04E66D452E00AB6A0059DBAC /* BackupSettingsStoreTests.swift in Sources */,
F9426283289B1B5600460798 /* BlockingManagerTests.swift in Sources */,
@ -20227,6 +20190,7 @@
D979CC3A2AD3964E006AAC49 /* Numbers+Random.swift in Sources */,
D95E149D2E3D22FD00B5B70B /* ObjectRetainerTest.swift in Sources */,
663D02DF2C069AB600350632 /* OrphanedAttachmentCleanerTest.swift in Sources */,
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */,
D9AA37A02A86E0910088EFFB /* OutgoingCallEventSyncMessageTest.swift in Sources */,
D925C7BB2B7BEC0F00AC73B0 /* OutgoingCallLogEventSyncMessageTest.swift in Sources */,
D9D3216A2A8AC9B0004FC110 /* OutgoingGroupCallUpdateMessageTest.swift in Sources */,

View File

@ -87,7 +87,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
appReadiness.runNowOrWhenAppDidBecomeReadySync {
self.refreshConnection(isAppActive: false, shouldRunCron: false)
self.refreshConnection(isAppActive: false)
}
clearAppropriateNotificationsAndRestoreBadgeCount()
@ -148,9 +148,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
debugLogger.enableFileLogging(appContext: mainAppContext, canLaunchInBackground: true)
DebugLogger.configureSwiftLogging()
if DebugFlags.audibleErrorLogging {
debugLogger.enableErrorReporting()
}
Logger.warn("Launching…")
defer { Logger.info("Launched.") }
@ -372,13 +369,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
private lazy var screenLockUI = ScreenLockUI(appReadiness: appReadiness)
private func configureGlobalUI(in window: UIWindow) {
let screenLockUI = AppEnvironment.shared.screenLockUI
let windowManager = AppEnvironment.shared.windowManagerRef
Theme.setupSignalAppearance()
screenLockUI.setupWithRootWindow(window)
AppEnvironment.shared.windowManagerRef.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
windowManager.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
screenLockUI.startObserving()
}
@ -400,7 +398,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
let dataMigrationContinuation = globalsContinuation.initGlobals(
appContext: launchContext.appContext,
appReadiness: appReadiness,
backupArchiveErrorPresenterFactory: BackupArchiveErrorPresenterFactoryInternal(),
deviceBatteryLevelManager: DeviceBatteryLevelManagerImpl(),
deviceSleepManager: launchContext.deviceSleepManager,
paymentsEvents: PaymentsEventsMainApp(),
@ -646,16 +643,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
let remoteMegaphoneFetcher = RemoteMegaphoneFetcher(
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
signalService: SSKEnvironment.shared.signalServiceRef,
let remoteReleaseNotesFetchingManager = RemoteReleaseNotesFetchingManager(
db: DependenciesBridge.shared.db,
remoteReleaseNotesService: DependenciesBridge.shared.remoteReleaseNotesService,
)
cron.schedulePeriodically(
uniqueKey: .fetchMegaphones,
approximateInterval: 3 * .day,
mustBeRegistered: false,
mustBeConnected: true,
operation: { try await remoteMegaphoneFetcher.syncRemoteMegaphones() },
operation: { try await remoteReleaseNotesFetchingManager.syncRemoteReleaseNotes() },
)
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
@ -720,6 +717,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// element" should call .restart() on the appropriate job.
dependenciesBridge.deletedCallRecordExpirationJob.start()
dependenciesBridge.disappearingMessagesExpirationJob.start()
dependenciesBridge.decryptionPlaceholderExpirationJob.start()
dependenciesBridge.storyMessageExpirationJob.start()
dependenciesBridge.pinnedMessageExpirationJob.start()
@ -781,6 +779,27 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
operation: { try await blockingManager.syncBlockListIfNecessary(force: false) },
)
let svr = DependenciesBridge.shared.svr
// We must refresh our SVR2 credentials periodically. We typically do this
// when updating to a new version, but we want to refresh it after 14 days
// if we haven't upgraded.
cron.schedulePeriodically(
uniqueKey: .refreshSVRCredentials,
approximateInterval: 14 * .day,
mustBeRegistered: true,
mustBeDeviceType: .primary,
mustBeConnected: true,
operation: { try await svr.refreshCredentialsIfNecessary() },
)
cron.scheduleFrequently(
mustBeRegistered: true,
mustBeDeviceType: .primary,
mustBeConnected: true,
operation: { try await svr.refreshBackupIfNecessary() },
)
// Warm the "available emoji" cache, intentionally off the main thread.
Task.detached {
Emoji.warmAvailableCache()
@ -790,7 +809,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// launching from the background, without this, we end up waiting some extra
// seconds before receiving an actionable push notification.
if !appContext.isMainAppAndActive {
self.refreshConnection(isAppActive: false, shouldRunCron: false)
self.refreshConnection(isAppActive: false)
}
if registeredState != nil {
@ -1233,14 +1252,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
switch action {
case .submitDebugLogsAndCrash:
addSubmitDebugLogsAction {
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
DebugLogs(dumper: logDumper).promptToSubmitLogs(
from: viewController,
supportTag: supportTag,
) {
owsFail("Exiting after submitting debug logs")
}
}
case .submitDebugLogsAndLaunchApp(let window, let launchContext):
addSubmitDebugLogsAction { [unowned window] in
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
DebugLogs(dumper: logDumper).promptToSubmitLogs(
from: viewController,
supportTag: supportTag,
) {
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
}
}
@ -1367,7 +1392,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
refreshConnection(isAppActive: true, shouldRunCron: true)
refreshConnection(isAppActive: true)
// Every time we become active...
if registeredState != nil {
@ -1435,7 +1460,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
/// is in the background.
private var backgroundFetchHandle: BackgroundTaskHandle?
private func refreshConnection(isAppActive: Bool, shouldRunCron: Bool) {
private func refreshConnection(isAppActive: Bool) {
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
let oldActiveConnectionTokens = self.activeConnectionTokens
@ -1443,9 +1468,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// If we're active, open a connection.
self.activeConnectionTokens = chatConnectionManager.requestConnections()
oldActiveConnectionTokens.forEach { $0.releaseConnection() }
if shouldRunCron {
self.startCronTask()
}
// Start a new Cron task on activate.
self.startCronTask()
// We're back in the foreground. We've passed off connection management to
// the foreground logic, so just tear it down without waiting for anything.
self.backgroundFetchHandle?.interrupt()
@ -1462,17 +1488,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
do {
await backgroundFetcher.start()
oldActiveConnectionTokens.forEach { $0.releaseConnection() }
// If there's a Cron task running that was started in the foreground, wait
// for it to finish.
await withTaskCancellationHandler(
operation: { await cronTask?.value },
onCancel: { cronTask?.cancel() },
)
// If there's a fresh request to run Cron when entering the background,
// start a new Cron instance.
if shouldRunCron {
await self.runCron()
}
// This will usually be limited to 30 seconds rather than 3 minutes.
let waitDeadline = startDate.adding(180)
if isPastRegistration {
@ -1745,8 +1768,24 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
return false
}
let isVideo = isVideoCall(intent)
appReadiness.runNowOrWhenAppDidBecomeReadySync {
Task { @MainActor [appReadiness] in
do {
try await appReadiness.waitForAppReady()
} catch {
return
}
let callService = AppEnvironment.shared.callService!
let screenLockUI = AppEnvironment.shared.screenLockUI
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
do {
try await screenLockUI.waitForScreenUnlockThrowingPrevious()
} catch {
return
}
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
Logger.warn("Ignoring user activity; not registered.")
return
@ -1764,7 +1803,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// * It can be received if the user taps the "video" button for a contact
// in the contacts app. If so, the correct response is to try to initiate a
// new call to that user - unless there is another call in progress.
let callService = AppEnvironment.shared.callService!
if let currentCall = callService.callServiceState.currentCall {
if isVideo, case .individual = currentCall.mode, currentCall.mode.matches(callTarget) {
Logger.info("Upgrading existing call to video")
@ -1776,6 +1814,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
callService.initiateCall(to: callTarget, isVideo: isVideo)
}
return true
}
@ -1790,17 +1829,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
scheduleBgAppRefresh()
let attachmentDownloadmanager = DependenciesBridge.shared.attachmentDownloadManager
let db = DependenciesBridge.shared.db
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let registeredState = try? tsAccountManager.registeredStateWithMaybeSneakyTransaction()
if let registeredState {
Logger.info("localAci: \(registeredState.localIdentifiers.aci)")
db.write { transaction in
ExperienceUpgradeFinder.markAllCompleteForNewUser(transaction: transaction)
}
attachmentDownloadmanager.beginDownloadingIfNecessary()
// Schedule a Cron run if we're in the foreground.
@ -1908,9 +1942,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
Task { @MainActor [appReadiness] () -> Void in
defer { completionHandler() }
try await self.appReadiness.waitForAppReady()
do {
try await self.appReadiness.waitForAppReady()
} catch {
return
}
let screenLockUI = AppEnvironment.shared.screenLockUI
let backgroundMessageFetcherFactory = DependenciesBridge.shared.backgroundMessageFetcherFactory
let backgroundMessageFetcher = backgroundMessageFetcherFactory.buildFetcher()
// So that we open up a connection for replies.
await backgroundMessageFetcher.start()
@ -1919,7 +1959,11 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let elapsedDuration = (MonotonicDate() - startDate).seconds
try await withCooperativeTimeout(seconds: 27 - elapsedDuration) {
// Do the actual thing we care about.
try await NotificationActionHandler.handleNotificationResponse(response, appReadiness: appReadiness)
try await NotificationActionHandler.handleNotificationResponse(
response,
appReadiness: appReadiness,
screenLockUI: screenLockUI,
)
// Then wait for any enqueued messages (e.g., read receipts) to be sent.
try await backgroundMessageFetcher.waitForFetchingProcessingAndSideEffects()

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
@ -284,11 +287,7 @@ public class AppEnvironment: NSObject {
// Things that should run on either the primary or linked devices.
if let registeredState, registeredState.isPrimary {
Task {
do {
try await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
} catch {
Logger.warn("Couldn't perform avatar default color migration: \(error)")
}
await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
}
Task {
@ -329,12 +328,6 @@ public class AppEnvironment: NSObject {
} else {
}
Task {
await db.awaitableWrite { tx in
masterKeySyncManager.runStartupJobs(tx: tx)
}
}
Task {
await db.awaitableWrite { tx in
groupCallRecordRingingCleanupManager.cleanupRingingCalls(tx: tx)

View File

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

View File

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

View File

@ -225,7 +225,6 @@ final class BackupDisablingManager {
accountEntropyPoolManager.setAccountEntropyPool(
newAccountEntropyPool: try! AccountEntropyPool(key: aepBeingRotatedString),
disablePIN: false,
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
@ -120,7 +121,7 @@ class BackupSettingsViewController:
self.onAppearAction = onAppearAction
switch onAppearAction {
case .presentWelcomeToBackupsSheet, nil:
case nil, .presentWelcomeToBackupsSheet, .disableOptimizeLocalStorage:
break
case .automaticallyStartBackup(let completion):
self.onBackupComplete = completion
@ -179,6 +180,8 @@ class BackupSettingsViewController:
presentWelcomeToBackupsSheet()
case .automaticallyStartBackup:
performManualBackup()
case .disableOptimizeLocalStorage:
setOptimizeLocalStorage(false)
}
}
@ -618,28 +621,87 @@ class BackupSettingsViewController:
final class WelcomeToBackupsSheet: HeroSheetViewController {
override var canBeDismissed: Bool { false }
init(onConfirm: @escaping () -> Void) {
init(
optimizeLocalStorage: (isOn: Bool, onValueChanged: (Bool) -> Void)?,
onConfirm: @escaping (HeroSheetViewController) -> Void,
) {
let toggle: HeroSheetViewController.Body.Toggle?
if let (isOn, onValueChanged) = optimizeLocalStorage {
toggle = HeroSheetViewController.Body.Toggle(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_TITLE",
comment: "Title for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
footer: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_FOOTER",
comment: "Footer for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
isOn: isOn,
onValueChanged: onValueChanged,
)
} else {
toggle = nil
}
super.init(
hero: .image(.backupsSubscribed),
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_TITLE",
comment: "Title for a sheet shown after the user enables backups.",
),
body: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
),
primaryButton: HeroSheetViewController.Button(
title: CommonStrings.okButton,
action: { _ in onConfirm() },
body: HeroSheetViewController.Body(
textContent: .plain(OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
)),
toggle: toggle,
),
primary: .button(HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_BUTTON_TITLE",
comment: "Title for a button in a sheet shown after the user enables backups.",
),
action: { onConfirm($0) },
)),
secondary: nil,
)
}
}
let welcomeToBackupsSheet = WelcomeToBackupsSheet { [self] in
viewModel.performManualBackup()
dismiss(animated: true)
let backupPlan = db.read { tx in
backupPlanManager.backupPlan(tx: tx)
}
let welcomeToBackupsSheet: WelcomeToBackupsSheet
switch backupPlan {
case .disabled,
.disabling,
.free:
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: nil,
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
viewModel.performManualBackup()
}
},
)
case .paid,
.paidAsTester,
.paidExpiringSoon:
var isOptimizeStorageEnabled = false
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: (
isOn: isOptimizeStorageEnabled,
onValueChanged: { isOptimizeStorageEnabled = $0 },
),
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
setOptimizeLocalStorage(isOptimizeStorageEnabled)
viewModel.performManualBackup()
}
},
)
}
present(welcomeToBackupsSheet, animated: true)
@ -1018,35 +1080,38 @@ class BackupSettingsViewController:
// MARK: -
fileprivate func setOptimizeLocalStorage(_ newOptimizeLocalStorage: Bool) {
let isPaidPlanTester: Bool = db.write { tx in
let hasMadeAtLeastOneBackup: Bool? = db.write { tx in
let currentBackupPlan = backupPlanManager.backupPlan(tx: tx)
let newBackupPlan: BackupPlan
let isPaidPlanTester: Bool
let lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
let newBackupPlan: BackupPlan
switch currentBackupPlan {
case .disabled, .disabling, .free:
owsFailDebug("Shouldn't be setting Optimize Local Storage: \(currentBackupPlan)")
return false
case .disabled,
.disabling,
.free,
.paid(optimizeLocalStorage: newOptimizeLocalStorage),
.paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage),
.paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage):
return nil
case .paid:
newBackupPlan = .paid(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidExpiringSoon:
newBackupPlan = .paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidAsTester:
newBackupPlan = .paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = true
}
backupPlanManager.setBackupPlan(newBackupPlan, tx: tx)
return isPaidPlanTester
return lastBackupDetails != nil
}
// If disabling Optimize Local Storage, offer to start downloads now.
if !newOptimizeLocalStorage {
if
hasMadeAtLeastOneBackup == true,
!newOptimizeLocalStorage
{
// If disabling Optimize Local Storage with media potentially
// offloaded, offer to start downloads now.
showDownloadOffloadedMediaSheet()
} else if isPaidPlanTester {
showOffloadedMediaForTestersWarningSheet(onAcknowledge: {})
}
}
@ -1085,54 +1150,41 @@ class BackupSettingsViewController:
presentActionSheet(actionSheet)
}
private func showOffloadedMediaForTestersWarningSheet(
onAcknowledge: @escaping () -> Void,
) {
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_TITLE",
comment: "Title for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_MESSAGE",
comment: "Message for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
)
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.okButton,
handler: { _ in
onAcknowledge()
},
))
presentActionSheet(actionSheet)
}
// MARK: -
fileprivate func setIsBackupDownloadQueueSuspended(_ isSuspended: Bool, backupPlan: BackupPlan) {
if isSuspended {
let warningTitle: String?
let warningMessage: String?
switch backupPlan {
case .disabled, .disabling, .free, .paid:
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
case .paidAsTester:
showOffloadedMediaForTestersWarningSheet(onAcknowledge: { [self] in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
})
case .paidExpiringSoon:
case .disabled, .disabling:
warningTitle = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_DISABLING_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads while disabling Backups.",
)
warningMessage = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_DISABLING_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads while disabling Backups.",
)
case .free, .paidExpiringSoon:
warningTitle = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_EXPIRING_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads that will expire.",
)
warningMessage = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_EXPIRING_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads that will expire.",
)
case .paid, .paidAsTester:
warningTitle = nil
warningMessage = nil
}
if let warningTitle, let warningMessage {
let warningSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads.",
),
title: warningTitle,
message: warningMessage,
)
warningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
@ -1141,9 +1193,31 @@ class BackupSettingsViewController:
),
style: .destructive,
handler: { [self] _ in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
let secondWarningSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_TITLE",
comment: "Title for a double-confirmation sheet warning the user about skipping downloads.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_MESSAGE",
comment: "Message for a double-confirmation sheet warning the user about skipping downloads.",
),
)
secondWarningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_ACTION_SKIP",
comment: "Title for an action in a double-confirmation sheet warning the user about skipping downloads.",
),
style: .destructive,
handler: { [self] _ in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
},
))
secondWarningSheet.addAction(.cancel)
presentActionSheet(secondWarningSheet)
},
))
warningSheet.addAction(ActionSheetAction(
@ -1158,6 +1232,10 @@ class BackupSettingsViewController:
warningSheet.addAction(.cancel)
presentActionSheet(warningSheet)
} else {
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
}
} else {
db.write { tx in
@ -1361,10 +1439,10 @@ class BackupSettingsViewController:
onConfirmed: { [weak self] _ in
guard let self else { return }
self.finalizeNewRecoveryKey(newCandidateAEP: newCandidateAEP)
// Pop all the way back to Backup Settings.
navigationController?.popToViewController(self, animated: true) {
self.finalizeNewRecoveryKey(newCandidateAEP: newCandidateAEP)
self.presentToast(text: OWSLocalizedString(
"BACKUP_SETTINGS_CREATE_NEW_KEY_SUCCESS_TOAST",
comment: "Toast shown when a new Recovery Key has been created successfully.",
@ -1390,7 +1468,6 @@ class BackupSettingsViewController:
accountEntropyPoolManager.setAccountEntropyPool(
newAccountEntropyPool: newCandidateAEP,
disablePIN: false,
tx: tx,
)
case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
@ -1633,16 +1710,21 @@ private class BackupSettingsViewModel: ObservableObject {
// MARK: -
var optimizeLocalStorageAvailable: Bool {
/// Whether the "Optimze Storage" feature is available, per the current
/// `BackupPlan`.
var isOptimizeLocalStorageAvailable: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
case .paid, .paidExpiringSoon, .paidAsTester:
case .paid, .paidAsTester:
true
case .paidExpiringSoon(let optimizeLocalStorage):
// Only allow disabling Optimize Storage if expiring soon, not enabling.
optimizeLocalStorage
}
}
var optimizeLocalStorage: Bool {
var isOptimizeLocalStorageEnabled: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
@ -1915,44 +1997,32 @@ struct BackupSettingsView: View {
viewModel: viewModel,
)
if BuildFlags.Backups.showOptimizeMedia {
Toggle(
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
),
isOn: Binding(
get: { viewModel.optimizeLocalStorage },
set: { viewModel.setOptimizeLocalStorage($0) },
),
).disabled(!viewModel.optimizeLocalStorageAvailable)
}
Toggle(
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
),
isOn: Binding(
get: { viewModel.isOptimizeLocalStorageEnabled },
set: { viewModel.setOptimizeLocalStorage($0) },
),
).disabled(!viewModel.isOptimizeLocalStorageAvailable)
} footer: {
if BuildFlags.Backups.showOptimizeMedia {
let footerText: String = if
viewModel.optimizeLocalStorageAvailable,
viewModel.isPaidPlanTester
{
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester.",
)
} else if viewModel.optimizeLocalStorageAvailable {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
)
} else {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
)
}
Text(footerText)
.foregroundStyle(Color.Signal.secondaryLabel)
.font(.caption)
let footerText: String = if viewModel.isOptimizeLocalStorageAvailable {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
)
} else {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
)
}
Text(footerText)
.foregroundStyle(Color.Signal.secondaryLabel)
.font(.caption)
}
SignalSection {
@ -3209,7 +3279,8 @@ private extension BackupSettingsViewModel {
backupSubscriptionLoadingState: .loaded(.paidButExpiring(
expirationDate: Date().addingTimeInterval(.week),
)),
backupPlan: .paidExpiringSoon(optimizeLocalStorage: false),
backupPlan: .paidExpiringSoon(optimizeLocalStorage: true),
latestBackupAttachmentDownloadUpdateState: .suspended,
))
}

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

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

View File

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

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

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

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

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

View File

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

View File

@ -159,11 +159,10 @@ class MessageUserSubsetSheet: OWSTableSheetViewController {
cell.selectionStyle = .none
let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser)
var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser)
configuration.forceDarkAppearance = self?.forceDarkMode ?? false
if
BuildFlags.MemberLabel.display,
let groupThread = self?.groupThread,
let senderAci = address.aci,
let memberLabelString = groupThread.groupModel.groupMembership.memberLabel(for: senderAci)?.labelForRendering(),

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()
}
}
@ -194,14 +192,19 @@ class CVAttachmentProgressView: ManualLayoutView {
applyState(.tapToDownload, animated: animateStateChange)
case .enqueuedOrDownloading:
applyState(.unknownProgress, animated: animateStateChange)
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadProgressNotification,
object: nil,
)
}
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadProgressNotification,
object: nil,
)
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadStoppedNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadStoppedNotification,
object: nil,
)
}
}
@ -360,6 +363,22 @@ class CVAttachmentProgressView: ManualLayoutView {
applyState(.progress(progress: progress), animated: window != nil)
}
@objc
private func processDownloadStoppedNotification(notification: Notification) {
AssertIsOnMainThread()
guard
let attachmentId = notification.userInfo?[AttachmentDownloads.attachmentDownloadAttachmentIDKey] as? Attachment.IDType
else {
owsFailDebug("Missing notificationAttachmentId.")
return
}
guard attachmentId == self.attachmentId else {
return
}
applyState(.tapToDownload, animated: window != nil)
}
@objc
private func processUploadNotification(notification: Notification) {
AssertIsOnMainThread()

View File

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

View File

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

View File

@ -493,6 +493,9 @@ public struct CVComponentState: Equatable {
let detailsText: NSAttributedString?
/// For mutual groups, lack thereof and note-to-self description.
let mutualGroupsText: NSAttributedString?
/// Populated if `mutualGroupsText` is not suitable for a11y, for
/// example if it embeds an image.
let mutualGroupsAccessibilityText: String?
let threadType: SafetyTipsType
let shouldShowSafetyTipsButton: Bool
let isOfficialChat: Bool
@ -525,7 +528,6 @@ public struct CVComponentState: Equatable {
static func ==(lhs: CollapseSet, rhs: CollapseSet) -> Bool {
return lhs.collapsedInteractions.map(\.uniqueId) == rhs.collapsedInteractions.map(\.uniqueId)
&& lhs.collapseSetType == rhs.collapseSetType
&& lhs.isExpanded == rhs.isExpanded
&& lhs.finalTimerDescription == rhs.finalTimerDescription
}
}
@ -1194,7 +1196,7 @@ private extension CVComponentState.Builder {
self.collapseSet = CVComponentState.CollapseSet(
collapsedInteractions: collapseSetInteraction.collapsedInteractions,
collapseSetType: collapseSetInteraction.collapseSetType,
isExpanded: collapseSetInteraction.isExpanded,
isExpanded: viewStateSnapshot.expandedCollapseSetIds.contains(collapseSetInteraction.uniqueId),
finalTimerDescription: collapseSetInteraction.finalTimerDescription,
)
return build()
@ -1361,7 +1363,33 @@ private extension CVComponentState.Builder {
case .failed:
mediaAlbumHasFailedAttachment = true
case .none:
mediaAlbumHasSkippedAttachment = true
// If optimize local storage is enabled, and the user has auto-downloads
// disabled, show the 'skipped attachment' download indicator. Otherwise
// render the attachment as normal, using the backup thumbnail for display.
let backupPlan = DependenciesBridge.shared.backupPlanManager.backupPlan(tx: transaction)
switch backupPlan {
case
.paid(let optimizeLocalStorage),
.paidAsTester(let optimizeLocalStorage),
.paidExpiringSoon(let optimizeLocalStorage):
if
optimizeLocalStorage,
canAutoDownloadAttachment(referencedAttachment: attachment),
attachment.attachment.localRelativeFilePathThumbnail != nil
{
// If optimize storage is enabled, auto-downloads are enabled,
// and the backup thumbnail is present, show the backup thumbnail
// as a true attachment (don't show the download icon overlay).
mediaAlbumHasSkippedAttachment = false
} else {
mediaAlbumHasSkippedAttachment = true
}
case
.free,
.disabled,
.disabling:
mediaAlbumHasSkippedAttachment = true
}
}
}
@ -1397,6 +1425,28 @@ private extension CVComponentState.Builder {
return result
}
private func canAutoDownloadAttachment(referencedAttachment: ReferencedAttachment) -> Bool {
let mediaBandwidthPreferenceStore = DependenciesBridge.shared.mediaBandwidthPreferenceStore
let autoDownloadableMediaTypes = mediaBandwidthPreferenceStore.autoDownloadableMediaTypes(tx: transaction)
let mimeType = referencedAttachment.attachment.mimeType
if MimeTypeUtil.isSupportedImageMimeType(mimeType) {
return autoDownloadableMediaTypes.contains(.photo)
}
if MimeTypeUtil.isSupportedVideoMimeType(mimeType) {
return autoDownloadableMediaTypes.contains(.video)
}
if MimeTypeUtil.isSupportedAudioMimeType(mimeType) {
if
autoDownloadableMediaTypes.contains(.audio),
referencedAttachment.reference.renderingFlag != .voiceMessage
{
return true
}
return false
}
return autoDownloadableMediaTypes.contains(.document)
}
mutating func buildThreadDetails() -> ThreadDetails {
owsAssertDebug(interaction is ThreadDetailsInteraction)
@ -1631,7 +1681,6 @@ private extension CVComponentState.Builder {
} else if let quotedMessage = message.quotedMessage {
var memberLabel: String?
if
BuildFlags.MemberLabel.display,
let groupThread = thread as? TSGroupThread,
!threadViewModel.hasPendingMessageRequest,
let originalMessageAuthor = quotedMessage.authorAddress.aci
@ -1714,10 +1763,10 @@ private extension CVComponentState.Builder {
let caption = referencedAttachment.reference.legacyMessageCaption
let hasCaption = caption.map {
return CVComponentState.displayableCaption(
return !CVComponentState.displayableCaption(
text: $0,
transaction: transaction,
).fullTextValue.isEmpty.negated
).fullTextValue.isEmpty
} ?? false
switch cvAttachment {

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))
@ -249,23 +255,30 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
innerViews.append(UIView.spacer(withHeight: vSpacingSafetySectionDefault))
let mutualGroupsLabelConfig = mutualGroupsLabelConfig(attributedText: mutualGroupsText)
mutualGroupsLabelConfig.applyForRendering(label: mutualGroupsLabel)
mutualGroupsLabel.accessibilityLabel = safetySection.mutualGroupsAccessibilityText
innerViews.append(mutualGroupsLabel)
}
if safetySection.shouldShowSafetyTipsButton {
let showTipsButton = componentView.showTipsButton
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &showTipsButton.configuration!)
showTipsButton.configuration?.baseBackgroundColor =
var buttonConfiguration = headerButtonConfigurationBase()
buttonConfiguration.contentInsets = safetyButtonContentInsets
buttonConfiguration.baseBackgroundColor =
conversationStyle.hasWallpaper ? .Signal.MaterialBase.button : .Signal.secondaryFill
showTipsButton.addAction(
UIAction { _ in
let safetyTipsButtonLabelConfig = safetyTipsButtonLabelConfig()
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &buttonConfiguration)
let showTipsButton = UIButton(
configuration: buttonConfiguration,
primaryAction: UIAction { _ in
componentDelegate.didTapSafetyTips()
},
for: .primaryActionTriggered,
)
innerViews.append(UIView.spacer(withHeight: vSpacingSafetyButton))
innerViews.append(showTipsButton)
componentView.showTipsButton = showTipsButton
}
}
@ -448,7 +461,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
)
}
private var safetyTipsButtonLabelConfig: CVLabelConfig {
private func safetyTipsButtonLabelConfig() -> CVLabelConfig {
CVLabelConfig.unstyledText(
OWSLocalizedString(
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
@ -642,10 +655,14 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
private let vSpacingSafetySectionDefault: CGFloat = 8
private let safetyButtonContentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 5)
private let hPaddingGroupDetails: CGFloat = 25
private let notVerifierButtonContentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 2)
private func headerButtonConfigurationBase() -> UIButton.Configuration {
var configuration = UIButton.Configuration.filled()
configuration.cornerStyle = .capsule
return configuration
}
private let vPaddingNotVerifiedButton: CGFloat = 2
private let hPaddingNotVerifiedButton: CGFloat = 12
private let hPaddingGroupDetails: CGFloat = 25
private let vOffsetThreadDetailsOutline: CGFloat = 16
@ -688,20 +705,18 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if let safetySection = threadDetails.safetySection {
if safetySection.shouldShowProfileNamesEducation {
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
let notVerifiedSize = CVText.measureLabel(
let buttonSize = CVText.measureLabel(
config: nameNotVerifiedConfig(),
maxWidth: maxContentWidth,
)
let notVerifiedSizeWithPadding = CGSize(width: notVerifiedSize.width + hPaddingNotVerifiedButton * 2, height: notVerifiedSize.height + vPaddingNotVerifiedButton * 2)
innerSubviewInfos.append(notVerifiedSizeWithPadding.asManualSubviewInfo)
) + notVerifierButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
} else if safetySection.isOfficialChat {
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
let officialLabelSize = CVText.measureLabel(
let buttonSize = CVText.measureLabel(
config: officialLabelConfig(),
maxWidth: maxContentWidth,
)
let officialLabelSizeWithPadding = CGSize(width: officialLabelSize.width + hPaddingNotVerifiedButton * 2, height: officialLabelSize.height + vPaddingNotVerifiedButton * 2)
innerSubviewInfos.append(officialLabelSizeWithPadding.asManualSubviewInfo)
) + notVerifierButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
}
}
@ -737,7 +752,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if safetySection.shouldShowSafetyTipsButton {
innerSubviewInfos.append(CGSize(square: vSpacingSafetyButton).asManualSubviewInfo)
let buttonSize = CVText.measureLabel(
config: safetyTipsButtonLabelConfig,
config: safetyTipsButtonLabelConfig(),
maxWidth: maxContentWidth,
) + safetyButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
@ -786,7 +801,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if let safetySection = threadDetails.safetySection {
if
safetySection.shouldShowSafetyTipsButton,
componentView.showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
let showTipsButton = componentView.showTipsButton,
showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
{
componentDelegate.didTapSafetyTips()
return true
@ -803,7 +819,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if
safetySection.shouldShowProfileNamesEducation,
componentView.profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
let profileNamesEducationButton = componentView.profileNamesEducationButton,
profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
{
componentDelegate.didTapNameEducation(type: safetySection.threadType)
return true
@ -838,17 +855,13 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
fileprivate let titleButton = CVButton()
fileprivate let bioLabel = CVLabel()
fileprivate let profileNamesEducationButton = OWSRoundedButton()
fileprivate var profileNamesEducationButton: UIButton?
fileprivate let officialLabel = CVLabel()
fileprivate let reviewCarefullyLabel = CVLabel()
fileprivate let detailsButton = CVButton()
fileprivate let mutualGroupsLabel = CVLabel()
fileprivate let showTipsButton: UIButton = {
let button = UIButton(configuration: .gray())
button.configuration?.contentInsets = NSDirectionalEdgeInsets(hMargin: 10, vMargin: 5)
return button
}()
fileprivate var showTipsButton: UIButton?
fileprivate let groupDescriptionPreviewView = GroupDescriptionPreviewView(
shouldDeactivateConstraints: true,
@ -906,6 +919,7 @@ extension CVComponentThreadDetails {
shouldShowProfileNamesEducation: false,
detailsText: NSAttributedString(string: OWSLocalizedString("RELEASE_NOTES_DETAILS", comment: "Details text for the thread details view of the release notes channel")),
mutualGroupsText: nil,
mutualGroupsAccessibilityText: nil,
threadType: .contact,
shouldShowSafetyTipsButton: false,
isOfficialChat: true,
@ -1038,6 +1052,7 @@ extension CVComponentThreadDetails {
shouldShowProfileNamesEducation: shouldShowUnknownThreadWarning,
detailsText: membersAttributedText,
mutualGroupsText: nil,
mutualGroupsAccessibilityText: nil,
threadType: .group,
shouldShowSafetyTipsButton: shouldShowUnknownThreadWarning && groupThread.hasPendingMessageRequest(transaction: tx),
isOfficialChat: false,
@ -1070,6 +1085,7 @@ extension CVComponentThreadDetails {
with: .font(.dynamicTypeSubheadline),
.color(UIColor.Signal.label),
),
mutualGroupsAccessibilityText: nil,
threadType: .contact,
shouldShowSafetyTipsButton: false,
isOfficialChat: false,
@ -1174,6 +1190,7 @@ extension CVComponentThreadDetails {
" ",
formattedString,
]),
mutualGroupsAccessibilityText: formattedString,
threadType: .contact,
shouldShowSafetyTipsButton: isMessageRequest,
isOfficialChat: false,

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

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

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

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

View File

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

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

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

View File

@ -25,6 +25,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
private let signalService: OWSSignalServiceProtocol
private let storageServiceManager: StorageServiceManager
private let svr: SecureValueRecovery
private let svrLocalStorage: SVRLocalStorage
private let syncManager: SyncManagerProtocol
private let threadStore: ThreadStore
private let tsAccountManager: TSAccountManager
@ -47,6 +48,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
signalService: OWSSignalServiceProtocol,
storageServiceManager: StorageServiceManager,
svr: SecureValueRecovery,
svrLocalStorage: SVRLocalStorage,
syncManager: SyncManagerProtocol,
threadStore: ThreadStore,
tsAccountManager: TSAccountManager,
@ -68,6 +70,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.signalService = signalService
self.storageServiceManager = storageServiceManager
self.svr = svr
self.svrLocalStorage = svrLocalStorage
self.syncManager = syncManager
self.threadStore = threadStore
self.tsAccountManager = tsAccountManager
@ -359,23 +362,11 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.tsAccountManager.setRegistrationId(aciRegistrationId, for: .aci, tx: tx)
self.tsAccountManager.setRegistrationId(pniRegistrationId, for: .pni, tx: tx)
do {
try svr.storeKeys(
fromProvisioningMessage: provisionMessage,
authedDevice: .explicit(authedDevice),
tx: tx,
)
} catch {
switch error {
case SVR.KeysError.missingMasterKey:
owsFailDebug("Failed to store master key from provisioning message")
return .obsoleteLinkedDeviceError
case SVR.KeysError.missingOrInvalidMRBK:
return .obsoleteLinkedDeviceError
default:
owsFailDebug("Unexpected Error")
}
}
self.svr.storeKeys(
fromProvisioningMessage: provisionMessage,
authedDevice: .explicit(authedDevice),
tx: tx,
)
self.receiptManager.setAreReadReceiptsEnabled(
provisionMessage.areReadReceiptsEnabled,
@ -402,7 +393,6 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
userProfileWriter: .linking,
transaction: tx,
)
self.svr.clearKeys(transaction: tx)
// reset to default (false)
self.receiptManager.setAreReadReceiptsEnabled(
@ -480,7 +470,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
didLinkNSync: Bool,
) async throws(CompleteProvisioningError) {
let hasBackedUpMasterKey = self.db.read { tx in
self.svr.hasBackedUpMasterKey(transaction: tx)
self.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
}
let capabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
do {
@ -703,7 +693,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
let phoneNumberDiscoverability = tsAccountManager.phoneNumberDiscoverability(tx: tx)
let hasSVRBackups = svr.hasBackedUpMasterKey(transaction: tx)
let hasSVRBackups = svrLocalStorage.isMasterKeyBackedUp(tx: tx)
return AccountAttributes(
isManualMessageFetchEnabled: isManualMessageFetchEnabled,

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

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

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

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

View File

@ -424,7 +424,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -485,9 +484,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
// Whenever we do this, wipe the keys we've got.
// We don't want to have them and use then implicitly later.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -524,9 +520,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
// Whenever we do this, wipe the keys we've got.
// We don't want to have them and use them implicitly later.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -888,7 +881,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
var hasBackedUpToSVR = false
var didSkipSVRBackup = false
var shouldBackUpToSVR: Bool {
return hasBackedUpToSVR.negated && didSkipSVRBackup.negated
return !hasBackedUpToSVR && !didSkipSVRBackup
}
var backupMetadataHeader: BackupNonce.MetadataHeader?
@ -1276,7 +1269,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// but these values aren't persisted to their final destination until the very end of
// registration, so persiting the these values once at the start is the easiest way to
// avoid problems.
// Note: We should not reuse existing registration ids if we are reregistering
// Note: We should generate new registration ids if we are reregistering
updatePersistedState(tx) {
if $0.aciRegistrationId == nil {
$0.aciRegistrationId = RegistrationIdGenerator.generate()
@ -1438,16 +1431,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
deps.backupArchiveManager.scheduleRestoreFromSVRBBeforeNextExport(tx: tx)
}
if
inMemoryState.hasBackedUpToSVR
|| inMemoryState.didHaveSVRBackupsPriorToReg
|| inMemoryState.backupRestoreState == .finalized
{
// No need to show the experience if we made the pin
// and backed up.
deps.experienceManager.clearIntroducingPinsExperience(tx)
}
// Persist the AEP. RegCoordinator manages all necessary side
// effects, like updating Account Attributes and rotating the
// Storage Service manifest.
@ -2043,9 +2026,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// Its possible we tried svr2 and kbs has the right info, or vice versa, but this is all
// best effort anyway; just fall back to session-based registration.
deps.svrAuthCredentialStore.removeSVR2CredentialsForCurrentUser(tx)
// Clear the SVR master key locally; we failed reglock so we know its wrong
// and useless anyway.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
self.updatePersistedState(tx) {
$0.e164WithKnownReglockEnabled = e164
@ -2185,7 +2165,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
private func loadSVRAuthCredentialCandidates(_ tx: DBReadTransaction) {
let svr2AuthCredentialCandidates: [SVR2AuthCredential] = deps.svrAuthCredentialStore.getAuthCredentials(tx)
if svr2AuthCredentialCandidates.isEmpty.negated {
if !svr2AuthCredentialCandidates.isEmpty {
inMemoryState.svr2AuthCredentialCandidates = svr2AuthCredentialCandidates
}
}
@ -2323,7 +2303,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// If we have a local master key, theres no need to restore after registration.
// (we will still back up though)
inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = localMasterKey == nil
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svr.hasBackedUpMasterKey(transaction: tx)
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
}
// MARK: - SVR Auth Credential Candidates Pathway
@ -2740,7 +2720,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
guard
(
inMemoryState.accountEntropyPool != nil ||
persistedState.hasGivenUpTryingToRestoreWithSVR.negated
!persistedState.hasGivenUpTryingToRestoreWithSVR
)
else {
// If we haven't set an AEP, and have already exhausted our SVR backup attempts, we are stuck.
@ -3544,13 +3524,13 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
let accountEntropyPool = getOrGenerateAccountEntropyPool()
if
let backupStepGuarantee = await performSVRBackupStepsIfNeeded(
let nextStep = await performSVRBackupStepsIfNeeded(
resetPINReminderInterval: false,
accountEntropyPool: accountEntropyPool,
accountIdentity: accountIdentity,
)
{
return backupStepGuarantee
return nextStep
}
return await exportAndWipeState(
@ -3918,7 +3898,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
}
if let reglockToken = self.reglockToken(for: accountIdentity.e164) {
if inMemoryState.hasSetReglock.negated {
if !inMemoryState.hasSetReglock {
return await self.enableReglock(accountIdentity: accountIdentity, reglockToken: reglockToken)
}
} else {
@ -4037,9 +4017,10 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
let masterKey = accountEntropyPool.getMasterKey()
do {
let backedUpMasterKey = try await deps.svr.backupMasterKey(
try await deps.svr.backupMasterKey(
pin: pin,
masterKey: masterKey,
force: true,
authMethod: authMethod,
)
@ -4047,7 +4028,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
await db.awaitableWrite { tx in
logger.info("Setting pin code after SVR backup")
updateMasterKeyAndLocalState(
masterKey: backedUpMasterKey,
masterKey: masterKey,
tx: tx,
)
deps.ows2FAManager.markPinEnabled(
@ -4389,7 +4370,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
switch mode {
case .reRegistering(let state):
if persistedState.hasResetForReRegistration.negated {
if !persistedState.hasResetForReRegistration {
db.write { tx in
let isPrimaryDevice = deps.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
let discoverability = deps.phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx)
@ -4780,10 +4761,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
private func reglockToken(for e164: E164) -> String? {
if
inMemoryState.wasReglockEnabledBeforeStarting
|| persistedState.e164WithKnownReglockEnabled == e164
,
inMemoryState.wasReglockEnabledBeforeStarting || persistedState.e164WithKnownReglockEnabled == e164,
let reglockToken = inMemoryState.reglockToken
{
return reglockToken

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

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

View File

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

View File

@ -89,8 +89,6 @@ protocol RegistrationPinPresenter: AnyObject {
func submitWithCreateNewPinInstead()
func exitRegistration()
func enterRecoveryKey()
}
// MARK: - RegistrationPinViewController
@ -146,21 +144,6 @@ class RegistrationPinViewController: OWSViewController {
// MARK: Rendering
private lazy var moreButton: ContextMenuButton = {
let result = ContextMenuButton(empty: ())
result.setImage(Theme.iconImage(.buttonMore), for: .normal)
if #unavailable(iOS 26) {
result.tintColor = .Signal.accent
}
result.autoSetDimensions(to: .square(40))
return result
}()
private lazy var moreBarButton = UIBarButtonItem(
customView: moreButton,
accessibilityIdentifier: "registration.pin.disablePinButton",
)
private lazy var backButton: UIButton = {
let result = UIButton()
result.setTemplateImage(
@ -330,19 +313,15 @@ class RegistrationPinViewController: OWSViewController {
super.viewDidLoad()
view.backgroundColor = .Signal.background
navigationItem.rightBarButtonItem = {
let barButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.pin.nextButton",
)
barButtonItem.tintColor = .Signal.accent
return barButtonItem
}()
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.pin.nextButton",
)
self.stackView = addStaticContentStackView(
stackView = addStaticContentStackView(
arrangedSubviews: [titleLabel, explanationView, pinTextField],
isScrollable: true,
shouldAvoidKeyboard: true,
@ -399,9 +378,7 @@ class RegistrationPinViewController: OWSViewController {
}
private func configureUIForCreatingNewPin() {
navigationItem.leftBarButtonItem = moreBarButton
moreButton.setActions(actions: [
navigationItem.leftBarButtonItem = .contextMenuButton(actions: [
UIAction(
title: OWSLocalizedString(
"PIN_CREATION_LEARN_MORE",
@ -462,9 +439,7 @@ class RegistrationPinViewController: OWSViewController {
skippability: RegistrationPinState.Skippability,
remainingAttempts: UInt?,
) {
navigationItem.leftBarButtonItem = moreBarButton
var actions = [UIMenuElement]()
var actions = [UIAction]()
if skippability.canSkip {
actions.append(UIAction(
title: OWSLocalizedString(
@ -477,23 +452,11 @@ class RegistrationPinViewController: OWSViewController {
))
}
actions.append(
UIAction(
title: OWSLocalizedString(
"PIN_ENTER_EXISTING_USE_RECOVERY_KEY",
comment: "If the user is re-registering, they need to enter their PIN to restore all their data. If they don't remember their PIN, they may remember their Recovery Key which can be used instead of a PIN.",
),
handler: { [weak self] _ in
self?.presenter?.enterRecoveryKey()
},
),
)
if let exitAction = exitAction() {
actions.append(exitAction)
}
moreButton.setActions(actions: actions)
navigationItem.leftBarButtonItem = .contextMenuButton(actions: actions)
showAttemptWarningIfNecessary(
remainingAttempts: remainingAttempts,
@ -705,15 +668,6 @@ class RegistrationPinViewController: OWSViewController {
}
}
actionSheet.addAction(.init(
title: OWSLocalizedString(
"ONBOARDING_2FA_SKIP_AND_USE_RECOVERY_KEY",
comment: "Label for action to use Recovery Key instead of PIN for registration.",
),
) { [weak self] _ in
self?.presenter?.enterRecoveryKey()
})
actionSheet.addAction(.init(title: CommonStrings.contactSupport) { [weak self] _ in
guard let self else { return }
ContactSupportActionSheet.present(

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