Compare commits

...

103 Commits

Author SHA1 Message Date
Pete Walters
9ea0a9c0f4 Update translations 2025-11-19 20:43:22 -06:00
Sasha Weiss
1490787671 Rotate uploadEra when BackupPlan becomes "paid" 2025-11-19 17:42:55 -08:00
Sasha Weiss
da818b8e0d Avoid infinite-retries of fatal errors in in ListMediaManager 2025-11-19 17:42:36 -08:00
Sasha Weiss
cfc4533ad0 Only run list-media on paid-tier primaries 2025-11-19 15:15:08 -08:00
Sasha Weiss
d0147ed416 Rotate uploadEra when disabling Backups 2025-11-19 15:09:00 -08:00
Sasha Weiss
3af9aa593d Show WelcomeToBackups when reenabling, but not when upgrading 2025-11-19 13:14:25 -08:00
sashaweiss-signal
c0f3f876df Remove verbose log line 2025-11-19 11:51:53 -08:00
sashaweiss-signal
f8dca3c504 Update Linked Devices header string 2025-11-19 10:20:26 -08:00
Igor Solomennikov
276b1d0c8f Give selected rows in chat list search results special treatment when in sidebar. 2025-11-19 10:19:22 -08:00
Igor Solomennikov
69cb1b2840 Some love for sticker pack view.
Modernize everything keeping layout the same.
2025-11-19 10:18:59 -08:00
Max Radermacher
308a438b2c Use typed throws when removing image metadata 2025-11-19 12:09:39 -06:00
Igor Solomennikov
8bdf99ccc1 Improve Dynamic Type support in chat input bar.
Make message input field and link preview instantly respond to content size changes.
2025-11-18 20:52:34 -08:00
Igor Solomennikov
d392b640d8 Stop re-creating ConversationInputToolbar on theme change.
I've noticed that iOS does change theme multiple times when the app is backgrounded (probably creating screenshots for the app switcher). And each change caused `ConversationInputToolbar` to be re-created and replaced - super ineffective and potentially? causing UI glitches.

Recent changes made to support iOS 26 have already added support for adjusting to dark/light UI style on the fly. What was needed to be done is extending that functionality to support UI used on iOS 15-18, which these changes do.
2025-11-18 20:29:24 -08:00
Igor Solomennikov
9c15eb5a6b Layout tweaks for stories lists when displayed in split view mode.
• additional 16 dp horizontal insets on both sides.
• rounded corners for selected list rows.
2025-11-18 20:15:53 -08:00
Igor Solomennikov
87243e9ea3 Improvements to long text view controller.
• align text to VC's "readable content layout guide". 
• proper toolbar on iOS 26.
• use dynamic colors instead of Theme.
• add support for changing text size.
2025-11-18 20:15:13 -08:00
Igor Solomennikov
77b195e7cd Explicitly call ConversationInputToolbar.scrollToBottom.
`ConversationViewController.updateInputToolbarLayout(initialLayout:)` was a no-op when parameter was `false`. When the parameter was `true` it was only calling `ConversationInputToolbar.scrollToBottom`.

Remove four call sites where the parameter was `false`. Update a single other call site to invoke `ConversationInputToolbar.scrollToBottom` directly.
2025-11-18 20:14:39 -08:00
Igor Solomennikov
b2b202bc71 Improvements to chat CV gesture recognizer logic.
• Initialize `collectionViewContextMenuSecondaryClickRecognizer` from within `CVViewState` like the rest of chat view's gesture recognizers. Do this with the purpose of uniformity.
• Do not use nullability of `chatInputToobar` as indication of first `viewWillAppear` call in order to configure CV gesture recognizers just once.
2025-11-18 20:13:51 -08:00
kate-signal
dd556d0417 Animate poll bar when vote changes 2025-11-18 15:44:06 -08:00
Igor Solomennikov
940312cae5 Show Send button when there's a voice message draft. 2025-11-18 15:39:17 -08:00
Elaine
7ff19e7bba Update location picker styling for iOS 26 2025-11-18 13:21:48 -08:00
sashaweiss-signal
1e5f1890c7 Change support URL for Backups 2025-11-18 11:31:47 -08:00
Sasha Weiss
f02d7b03c6 Add 'Copied to Clipboard' toast to DebugLogs action sheet 2025-11-18 13:10:16 -06:00
Elaine
75f6e77aa8 Refactor table sheet footers 2025-11-18 13:13:21 -05:00
kate-signal
d6d35606a7 Live updates to poll details view 2025-11-18 12:42:45 -05:00
kate-signal
ec0cd23b53 Drafts with non-contacts don't show up in chat list 2025-11-18 11:59:05 -05:00
kate-signal
e234f56623 Handle early poll terminate 2025-11-18 11:57:30 -05:00
Kate
58d781f1bf Fix end poll icon name 2025-11-18 10:28:30 -05:00
kate-signal
ac7243c25d Remove payments from attachment menu if they aren't activated 2025-11-17 21:27:28 -05:00
Sasha Weiss
ab616d0cef Only attempt deleting Backup when deleting account if Backups are enabled 2025-11-17 16:33:08 -08:00
Elaine
0f0e7de2df App icon tweaks 2025-11-17 16:20:24 -08:00
Elaine
d727d0091c Update header padding on forward sheet 2025-11-17 16:18:17 -08:00
Max Radermacher
58207219e5 Downgrade early message assertions to warnings 2025-11-17 17:12:17 -06:00
Igor Solomennikov
1385a10b9a Implement new design for chat view banners. 2025-11-17 14:50:03 -08:00
Igor Solomennikov
2e50d797ba Do not reset scroll position in the attachment menu between presentations. 2025-11-17 14:49:18 -08:00
Igor Solomennikov
e20ad23c7b Optimize attachment picker for compact vertical size class environments.
• smaller corner radius: 20 vs 36.
• smaller top margin: 20 vs 36.
2025-11-17 14:48:53 -08:00
Igor Solomennikov
c0a267b030 Update attachment menu top inset from 40 to 36. 2025-11-17 14:17:05 -08:00
Max Radermacher
4d0a45136d Reduce duplication with video recording file type 2025-11-17 16:16:48 -06:00
Max Radermacher
47afe79d2f Add assertion for aspect ratio validity 2025-11-17 16:16:33 -06:00
Max Radermacher
fb6c6863b1 Use remote config for video recording size limit 2025-11-17 16:16:18 -06:00
Max Radermacher
d4141a3cfb Use constant overhead for video recording teardown 2025-11-17 16:15:30 -06:00
Max Radermacher
7c3a5c98a7 Limit video size checks to 1 Hz while recording 2025-11-17 16:14:48 -06:00
Pete Walters
8c3fef27e1 Make the Registration splash an OWSNavigationChildController 2025-11-17 15:25:43 -06:00
Igor Solomennikov
2b07e73614 Fix mention picker too wide in iPhone landscape and on iPad. 2025-11-17 13:23:03 -08:00
kate-signal
d7bf01bec9 Fix long press for end poll part 2 2025-11-17 16:12:10 -05:00
Sasha Weiss
2e1587acf7 Remove DeleteForMeInfoSheetCoordinator 2025-11-17 12:10:39 -08:00
Sasha Weiss
1cde05f9b1 Remove unnecessary code from ConversationSettings, OWSTableViewController2 2025-11-17 12:10:29 -08:00
Sasha Weiss
bb6ad0eb95 More precisely decide which notifications to clear on app activate 2025-11-17 12:10:08 -08:00
kate-signal
006267443e Change end-poll weight for long press menu 2025-11-17 12:01:54 -05:00
Pete Walters
c2500f6dc9 De-protocolize & de-shim EditManager 2025-11-17 10:51:21 -06:00
Max Radermacher
2e348030e9 Sort remote configs 2025-11-17 10:19:05 -06:00
kate-signal
6e1186fc19 Add timer to poll end info message 2025-11-17 08:47:56 -05:00
Igor Solomennikov
6d9f97ec69 Layout tweaks for call list when displayed in split view mode.
• extra 18 pt padding along leading and trailing edges.
• rounded corners for background in "selected" state.
2025-11-14 22:06:05 -08:00
Igor Solomennikov
bb82459818 More tweaks to chat list displayed in split view mode. 2025-11-14 22:05:29 -08:00
Max Radermacher
d639897f31 Use DataSourcePath explicitly for a few uploads 2025-11-14 21:07:50 -06:00
Max Radermacher
9f00ab0b93 De-protocolize & move SignalAttachmentCloner 2025-11-14 21:06:58 -06:00
Max Radermacher
68adce5056 Clean up ForwardMessageItem & friends 2025-11-14 21:05:21 -06:00
Max Radermacher
5acdd04d9e Measure video transcode/render performance 2025-11-14 21:04:14 -06:00
Max Radermacher
0d455ecbbf Clean up file provider document handling 2025-11-14 21:03:19 -06:00
Max Radermacher
1ccfb3966a Remove isVideoThatNeedsCompression 2025-11-14 21:01:06 -06:00
Max Radermacher
cf3a2fb3a4 Don’t specify dataUTI when compressing videos 2025-11-14 20:59:05 -06:00
Max Radermacher
c38b968d9f Require DataSourcePath when compressing videos 2025-11-14 20:58:19 -06:00
Max Radermacher
ab01da1436 Reuse videoAttachment method after compressing 2025-11-14 20:57:23 -06:00
Max Radermacher
1152c38d50 Remove redundant check for compressing videos 2025-11-14 20:56:27 -06:00
Max Radermacher
9c217d29f1 Prefer strongly-typed SignalAttachment methods 2025-11-14 20:55:54 -06:00
Max Radermacher
34e8f4633d Require Memoji to be images 2025-11-14 20:55:17 -06:00
Max Radermacher
061f0f0674 Make SignalAttachment’s dataSource non-Optional 2025-11-14 20:54:11 -06:00
Max Radermacher
38c64bd997 Remove SignalAttachment.dataSource wrappers 2025-11-14 20:53:23 -06:00
Max Radermacher
4991bb753e Remove unnecessary video validation code 2025-11-14 20:52:07 -06:00
Max Radermacher
2fc6fb8994 Un-consolidate video validation methods 2025-11-14 20:50:52 -06:00
Max Radermacher
95c882099f Simplify error handling for large recorded videos 2025-11-14 20:49:47 -06:00
Sasha Weiss
fde498d01e Remove GroupCallTooltip 2025-11-14 16:42:00 -08:00
Igor Solomennikov
83688f3e62 Add dynamic color support for the "No Access To Photos" UI in attachment menu. 2025-11-14 16:39:11 -08:00
Igor Solomennikov
993d630c5c Give selected chats on iPad in split view configuration a special treatment.
Rounded corners for selected cell's background.
Extra horizontal padding for all chat list cells.
2025-11-14 12:51:27 -08:00
kate-signal
88228a5502 Fix poll long press 2025-11-14 15:08:34 -05:00
kate-signal
d5fa2dd912 Add icon to pinned messages 2025-11-14 13:56:50 -05:00
Sasha Weiss
c2ca54fdca Reduce subscription-related jitter in BackupSettings 2025-11-14 10:13:04 -08:00
kate-signal
2606068db4 Schema for pinned messages 2025-11-14 12:47:40 -05:00
Max Radermacher
fb019581e6 Remove “unknown” mime type 2025-11-14 09:53:04 -06:00
Max Radermacher
1e416c46f4 Remove unused moveToUrlAndConsume method 2025-11-14 09:52:50 -06:00
Max Radermacher
5868ce3518 Fix error when compressed video is too large 2025-11-14 09:52:26 -06:00
Igor Solomennikov
444edd9150 Tweaks for chat input bar. 2025-11-13 18:52:08 -08:00
Pete Walters
1716016d28 Remove some shims 2025-11-13 19:54:23 -06:00
Max Radermacher
5f99c76b01 Fix date calculation during background fetches 2025-11-13 18:09:45 -06:00
Max Radermacher
8581b1f541 Fix handling for background fetches that finish 2025-11-13 18:09:10 -06:00
Max Radermacher
19bdd8cd05 Assume we have a PNI identity key 2025-11-13 18:08:44 -06:00
Max Radermacher
816fe08c43 Remove various path-related wrappers from String 2025-11-13 18:08:03 -06:00
Max Radermacher
9efc1cea74 Throw errors in fileSize(of: …) 2025-11-13 18:07:34 -06:00
Max Radermacher
ea3e1474af Simplify video trimming logic; remove unused code 2025-11-13 17:52:19 -06:00
Pete Walters
67d0b5b946 Launch SDSDB into the sun 2025-11-13 17:49:54 -06:00
Max Radermacher
79469c571c Remove unused video-related methods 2025-11-13 17:49:46 -06:00
Max Radermacher
2821affb49 Remove unused video compression method 2025-11-13 17:49:17 -06:00
Sasha Weiss
3582fde3a1 Remove unnecessary code from BackupArchiveManager 2025-11-13 15:48:24 -08:00
Igor Solomennikov
3af22e8252 Fix "suggested stickers" panel becomes misplaced after backgrounding the app. 2025-11-13 15:38:32 -08:00
Sasha Weiss
40d9577d7c Use same timestamp for LastBackupDetails as in Backup header 2025-11-13 15:19:48 -08:00
Sasha Weiss
ec322816a3 Tweaks to BackupExportJob 2025-11-13 15:17:08 -08:00
Sasha Weiss
b2aab9de7e Consolidate "last Backup details" into LastBackupDetails 2025-11-13 15:16:05 -08:00
Sasha Weiss
27f6dfa633 Post a notification when lastBackupDate changes 2025-11-13 15:15:28 -08:00
Max Radermacher
e84013727a Remove appForegroundTime property 2025-11-13 14:53:28 -06:00
Max Radermacher
faf4618355 Use random bytes for profile rotation token 2025-11-13 14:52:04 -06:00
Max Radermacher
f287ee8cfb Use random bytes for attributes change token 2025-11-13 14:50:52 -06:00
Max Radermacher
333f0776eb Partially revert "Fix build warnings from XCode 9." 2025-11-13 11:33:13 -06:00
Max Radermacher
9aa21c694d Fix loop when cleaning up view once messages 2025-11-13 11:32:51 -06:00
sashaweiss-signal
36a4a1a101 Bump version to 7.87 2025-11-12 16:57:50 -08:00
440 changed files with 6612 additions and 9252 deletions

View File

@@ -9,12 +9,16 @@
/* Begin PBXBuildFile section */
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */; };
041C24ED2DF782AF0065B685 /* OutgoingGroupUpdateMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */; };
0426758B2EC4D5C800124C5F /* PinnedMessageIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */; };
0436E4B02E5E2DC20011E125 /* OWSPollTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0436E4AF2E5E2DB90011E125 /* OWSPollTest.swift */; };
043CC30E2E1EF79C00D9002E /* ThreadFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */; };
04472D5E2E8AD74200D69EE0 /* OutgoingPollVote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04472D5D2E8AD73A00D69EE0 /* OutgoingPollVote.swift */; };
044D77782E5E6D750048C21A /* PollVoteRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D77772E5E6D700048C21A /* PollVoteRecord.swift */; };
044D84452E9FDDF00090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D84442E9FDDE30090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift */; };
044D84472E9FEE010090BA64 /* BackupArchivePollArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D84462E9FEDE20090BA64 /* BackupArchivePollArchiver.swift */; };
045B40892EC67510002D3F9A /* PinnedMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B40882EC6750A002D3F9A /* PinnedMessageManager.swift */; };
045B408C2EC67C03002D3F9A /* PinnedMessageRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B408B2EC67BFF002D3F9A /* PinnedMessageRecord.swift */; };
045B408E2EC6897B002D3F9A /* PinnedMessageManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */; };
046926092E8EBAAE00B1FC74 /* TSInfoMessage+Polls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */; };
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
0480F0002E57C51A006CBB29 /* BackupsEnabledNotificationMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */; };
@@ -736,6 +740,7 @@
50C4AEFC2CE6766B005609F6 /* GroupSendEndorsements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C4AEFB2CE6766B005609F6 /* GroupSendEndorsements.swift */; };
50C4AEFE2CE6767C005609F6 /* GroupSendFullTokenBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C4AEFD2CE6767C005609F6 /* GroupSendFullTokenBuilder.swift */; };
50C831762BAA3A8000BEBF25 /* CallMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C831752BAA3A8000BEBF25 /* CallMessageHandler.swift */; };
50C861E62EC52CA90001A4FA /* SignalAttachmentCloner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664E8D872BD6D87700C4968A /* SignalAttachmentCloner.swift */; };
50C97C252C3C7F7000A9F384 /* CallEventConversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C97C242C3C7F7000A9F384 /* CallEventConversation.swift */; };
50C98A412B69D9340065BD2E /* PhoneNumberVisibilityFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C98A402B69D9340065BD2E /* PhoneNumberVisibilityFetcher.swift */; };
50CDC6592DFB92FD00824B4A /* MonitorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CDC6582DFB92FD00824B4A /* MonitorTest.swift */; };
@@ -806,7 +811,6 @@
66062E712E43F83600D5F8C1 /* OWSSequentialProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66062E702E43F83200D5F8C1 /* OWSSequentialProgress.swift */; };
66076B4C2BC053290043D547 /* LinkPreviewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B4B2BC053290043D547 /* LinkPreviewBuilder.swift */; };
66076B4E2BC056980043D547 /* LinkPreviewBuilderImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B4D2BC056980043D547 /* LinkPreviewBuilderImpl.swift */; };
66076B5C2BC06CA70043D547 /* EditManagerAttachmentsShims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B582BC06CA70043D547 /* EditManagerAttachmentsShims.swift */; };
66076B5D2BC06CA70043D547 /* MockEditManagerAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B592BC06CA70043D547 /* MockEditManagerAttachments.swift */; };
66076B5E2BC06CA70043D547 /* EditManagerAttachmentsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B5A2BC06CA70043D547 /* EditManagerAttachmentsImpl.swift */; };
66076B5F2BC06CA70043D547 /* EditManagerAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66076B5B2BC06CA70043D547 /* EditManagerAttachments.swift */; };
@@ -951,7 +955,6 @@
664BA8452BB5CE12005638E0 /* PreparedOutgoingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664BA8442BB5CE12005638E0 /* PreparedOutgoingMessage.swift */; };
664BA8472BB5CE1A005638E0 /* UnpreparedOutgoingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664BA8462BB5CE1A005638E0 /* UnpreparedOutgoingMessage.swift */; };
664BA84A2BB5DFE1005638E0 /* ContactShareDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664BA8492BB5DFE1005638E0 /* ContactShareDraft.swift */; };
664E8D882BD6D87700C4968A /* SignalAttachmentCloner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664E8D872BD6D87700C4968A /* SignalAttachmentCloner.swift */; };
664E8D942BD86AFB00C4968A /* AttachmentDownloadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664E8D932BD86AFB00C4968A /* AttachmentDownloadState.swift */; };
665229892E218D5F002C14A0 /* InternalDiskUsageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665229882E218D53002C14A0 /* InternalDiskUsageViewController.swift */; };
6652298B2E26E7D4002C14A0 /* BackupOversizeTextCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6652298A2E26E7D1002C14A0 /* BackupOversizeTextCache.swift */; };
@@ -968,7 +971,6 @@
66586D4129009C0000DDA9B9 /* TextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66586D4029009C0000DDA9B9 /* TextAttachment.swift */; };
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */; };
6659A0282A7C11ED00066AB7 /* MockPreKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */; };
6659A02A2A7C121C00066AB7 /* PreKeyManager+Shims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0292A7C121C00066AB7 /* PreKeyManager+Shims.swift */; };
6659A0312A7C5B9700066AB7 /* PreKeyUploadBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */; };
6659A0392A81933B00066AB7 /* ProvisioningPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */; };
6659CCB129CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659CCB029CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift */; };
@@ -995,7 +997,6 @@
6673FF752979F87500F96CFD /* SVRAuthCredentialStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6673FF742979F87500F96CFD /* SVRAuthCredentialStorageTests.swift */; };
6673FF87297B694C00F96CFD /* DB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6673FF86297B694C00F96CFD /* DB.swift */; };
6673FF89297B6AF800F96CFD /* DBTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6673FF88297B6AF800F96CFD /* DBTransaction.swift */; };
6673FF8B297B6FA800F96CFD /* SDSDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6673FF8A297B6FA800F96CFD /* SDSDB.swift */; };
6674807C2E32EA52004DA6E9 /* Optional+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6674807B2E32EA4D004DA6E9 /* Optional+SSK.swift */; };
6674807E2E344EE9004DA6E9 /* OWSOutgoingPaymentMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6674807D2E344EE9004DA6E9 /* OWSOutgoingPaymentMessage.swift */; };
6675F64F29261C39007A311E /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6675F64E29261C39007A311E /* SyncPushTokensJob.swift */; };
@@ -2734,7 +2735,6 @@
D97C43962D7F74F300BA41E0 /* recipient_contacts_05.txtproto in Resources */ = {isa = PBXBuildFile; fileRef = D97C43952D7F74F300BA41E0 /* recipient_contacts_05.txtproto */; };
D97C43972D7F74F300BA41E0 /* recipient_contacts_05.binproto in Resources */ = {isa = PBXBuildFile; fileRef = D97C43942D7F74F300BA41E0 /* recipient_contacts_05.binproto */; };
D984F7242C21FF1600E1CA49 /* DeleteForMeOutgoingSyncMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D984F7232C21FF1600E1CA49 /* DeleteForMeOutgoingSyncMessageTest.swift */; };
D985D86629B949D20087C90C /* ChangePhoneNumberPniManager+Shims.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985D86529B949D20087C90C /* ChangePhoneNumberPniManager+Shims.swift */; };
D985D86829B94EC60087C90C /* ChangePhoneNumberPniManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985D86329B91C400087C90C /* ChangePhoneNumberPniManagerTest.swift */; };
D987C8CF2E1CBE00004072BC /* BackupTestFlightEntitlementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D987C8CE2E1CBDFB004072BC /* BackupTestFlightEntitlementManager.swift */; };
D98ACAA72E0CA11F00FA497F /* BackupAttachmentUploadQueueStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98ACAA62E0CA11F00FA497F /* BackupAttachmentUploadQueueStatusManager.swift */; };
@@ -2793,7 +2793,6 @@
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 */; };
D9B2E1182E748E1900A823E4 /* OWSByteCountFormatStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */; };
D9B3E6352D4067CA00594475 /* DBFileSizeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3E6342D4067C400594475 /* DBFileSizeProvider.swift */; };
D9B8541229137C150058F97B /* JobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B8541129137C150058F97B /* JobRecord.swift */; };
D9B95A9629E6830B00D7CB95 /* JobRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */; };
D9B95A9B29E8923B00D7CB95 /* InMemoryDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9929E8918200D7CB95 /* InMemoryDB.swift */; };
@@ -2883,7 +2882,6 @@
D9E43BFD2CC194140001536E /* GroupCallErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BAE2CC194140001536E /* GroupCallErrorView.swift */; };
D9E43BFE2CC194140001536E /* GroupCallNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BAF2CC194140001536E /* GroupCallNotificationView.swift */; };
D9E43BFF2CC194140001536E /* GroupCallSwipeToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BB02CC194140001536E /* GroupCallSwipeToastView.swift */; };
D9E43C002CC194140001536E /* GroupCallTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BB12CC194140001536E /* GroupCallTooltip.swift */; };
D9E43C012CC194140001536E /* GroupCallVideoGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BB22CC194140001536E /* GroupCallVideoGrid.swift */; };
D9E43C022CC194140001536E /* GroupCallVideoGridLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BB32CC194140001536E /* GroupCallVideoGridLayout.swift */; };
D9E43C032CC194140001536E /* GroupCallVideoOverflow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43BB42CC194140001536E /* GroupCallVideoOverflow.swift */; };
@@ -2933,7 +2931,6 @@
D9E7C8752B9A3FD1005BD3B9 /* CallRecordCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E7C8742B9A3FD1005BD3B9 /* CallRecordCursor.swift */; };
D9E7C8772B9A4A9C005BD3B9 /* CallRecord+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E7C8762B9A4A9C005BD3B9 /* CallRecord+Sorting.swift */; };
D9E8EDED2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E8EDEC2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift */; };
D9E8EDF32C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */; };
D9EA2A872C2B609800B367DF /* BackupArchiveChatUpdateMessageArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EA2A862C2B609800B367DF /* BackupArchiveChatUpdateMessageArchiver.swift */; };
D9EA2A892C2B929400B367DF /* BackupArchiveSimpleChatUpdateArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EA2A882C2B929400B367DF /* BackupArchiveSimpleChatUpdateArchiver.swift */; };
D9EB221E2A4B636C00C73E1D /* Bitmaps+LineDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EB22192A4B636B00C73E1D /* Bitmaps+LineDrawing.swift */; };
@@ -3871,12 +3868,16 @@
/* Begin PBXFileReference section */
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>"; };
0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageIconView.swift; sourceTree = "<group>"; };
0436E4AF2E5E2DB90011E125 /* OWSPollTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSPollTest.swift; sourceTree = "<group>"; };
043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadFinderTests.swift; sourceTree = "<group>"; };
04472D5D2E8AD73A00D69EE0 /* OutgoingPollVote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingPollVote.swift; sourceTree = "<group>"; };
044D77772E5E6D700048C21A /* PollVoteRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteRecord.swift; sourceTree = "<group>"; };
044D84442E9FDDE30090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchivePollTerminateChatUpdateArchiver.swift; sourceTree = "<group>"; };
044D84462E9FEDE20090BA64 /* BackupArchivePollArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchivePollArchiver.swift; sourceTree = "<group>"; };
045B40882EC6750A002D3F9A /* PinnedMessageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageManager.swift; sourceTree = "<group>"; };
045B408B2EC67BFF002D3F9A /* PinnedMessageRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageRecord.swift; sourceTree = "<group>"; };
045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageManagerTest.swift; sourceTree = "<group>"; };
046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+Polls.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>"; };
@@ -4788,7 +4789,6 @@
66062E702E43F83200D5F8C1 /* OWSSequentialProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSequentialProgress.swift; sourceTree = "<group>"; };
66076B4B2BC053290043D547 /* LinkPreviewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewBuilder.swift; sourceTree = "<group>"; };
66076B4D2BC056980043D547 /* LinkPreviewBuilderImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewBuilderImpl.swift; sourceTree = "<group>"; };
66076B582BC06CA70043D547 /* EditManagerAttachmentsShims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditManagerAttachmentsShims.swift; sourceTree = "<group>"; };
66076B592BC06CA70043D547 /* MockEditManagerAttachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEditManagerAttachments.swift; sourceTree = "<group>"; };
66076B5A2BC06CA70043D547 /* EditManagerAttachmentsImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditManagerAttachmentsImpl.swift; sourceTree = "<group>"; };
66076B5B2BC06CA70043D547 /* EditManagerAttachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditManagerAttachments.swift; sourceTree = "<group>"; };
@@ -4951,7 +4951,6 @@
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>"; };
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPreKeyManager.swift; sourceTree = "<group>"; };
6659A0292A7C121C00066AB7 /* PreKeyManager+Shims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreKeyManager+Shims.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>"; };
6659CCB029CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationConfirmModeSwitchViewController.swift; sourceTree = "<group>"; };
@@ -4978,7 +4977,6 @@
6673FF742979F87500F96CFD /* SVRAuthCredentialStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVRAuthCredentialStorageTests.swift; sourceTree = "<group>"; };
6673FF86297B694C00F96CFD /* DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DB.swift; sourceTree = "<group>"; };
6673FF88297B6AF800F96CFD /* DBTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBTransaction.swift; sourceTree = "<group>"; };
6673FF8A297B6FA800F96CFD /* SDSDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDSDB.swift; sourceTree = "<group>"; };
6674807B2E32EA4D004DA6E9 /* Optional+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+SSK.swift"; sourceTree = "<group>"; };
6674807D2E344EE9004DA6E9 /* OWSOutgoingPaymentMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSOutgoingPaymentMessage.swift; sourceTree = "<group>"; };
6675F64C2925C012007A311E /* APNSRotationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSRotationStore.swift; sourceTree = "<group>"; };
@@ -6733,7 +6731,6 @@
D98300B12936E6C70018FDC2 /* SubscriptionConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionConfigManager.swift; sourceTree = "<group>"; };
D984F7232C21FF1600E1CA49 /* DeleteForMeOutgoingSyncMessageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteForMeOutgoingSyncMessageTest.swift; sourceTree = "<group>"; };
D985D86329B91C400087C90C /* ChangePhoneNumberPniManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberPniManagerTest.swift; sourceTree = "<group>"; };
D985D86529B949D20087C90C /* ChangePhoneNumberPniManager+Shims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChangePhoneNumberPniManager+Shims.swift"; sourceTree = "<group>"; };
D987C8CE2E1CBDFB004072BC /* BackupTestFlightEntitlementManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupTestFlightEntitlementManager.swift; sourceTree = "<group>"; };
D98ACAA62E0CA11F00FA497F /* BackupAttachmentUploadQueueStatusManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentUploadQueueStatusManager.swift; sourceTree = "<group>"; };
D98ACAA82E0DA71700FA497F /* BackupPlanManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPlanManager.swift; sourceTree = "<group>"; };
@@ -6791,7 +6788,6 @@
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>"; };
D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSByteCountFormatStyle.swift; sourceTree = "<group>"; };
D9B3E6342D4067C400594475 /* DBFileSizeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBFileSizeProvider.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>"; };
D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecordTest.swift; sourceTree = "<group>"; };
@@ -6890,7 +6886,6 @@
D9E43BAE2CC194140001536E /* GroupCallErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallErrorView.swift; sourceTree = "<group>"; };
D9E43BAF2CC194140001536E /* GroupCallNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallNotificationView.swift; sourceTree = "<group>"; };
D9E43BB02CC194140001536E /* GroupCallSwipeToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallSwipeToastView.swift; sourceTree = "<group>"; };
D9E43BB12CC194140001536E /* GroupCallTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTooltip.swift; sourceTree = "<group>"; };
D9E43BB22CC194140001536E /* GroupCallVideoGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoGrid.swift; sourceTree = "<group>"; };
D9E43BB32CC194140001536E /* GroupCallVideoGridLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoGridLayout.swift; sourceTree = "<group>"; };
D9E43BB42CC194140001536E /* GroupCallVideoOverflow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoOverflow.swift; sourceTree = "<group>"; };
@@ -6940,7 +6935,6 @@
D9E7C8742B9A3FD1005BD3B9 /* CallRecordCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRecordCursor.swift; sourceTree = "<group>"; };
D9E7C8762B9A4A9C005BD3B9 /* CallRecord+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallRecord+Sorting.swift"; sourceTree = "<group>"; };
D9E8EDEC2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteForMeOutgoingSyncMessageManagerTest.swift; sourceTree = "<group>"; };
D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteForMeInfoSheetCoordinator.swift; sourceTree = "<group>"; };
D9EA2A862C2B609800B367DF /* BackupArchiveChatUpdateMessageArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveChatUpdateMessageArchiver.swift; sourceTree = "<group>"; };
D9EA2A882C2B929400B367DF /* BackupArchiveSimpleChatUpdateArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveSimpleChatUpdateArchiver.swift; sourceTree = "<group>"; };
D9EB22192A4B636B00C73E1D /* Bitmaps+LineDrawing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bitmaps+LineDrawing.swift"; sourceTree = "<group>"; };
@@ -7901,6 +7895,15 @@
path = Records;
sourceTree = "<group>";
};
045B408A2EC67BDB002D3F9A /* PinnedMessages */ = {
isa = PBXGroup;
children = (
045B40882EC6750A002D3F9A /* PinnedMessageManager.swift */,
045B408B2EC67BFF002D3F9A /* PinnedMessageRecord.swift */,
);
path = PinnedMessages;
sourceTree = "<group>";
};
04A5736E2E4D4BCB0019651F /* Polls */ = {
isa = PBXGroup;
children = (
@@ -8857,6 +8860,7 @@
4C25768923AD510800E0398D /* LoadMoreMessagesView.swift */,
34EB0E712629DC2B00B62DC3 /* MessageSelectionView.swift */,
34EB0CEA26289D8800B62DC3 /* MessageTimerView.swift */,
0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */,
3470C8732554926200F5847C /* QuotedMessageView.swift */,
348EE28D25B897BF00814FC2 /* ReusableMediaView.swift */,
D968B4972C9E1AC3006B14E1 /* SmsLockIconView.swift */,
@@ -9422,7 +9426,6 @@
children = (
66076B5B2BC06CA70043D547 /* EditManagerAttachments.swift */,
66076B5A2BC06CA70043D547 /* EditManagerAttachmentsImpl.swift */,
66076B582BC06CA70043D547 /* EditManagerAttachmentsShims.swift */,
66076B592BC06CA70043D547 /* MockEditManagerAttachments.swift */,
);
path = Attachments;
@@ -9504,6 +9507,7 @@
isa = PBXGroup;
children = (
661AEE472C2088FD0046B1D8 /* AttachmentDownloadRetryRunner.swift */,
664E8D872BD6D87700C4968A /* SignalAttachmentCloner.swift */,
);
path = Attachments;
sourceTree = "<group>";
@@ -9680,7 +9684,6 @@
isa = PBXGroup;
children = (
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
6659A0292A7C121C00066AB7 /* PreKeyManager+Shims.swift */,
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */,
6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */,
C17345BA2A5E000300C6426D /* PreKeyTarget.swift */,
@@ -9769,9 +9772,7 @@
isa = PBXGroup;
children = (
6673FF86297B694C00F96CFD /* DB.swift */,
D9B3E6342D4067C400594475 /* DBFileSizeProvider.swift */,
6673FF88297B6AF800F96CFD /* DBTransaction.swift */,
6673FF8A297B6FA800F96CFD /* SDSDB.swift */,
);
path = V2;
sourceTree = "<group>";
@@ -10492,7 +10493,6 @@
664428952C12305D0092D0E2 /* SignalAttachment+Sending.swift */,
66AF4D7228D1377E008A156E /* SignalAttachment+VideoSegmenting.swift */,
34D913491F62D4A500722898 /* SignalAttachment.swift */,
664E8D872BD6D87700C4968A /* SignalAttachmentCloner.swift */,
);
path = Attachments;
sourceTree = "<group>";
@@ -11487,7 +11487,6 @@
D9E43BE12CC194140001536E /* Calls */,
50B6BCAF2AEC4F3B0010FB3B /* Contacts */,
3448BFC01EDF0EA7005B2D69 /* ConversationView */,
D9E8EDEF2C0FCB0600923E3C /* DeleteForMe */,
88C4E38124671F9D009C9B97 /* DeviceTransfer */,
3428576F26BD8777005A2A96 /* Emoji */,
50E7E1CC2BACBDE000A94861 /* Expiration */,
@@ -12955,7 +12954,6 @@
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */ = {
isa = PBXGroup;
children = (
D985D86529B949D20087C90C /* ChangePhoneNumberPniManager+Shims.swift */,
50E5E4B22993352C00E15A1C /* ChangePhoneNumberPniManager.swift */,
6603AC2C29C220F30079BC82 /* ChangePhoneNumberPniManagerMock.swift */,
);
@@ -13090,7 +13088,6 @@
D9E43BAE2CC194140001536E /* GroupCallErrorView.swift */,
D9E43BAF2CC194140001536E /* GroupCallNotificationView.swift */,
D9E43BB02CC194140001536E /* GroupCallSwipeToastView.swift */,
D9E43BB12CC194140001536E /* GroupCallTooltip.swift */,
D9E43BB22CC194140001536E /* GroupCallVideoGrid.swift */,
D9E43BB32CC194140001536E /* GroupCallVideoGridLayout.swift */,
D9E43BB42CC194140001536E /* GroupCallVideoOverflow.swift */,
@@ -13158,14 +13155,6 @@
path = DisappearingMessagesConfiguration;
sourceTree = "<group>";
};
D9E8EDEF2C0FCB0600923E3C /* DeleteForMe */ = {
isa = PBXGroup;
children = (
D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */,
);
path = DeleteForMe;
sourceTree = "<group>";
};
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */ = {
isa = PBXGroup;
children = (
@@ -13475,6 +13464,7 @@
F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */,
F93A76EC29133A4B005FDE4F /* OWSDisappearingMessagesJobTest.swift */,
F9426237289B1B5500460798 /* OWSUDManagerTest.swift */,
045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */,
50B62C752AB216E300705A89 /* PniSignatureProcessorTest.swift */,
F942622C289B1B5500460798 /* ReceiptSenderTest.swift */,
50B0E9492AC747B3005D46AB /* RecipientStateMergerTest.swift */,
@@ -13880,6 +13870,7 @@
children = (
664BA8482BB5DF44005638E0 /* ContactShare */,
669FAE162B7A8DA4009EE2FE /* LinkPreview */,
045B408A2EC67BDB002D3F9A /* PinnedMessages */,
04A5736E2E4D4BCB0019651F /* Polls */,
669FAE0C2B744BA4009EE2FE /* Quotes */,
667AF9E12B4DC5EE008AEE5D /* GroupUpdateSource.swift */,
@@ -17134,7 +17125,6 @@
76E7A2112A01B6A500A8F538 /* DebugUITableViewController.swift in Sources */,
88DBDFB9263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift in Sources */,
887B6DC925F6C3E900E677D4 /* DeleteAccountConfirmationViewController.swift in Sources */,
D9E8EDF32C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift in Sources */,
66F98DE22DB71220009F1A86 /* DeviceBatteryLevelManagerImpl.swift in Sources */,
C17A54982D7B3DCD00E1D267 /* DeviceProvisioningURL.swift in Sources */,
505347F92DA08548004AF0B5 /* DeviceSleepManagerImpl.swift in Sources */,
@@ -17224,7 +17214,6 @@
D9E43C272CC194140001536E /* GroupCallRecordRingingCleanupManager.swift in Sources */,
D9E43C282CC194140001536E /* GroupCallRemoteVideoManager.swift in Sources */,
D9E43BFF2CC194140001536E /* GroupCallSwipeToastView.swift in Sources */,
D9E43C002CC194140001536E /* GroupCallTooltip.swift in Sources */,
D9E43C012CC194140001536E /* GroupCallVideoGrid.swift in Sources */,
D9E43C022CC194140001536E /* GroupCallVideoGridLayout.swift in Sources */,
D9E43C032CC194140001536E /* GroupCallVideoOverflow.swift in Sources */,
@@ -17384,6 +17373,7 @@
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
887889A52476E999001B5FCF /* PinConfirmationViewController.swift in Sources */,
0426758B2EC4D5C800124C5F /* PinnedMessageIconView.swift in Sources */,
887EEC1F23F0B20600F8C26D /* PinReminderMegaphone.swift in Sources */,
881677C522DD2B21007BAF49 /* PinReminderViewController.swift in Sources */,
881D85B822D92C2B00E118DF /* PinSetupViewController.swift in Sources */,
@@ -17496,6 +17486,7 @@
886BB3D325BA0CA400079781 /* SetWallpaperViewController.swift in Sources */,
F915A77029CB6D4C00EB6F68 /* ShareActivityUtil.swift in Sources */,
880D90302481E617003D2B14 /* SignalApp.swift in Sources */,
50C861E62EC52CA90001A4FA /* SignalAttachmentCloner.swift in Sources */,
D9E43C2D2CC194140001536E /* SignalCall.swift in Sources */,
8822558D26B9D1D7001A33C4 /* SignalDotMePhoneNumberLink.swift in Sources */,
D9E43C0F2CC194140001536E /* SimulatorCallUIAdaptee.swift in Sources */,
@@ -17921,7 +17912,6 @@
F9C5CC0C289453B300548EEE /* CDNDownloadOperation.swift in Sources */,
727328072CA6CF570080E2C7 /* Certificates.swift in Sources */,
661396AD28BE74DC00E0C4DF /* ChainedPromise.swift in Sources */,
D985D86629B949D20087C90C /* ChangePhoneNumberPniManager+Shims.swift in Sources */,
50E5E4B32993352C00E15A1C /* ChangePhoneNumberPniManager.swift in Sources */,
6603AC2D29C220F30079BC82 /* ChangePhoneNumberPniManagerMock.swift in Sources */,
66F6D6A32C7D0CCA00EFAF75 /* ChatColors.swift in Sources */,
@@ -17976,7 +17966,6 @@
6600F36C298DAA6200B1EDB7 /* DateProvider.swift in Sources */,
72345D1F2B9A1FCF000237B3 /* DateUtil.swift in Sources */,
6673FF87297B694C00F96CFD /* DB.swift in Sources */,
D9B3E6352D4067CA00594475 /* DBFileSizeProvider.swift in Sources */,
6673FF89297B6AF800F96CFD /* DBTransaction.swift in Sources */,
F9C5CDD8289453B400548EEE /* DebouncedEvent.swift in Sources */,
668A00DF2C2B5ECF007B8808 /* DebuggerUtils.m in Sources */,
@@ -18038,7 +18027,6 @@
C1DB22C329C9F95500757380 /* EditManager.swift in Sources */,
66076B5F2BC06CA70043D547 /* EditManagerAttachments.swift in Sources */,
66076B5E2BC06CA70043D547 /* EditManagerAttachmentsImpl.swift in Sources */,
66076B5C2BC06CA70043D547 /* EditManagerAttachmentsShims.swift in Sources */,
668B30092BBDD9A20001FD25 /* EditManagerImpl.swift in Sources */,
C1C4AA3329E7038D000CE9D3 /* EditManagerShims.swift in Sources */,
C167387529E8397B0068EA92 /* EditMessageStore.swift in Sources */,
@@ -18467,6 +18455,8 @@
F9CAC7832919B35E00EEC1DE /* PhoneNumberRegions.swift in Sources */,
F9C5CCD6289453B300548EEE /* PhoneNumberUtil.swift in Sources */,
50C98A412B69D9340065BD2E /* PhoneNumberVisibilityFetcher.swift in Sources */,
045B40892EC67510002D3F9A /* PinnedMessageManager.swift in Sources */,
045B408C2EC67C03002D3F9A /* PinnedMessageRecord.swift in Sources */,
6694BF682B36484900B18764 /* PinnedThreadManager.swift in Sources */,
F9C5CDEF289453B400548EEE /* PinnedThreadManagerImpl.swift in Sources */,
6694BF6A2B3650E400B18764 /* PinnedThreadStore.swift in Sources */,
@@ -18486,7 +18476,6 @@
50589CE02E8C4AD5003EF42A /* PreKey.swift in Sources */,
5010B6B42C6BD41E00314CD4 /* PreKeyBundle.swift in Sources */,
5050A8792B76E2E100E9BFA4 /* PreKeyId.swift in Sources */,
6659A02A2A7C121C00066AB7 /* PreKeyManager+Shims.swift in Sources */,
F9C5CCAC289453B300548EEE /* PreKeyManager.swift in Sources */,
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */,
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */,
@@ -18587,7 +18576,6 @@
72A132A72CA25EF0000ACED6 /* SDSCrossProcess.swift in Sources */,
F9C5CD2B289453B300548EEE /* SDSDatabaseStorage+Objc.m in Sources */,
F9C5CD1A289453B300548EEE /* SDSDatabaseStorage.swift in Sources */,
6673FF8B297B6FA800F96CFD /* SDSDB.swift in Sources */,
F9C5CD1B289453B300548EEE /* SDSDeserialization.swift in Sources */,
F9C5CD13289453B300548EEE /* SDSError.swift in Sources */,
F9C5CD15289453B300548EEE /* SDSModel.swift in Sources */,
@@ -18629,7 +18617,6 @@
664428962C12305D0092D0E2 /* SignalAttachment+Sending.swift in Sources */,
7255A4C62B98DEFB00E95368 /* SignalAttachment+VideoSegmenting.swift in Sources */,
7255A4C72B98DEFB00E95368 /* SignalAttachment.swift in Sources */,
664E8D882BD6D87700C4968A /* SignalAttachmentCloner.swift in Sources */,
F9C5CC8F289453B300548EEE /* SignalIOS.pb.swift in Sources */,
F9C5CC9E289453B300548EEE /* SignalIOSProto.swift in Sources */,
725465382BA01FAA00EABFD2 /* SignalMessagingJobQueues.swift in Sources */,
@@ -19062,6 +19049,7 @@
F9CAC7852919B5A400EEC1DE /* PhoneNumberRegionsTest.swift in Sources */,
F9426274289B1B5500460798 /* PhoneNumberTest.swift in Sources */,
F9426277289B1B5600460798 /* PhoneNumberUtilTest.swift in Sources */,
045B408E2EC6897B002D3F9A /* PinnedMessageManagerTest.swift in Sources */,
F97823F428CD0AC7005533BF /* PngChunkerTest.swift in Sources */,
D9CAF7532A0ADA4B0049193A /* PniDistributionParameterBuilderTest.swift in Sources */,
50B62C762AB216E300705A89 /* PniSignatureProcessorTest.swift in Sources */,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -13,10 +13,15 @@
"layers" : [
{
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:0.00000,0.47843,1.00000,1.00000"
}
},
{
"appearance" : "tinted",
"value" : {
"solid" : "gray:0.34749,1.00000"
"solid" : "gray:0.31820,1.00000"
}
}
],
@@ -29,6 +34,24 @@
{
"blend-mode" : "normal",
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"display-p3:0.65091,0.81203,1.00000,1.00000",
"srgb:0.02009,0.68619,0.89532,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5057908442982456,
"y" : 1
}
}
}
},
{
"appearance" : "tinted",
"value" : {
@@ -41,10 +64,40 @@
],
"image-name" : "bubbles-small.svg",
"name" : "bubbles-small",
"opacity" : 1
"opacity-specializations" : [
{
"value" : 1
},
{
"appearance" : "dark",
"value" : 0.92
},
{
"appearance" : "tinted",
"value" : 0.86
}
]
},
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"display-p3:1.00000,0.56471,0.16078,1.00000",
"srgb:1.00000,0.24706,0.20000,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.4822402887079063,
"y" : 1
}
}
}
},
{
"appearance" : "tinted",
"value" : {
@@ -83,4 +136,4 @@
"iOS"
]
}
}
}

View File

@@ -45,10 +45,28 @@
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.2
}
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.5
}
},
{
"appearance" : "dark",
"value" : {
"enabled" : true,
"value" : 0.3
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.3
}
}
]
}
],
"supported-platforms" : {
@@ -56,4 +74,4 @@
"iOS"
]
}
}
}

View File

@@ -37,18 +37,23 @@
{
"blend-mode-specializations" : [
{
"value" : "overlay"
"value" : "normal"
},
{
"appearance" : "dark",
"value" : "overlay"
"value" : "normal"
},
{
"appearance" : "tinted",
"value" : "overlay"
"value" : "normal"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
@@ -65,7 +70,19 @@
"hidden" : false,
"image-name" : "notes-bottom.svg",
"name" : "notes-bottom",
"opacity" : 0.96
"opacity-specializations" : [
{
"value" : 0.8
},
{
"appearance" : "dark",
"value" : 0.6
},
{
"appearance" : "tinted",
"value" : 0.8
}
]
},
{
"fill-specializations" : [
@@ -78,6 +95,53 @@
],
"image-name" : "notes-middle.svg",
"name" : "notes-middle"
},
{
"blend-mode-specializations" : [
{
"value" : "normal"
},
{
"appearance" : "dark",
"value" : "normal"
},
{
"appearance" : "tinted",
"value" : "normal"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"solid" : "srgb:0.93333,0.83922,0.45098,1.00000"
}
},
{
"appearance" : "tinted",
"value" : {
"solid" : "gray:0.94624,1.00000"
}
}
],
"glass" : false,
"hidden" : false,
"image-name" : "notes-bottom.svg",
"name" : "notes-bottom",
"opacity-specializations" : [
{
"value" : 0.9
},
{
"appearance" : "tinted",
"value" : 0.9
}
]
}
],
"shadow" : {
@@ -95,4 +159,4 @@
"iOS"
]
}
}
}

View File

@@ -655,7 +655,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
SSKEnvironment.shared.udManagerRef.setPhoneNumberSharingMode(
.nobody,
updateStorageServiceAndProfile: true,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
}
}
@@ -830,7 +830,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
let application: UIApplication = .shared
let userNotificationCenter: UNUserNotificationCenter = .current()
UserNotificationPresenter().clearAllNonScheduledNotifications()
UserNotificationPresenter().clearNotificationsForAppActivate()
application.applicationIconBadgeNumber = 0
userNotificationCenter.add(notificationRequest)
@@ -1388,7 +1388,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
if isPastRegistration {
try await backgroundFetcher.waitUntil(deadline: waitDeadline)
} else {
try await Task.sleep(nanoseconds: (waitDeadline - MonotonicDate()).nanoseconds)
let now = MonotonicDate()
if now < waitDeadline {
try await Task.sleep(nanoseconds: (waitDeadline - now).nanoseconds)
}
}
} catch {
// We were canceled, either because we entered the foreground or our
@@ -1397,9 +1400,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
},
completionHandler: { result in
switch result {
case .finished, .interrupted:
case .interrupted:
await backgroundFetcher.reset()
case .expired:
case .finished, .expired:
await backgroundFetcher.stopAndWaitBeforeSuspending()
}
},
@@ -1554,7 +1557,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
appReadiness.runNowOrWhenAppDidBecomeReadySync {
let oldBadgeValue = UIApplication.shared.applicationIconBadgeNumber
SSKEnvironment.shared.notificationPresenterRef.clearAllNonScheduledNotifications()
SSKEnvironment.shared.notificationPresenterRef.clearNotificationsForAppActivate()
UIApplication.shared.applicationIconBadgeNumber = oldBadgeValue
}
}

View File

@@ -95,7 +95,7 @@ public class AppEnvironment: NSObject {
deviceProvisioningService: deviceProvisioningService,
identityManager: DependenciesBridge.shared.identityManager,
linkAndSyncManager: DependenciesBridge.shared.linkAndSyncManager,
profileManager: ProvisioningManager.Wrappers.ProfileManager(SSKEnvironment.shared.profileManagerRef),
profileManager: SSKEnvironment.shared.profileManagerRef,
receiptManager: ProvisioningManager.Wrappers.ReceiptManager(SSKEnvironment.shared.receiptManagerRef),
tsAccountManager: DependenciesBridge.shared.tsAccountManager
)

View File

@@ -11,14 +11,11 @@ class MainAppContext: NSObject, AppContext {
let appLaunchTime: Date
private(set) var appForegroundTime: Date
override init() {
_reportedApplicationState = AtomicValue(.inactive, lock: .init())
let launchDate = Date()
appLaunchTime = launchDate
appForegroundTime = launchDate
_mainApplicationStateOnLaunch = UIApplication.shared.applicationState
super.init()
@@ -68,7 +65,6 @@ class MainAppContext: NSObject, AppContext {
AssertIsOnMainThread()
self.reportedApplicationState = .inactive
self.appForegroundTime = Date()
BenchManager.bench(title: "Slow WillEnterForeground", logIfLongerThan: 0.2, logInProduction: true) {
NotificationCenter.default.post(name: .OWSApplicationWillEnterForeground, object: nil)
@@ -185,9 +181,4 @@ class MainAppContext: NSObject, AppContext {
let hasUI: Bool = true
var debugLogsDirPath: String { DebugLogger.mainAppDebugLogsDirPath }
@MainActor
func resetAppDataAndExit() -> Never {
SignalApp.resetAppDataAndExit(keyFetcher: SSKEnvironment.shared.databaseStorageRef.keyFetcher)
}
}

View File

@@ -4,21 +4,10 @@
//
import Foundation
import SignalServiceKit
public protocol SignalAttachmentCloner {
func cloneAsSignalAttachment(
attachment: ReferencedAttachmentStream
) throws -> SignalAttachment
}
public class SignalAttachmentClonerImpl: SignalAttachmentCloner {
public init() {}
public func cloneAsSignalAttachment(
attachment: ReferencedAttachmentStream
) throws -> SignalAttachment {
enum SignalAttachmentCloner {
static func cloneAsSignalAttachment(attachment: ReferencedAttachmentStream) throws -> SignalAttachment {
guard let dataUTI = MimeTypeUtil.utiTypeForMimeType(attachment.attachmentStream.mimeType) else {
throw OWSAssertionError("Missing dataUTI.")
}
@@ -41,7 +30,7 @@ public class SignalAttachmentClonerImpl: SignalAttachmentCloner {
case .voiceMessage:
signalAttachment = try SignalAttachment.voiceMessageAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
case .borderless:
signalAttachment = try SignalAttachment.attachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
signalAttachment = try SignalAttachment.imageAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
signalAttachment.isBorderless = true
case .shouldLoop:
signalAttachment = try SignalAttachment.attachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
@@ -51,16 +40,3 @@ public class SignalAttachmentClonerImpl: SignalAttachmentCloner {
return signalAttachment
}
}
#if TESTABLE_BUILD
public class SignalAttachmentClonerMock: SignalAttachmentCloner {
public func cloneAsSignalAttachment(
attachment: ReferencedAttachmentStream
) throws -> SignalAttachment {
throw OWSAssertionError("Unimplemented!")
}
}
#endif

View File

@@ -203,9 +203,9 @@ final class BackupEnablingManager {
))
}
try await setBackupPlan { tx in
try await setBackupPlan { currentBackupPlan in
let currentOptimizeLocalStorage: Bool
switch backupPlanManager.backupPlan(tx: tx) {
switch currentBackupPlan {
case .disabled, .disabling, .free:
currentOptimizeLocalStorage = false
case
@@ -241,11 +241,7 @@ final class BackupEnablingManager {
throw .genericError
}
try await setBackupPlan { tx in
// Rotate the upload era, since now that we're paid we'll probably
// want to do uploads, and to that end need to do a list-media.
backupAttachmentUploadEraStore.rotateUploadEra(tx: tx)
try await setBackupPlan { _ in
return .paidAsTester(optimizeLocalStorage: false)
}
}
@@ -253,11 +249,11 @@ final class BackupEnablingManager {
// MARK: -
private func setBackupPlan(
block: (DBWriteTransaction) -> BackupPlan,
block: (_ currentBackupPlan: BackupPlan) -> BackupPlan,
) async throws(ActionSheetDisplayableError) {
do {
try await db.awaitableWriteWithRollbackIfThrows { tx in
let newBackupPlan = block(tx)
let newBackupPlan = block(backupPlanManager.backupPlan(tx: tx))
try backupPlanManager.setBackupPlan(newBackupPlan, tx: tx)
}
} catch {

View File

@@ -108,7 +108,7 @@ class BackupRecordKeyViewController: OWSViewController, OWSNavigationChildContro
aepTextView.backgroundColor = .Signal.secondaryGroupedBackground
let copyToClipboardButton = UIButton(
configuration: .compactGray(title: OWSLocalizedString(
configuration: .smallSecondary(title: OWSLocalizedString(
"BACKUP_RECORD_KEY_COPY_TO_CLIPBOARD_BUTTON_TITLE",
comment: "Title for a button allowing users to copy their 'Recovery Key' to the clipboard."
)),

View File

@@ -137,8 +137,7 @@ class BackupSettingsViewController:
latestBackupExportProgressUpdate: nil,
latestBackupAttachmentDownloadUpdate: nil,
latestBackupAttachmentUploadUpdate: nil,
lastBackupDate: backupSettingsStore.lastBackupDate(tx: tx),
lastBackupSizeBytes: backupSettingsStore.lastBackupSizeBytes(tx: tx),
lastBackupDetails: backupSettingsStore.lastBackupDetails(tx: tx),
shouldAllowBackupUploadsOnCellular: backupSettingsStore.shouldAllowBackupUploadsOnCellular(tx: tx),
mediaTierCapacityOverflow: Self.getMediaTierCapacityOverflow(
backupAttachmentUploadStore: backupAttachmentUploadStore,
@@ -178,7 +177,7 @@ class BackupSettingsViewController:
case nil:
break
case .presentWelcomeToBackupsSheet:
presentWelcomeToBackupsSheetIfNecessary()
presentWelcomeToBackupsSheet()
}
}
@@ -258,8 +257,6 @@ class BackupSettingsViewController:
}
db.read { tx in
self.viewModel.lastBackupDate = self.backupSettingsStore.lastBackupDate(tx: tx)
self.viewModel.lastBackupSizeBytes = self.backupSettingsStore.lastBackupSizeBytes(tx: tx)
self.viewModel.hasBackupFailed = self.backupFailureStateManager.hasFailedBackup(tx: tx)
}
}
@@ -283,6 +280,16 @@ class BackupSettingsViewController:
viewModel.latestBackupAttachmentUploadUpdate = uploadUpdate
}
},
Task.detached { [weak self] in
for await _ in NotificationCenter.default.notifications(
named: .OWSApplicationDidBecomeActive,
) {
await MainActor.run { [weak self] in
guard let self else { return }
loadBackupSubscription()
}
}
},
Task.detached { [weak self] in
for await _ in NotificationCenter.default.notifications(
named: .backupPlanChanged
@@ -293,6 +300,16 @@ class BackupSettingsViewController:
}
}
},
Task.detached { [weak self] in
for await _ in NotificationCenter.default.notifications(
named: .lastBackupDetailsDidChange,
) {
await MainActor.run { [weak self] in
guard let self else { return }
_lastBackupDetailsDidChange()
}
}
},
Task.detached { [weak self] in
for await _ in NotificationCenter.default.notifications(
named: .shouldAllowBackupUploadsOnCellularChanged
@@ -395,8 +412,7 @@ class BackupSettingsViewController:
db.read { tx in
viewModel.backupPlan = backupPlanManager.backupPlan(tx: tx)
viewModel.failedToDisableBackupsRemotely = backupDisablingManager.disableRemotelyFailed(tx: tx)
viewModel.lastBackupDate = backupSettingsStore.lastBackupDate(tx: tx)
viewModel.lastBackupSizeBytes = backupSettingsStore.lastBackupSizeBytes(tx: tx)
viewModel.lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
viewModel.shouldAllowBackupUploadsOnCellular = backupSettingsStore.shouldAllowBackupUploadsOnCellular(tx: tx)
}
@@ -417,6 +433,12 @@ class BackupSettingsViewController:
}
}
private func _lastBackupDetailsDidChange() {
db.read { tx in
viewModel.lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
}
}
private func _shouldAllowBackupUploadsOnCellularDidChange() {
db.read { tx in
viewModel.shouldAllowBackupUploadsOnCellular = backupSettingsStore.shouldAllowBackupUploadsOnCellular(tx: tx)
@@ -473,25 +495,32 @@ class BackupSettingsViewController:
// MARK: - BackupSettingsViewModel.ActionsDelegate
fileprivate func enableBackups(
implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection?
planSelection: BackupSettingsViewModel.EnableBackupsPlanSelection,
shouldShowWelcomeToBackupsSheet: Bool,
) {
// TODO: [Backups] Show the rest of the onboarding flow.
Task {
if let planSelection = implicitPlanSelection {
switch planSelection {
case .required(let planSelection):
await _enableBackups(
fromViewController: self,
planSelection: planSelection
planSelection: planSelection,
shouldShowWelcomeToBackupsSheet: shouldShowWelcomeToBackupsSheet,
)
case .userChoice(let initialSelection):
await _showChooseBackupPlan(
initialPlanSelection: initialSelection,
shouldShowWelcomeToBackupsSheet: shouldShowWelcomeToBackupsSheet,
)
} else {
await _showChooseBackupPlan(initialPlanSelection: nil)
}
}
}
@MainActor
private func _showChooseBackupPlan(
initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?
initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?,
shouldShowWelcomeToBackupsSheet: Bool,
) async {
do throws(ActionSheetDisplayableError) {
let chooseBackupPlanViewController: ChooseBackupPlanViewController = try await .load(
@@ -503,7 +532,8 @@ class BackupSettingsViewController:
await _enableBackups(
fromViewController: chooseBackupPlanViewController,
planSelection: planSelection
planSelection: planSelection,
shouldShowWelcomeToBackupsSheet: shouldShowWelcomeToBackupsSheet,
)
}
}
@@ -521,7 +551,8 @@ class BackupSettingsViewController:
@MainActor
private func _enableBackups(
fromViewController: UIViewController,
planSelection: ChooseBackupPlanViewController.PlanSelection
planSelection: ChooseBackupPlanViewController.PlanSelection,
shouldShowWelcomeToBackupsSheet: Bool,
) async {
do throws(ActionSheetDisplayableError) {
try await backupEnablingManager.enableBackups(
@@ -530,24 +561,16 @@ class BackupSettingsViewController:
)
navigationController?.popToViewController(self, animated: true) { [self] in
presentWelcomeToBackupsSheetIfNecessary()
if shouldShowWelcomeToBackupsSheet {
presentWelcomeToBackupsSheet()
}
}
} catch {
error.showActionSheet(from: fromViewController)
}
}
private func presentWelcomeToBackupsSheetIfNecessary() {
let hasNeverRunBackups = db.read { tx in
backupSettingsStore.firstBackupDate(tx: tx) == nil
}
if hasNeverRunBackups {
_presentWelcomeToBackupsSheet()
}
}
private func _presentWelcomeToBackupsSheet() {
private func presentWelcomeToBackupsSheet() {
final class WelcomeToBackupsSheet: HeroSheetViewController {
override var canBeDismissed: Bool { false }
@@ -714,8 +737,13 @@ class BackupSettingsViewController:
return
}
withAnimation {
viewModel.backupSubscriptionLoadingState = .loading
switch viewModel.backupSubscriptionLoadingState {
case .loading, .loaded:
break
case .networkError, .genericError:
withAnimation {
viewModel.backupSubscriptionLoadingState = .loading
}
}
let newLoadingState: BackupSettingsViewModel.BackupSubscriptionLoadingState
@@ -819,12 +847,6 @@ class BackupSettingsViewController:
// MARK: -
fileprivate func showChooseBackupPlan(initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?) {
Task {
await _showChooseBackupPlan(initialPlanSelection: initialPlanSelection)
}
}
fileprivate func showAppStoreManageSubscriptions() {
guard let windowScene = view.window?.windowScene else {
owsFailDebug("Missing window scene!")
@@ -1427,13 +1449,16 @@ class BackupSettingsViewController:
// MARK: -
private class BackupSettingsViewModel: ObservableObject {
protocol ActionsDelegate: AnyObject {
func enableBackups(implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection?)
enum EnableBackupsPlanSelection {
case required(ChooseBackupPlanViewController.PlanSelection)
case userChoice(initialSelection: ChooseBackupPlanViewController.PlanSelection?)
}
protocol ActionsDelegate: AnyObject {
func enableBackups(planSelection: EnableBackupsPlanSelection, shouldShowWelcomeToBackupsSheet: Bool)
func disableBackups()
func loadBackupSubscription()
func showChooseBackupPlan(initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?)
func showAppStoreManageSubscriptions()
func performManualBackup()
@@ -1454,8 +1479,8 @@ private class BackupSettingsViewModel: ObservableObject {
func showBackgroundAppRefreshDisabledWarningSheet()
}
enum BackupSubscriptionLoadingState {
enum LoadedBackupSubscription {
enum BackupSubscriptionLoadingState: Equatable {
enum LoadedBackupSubscription: Equatable {
case freeAndEnabled
case freeAndDisabled
case paidButFreeForTesters
@@ -1484,8 +1509,7 @@ private class BackupSettingsViewModel: ObservableObject {
@Published var latestBackupAttachmentDownloadUpdate: BackupSettingsAttachmentDownloadTracker.DownloadUpdate?
@Published var latestBackupAttachmentUploadUpdate: BackupSettingsAttachmentUploadTracker.UploadUpdate?
@Published var lastBackupDate: Date?
@Published var lastBackupSizeBytes: UInt64?
@Published var lastBackupDetails: BackupSettingsStore.LastBackupDetails?
@Published var shouldAllowBackupUploadsOnCellular: Bool
/// Nil means has not consumed capacity; non-nil value represents the total byte count over
@@ -1511,8 +1535,7 @@ private class BackupSettingsViewModel: ObservableObject {
latestBackupExportProgressUpdate: OWSSequentialProgress<BackupExportJobStep>?,
latestBackupAttachmentDownloadUpdate: BackupSettingsAttachmentDownloadTracker.DownloadUpdate?,
latestBackupAttachmentUploadUpdate: BackupSettingsAttachmentUploadTracker.UploadUpdate?,
lastBackupDate: Date?,
lastBackupSizeBytes: UInt64?,
lastBackupDetails: BackupSettingsStore.LastBackupDetails?,
shouldAllowBackupUploadsOnCellular: Bool,
mediaTierCapacityOverflow: UInt64?,
hasBackupFailed: Bool,
@@ -1530,8 +1553,7 @@ private class BackupSettingsViewModel: ObservableObject {
self.latestBackupAttachmentDownloadUpdate = latestBackupAttachmentDownloadUpdate
self.latestBackupAttachmentUploadUpdate = latestBackupAttachmentUploadUpdate
self.lastBackupDate = lastBackupDate
self.lastBackupSizeBytes = lastBackupSizeBytes
self.lastBackupDetails = lastBackupDetails
self.shouldAllowBackupUploadsOnCellular = shouldAllowBackupUploadsOnCellular
self.mediaTierCapacityOverflow = mediaTierCapacityOverflow
@@ -1541,8 +1563,11 @@ private class BackupSettingsViewModel: ObservableObject {
// MARK: -
func enableBackups(implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection?) {
actionsDelegate?.enableBackups(implicitPlanSelection: implicitPlanSelection)
func enableBackups(planSelection: EnableBackupsPlanSelection, shouldShowWelcomeToBackupsSheet: Bool) {
actionsDelegate?.enableBackups(
planSelection: planSelection,
shouldShowWelcomeToBackupsSheet: shouldShowWelcomeToBackupsSheet,
)
}
func disableBackups() {
@@ -1566,10 +1591,6 @@ private class BackupSettingsViewModel: ObservableObject {
actionsDelegate?.loadBackupSubscription()
}
func showChooseBackupPlan(initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?) {
actionsDelegate?.showChooseBackupPlan(initialPlanSelection: initialPlanSelection)
}
func showAppStoreManageSubscriptions() {
actionsDelegate?.showAppStoreManageSubscriptions()
}
@@ -1887,8 +1908,7 @@ struct BackupSettingsView: View {
SignalSection {
BackupDetailsView(
lastBackupDate: viewModel.lastBackupDate,
lastBackupSizeBytes: viewModel.lastBackupSizeBytes,
lastBackupDetails: viewModel.lastBackupDetails,
shouldAllowBackupUploadsOnCellular: viewModel.shouldAllowBackupUploadsOnCellular,
viewModel: viewModel,
)
@@ -2058,7 +2078,7 @@ private struct ReenableBackupsButton: View {
let backupSubscriptionLoadingState: BackupSettingsViewModel.BackupSubscriptionLoadingState
let viewModel: BackupSettingsViewModel
private var implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection?? {
private var enableBackupsPlanSelection: BackupSettingsViewModel.EnableBackupsPlanSelection? {
switch backupSubscriptionLoadingState {
case .loading, .networkError:
// Don't let them reenable until we know more.
@@ -2071,18 +2091,20 @@ private struct ReenableBackupsButton: View {
.loaded(.paidButFailedToRenew),
.loaded(.paidButIAPNotFoundLocally),
.genericError:
// Let them choose with which plan to reenable.
return .some(nil)
return .userChoice(initialSelection: nil)
case .loaded(.paid), .loaded(.paidButExpiring):
// They're currently paid, so automatically reenable with paid.
return .paid
return .required(.paid)
}
}
var body: some View {
if let implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection? {
if let enableBackupsPlanSelection {
Button {
viewModel.enableBackups(implicitPlanSelection: implicitPlanSelection)
viewModel.enableBackups(
planSelection: enableBackupsPlanSelection,
shouldShowWelcomeToBackupsSheet: true,
)
} label: {
Text(OWSLocalizedString(
"BACKUP_SETTINGS_REENABLE_BACKUPS_BUTTON_TITLE",
@@ -2806,7 +2828,10 @@ private struct BackupSubscriptionLoadedView: View {
"BACKUP_SETTINGS_BACKUP_PLAN_FREE_ACTION_BUTTON_TITLE",
comment: "Title for a button allowing users to upgrade from a free to paid backup plan."
), action: {
viewModel.showChooseBackupPlan(initialPlanSelection: .free)
viewModel.enableBackups(
planSelection: .userChoice(initialSelection: .free),
shouldShowWelcomeToBackupsSheet: false,
)
}
)
case .freeAndDisabled:
@@ -2818,7 +2843,10 @@ private struct BackupSubscriptionLoadedView: View {
"BACKUP_SETTINGS_BACKUP_PLAN_PAID_BUT_FREE_FOR_TESTERS_ACTION_BUTTON_TITLE",
comment: "Title for a button allowing users to manage their backup plan as a tester."
), action: {
viewModel.showChooseBackupPlan(initialPlanSelection: .paid)
viewModel.enableBackups(
planSelection: .userChoice(initialSelection: .paid),
shouldShowWelcomeToBackupsSheet: false,
)
}
)
case .paid:
@@ -2857,7 +2885,10 @@ private struct BackupSubscriptionLoadedView: View {
),
expandWidth: true,
action: {
viewModel.showChooseBackupPlan(initialPlanSelection: nil)
viewModel.enableBackups(
planSelection: .userChoice(initialSelection: nil),
shouldShowWelcomeToBackupsSheet: false,
)
},
)
@@ -2898,15 +2929,14 @@ private struct BackupSubscriptionLoadedView: View {
// MARK: -
private struct BackupDetailsView: View {
let lastBackupDate: Date?
let lastBackupSizeBytes: UInt64?
let lastBackupDetails: BackupSettingsStore.LastBackupDetails?
let shouldAllowBackupUploadsOnCellular: Bool
let viewModel: BackupSettingsViewModel
var body: some View {
HStack {
let lastBackupMessage: String? = {
guard let lastBackupDate else {
guard let lastBackupDate = lastBackupDetails?.date else {
return nil
}
@@ -2948,7 +2978,7 @@ private struct BackupDetailsView: View {
}
}
if let lastBackupSizeBytes {
if let lastBackupSizeBytes = lastBackupDetails?.backupTotalSizeBytes {
HStack {
Text(OWSLocalizedString(
"BACKUP_SETTINGS_ENABLED_BACKUP_SIZE_LABEL",
@@ -3036,11 +3066,10 @@ private extension BackupSettingsViewModel {
isBackgroundAppRefreshDisabled: Bool = false,
) -> BackupSettingsViewModel {
class PreviewActionsDelegate: ActionsDelegate {
func enableBackups(implicitPlanSelection: ChooseBackupPlanViewController.PlanSelection?) { print("Enabling! implicitPlanSelection: \(implicitPlanSelection as Any)") }
func enableBackups(planSelection: EnableBackupsPlanSelection, shouldShowWelcomeToBackupsSheet: Bool) { print("Enabling! planSelection: \(planSelection)") }
func disableBackups() { print("Disabling!") }
func loadBackupSubscription() { print("Loading BackupSubscription!") }
func showChooseBackupPlan(initialPlanSelection: ChooseBackupPlanViewController.PlanSelection?) { print("ChooseBackupPlan! \(initialPlanSelection as Any)") }
func showAppStoreManageSubscriptions() { print("AppStore Manage Subscriptions!") }
func performManualBackup() { print("Manually backing up!") }
@@ -3085,8 +3114,11 @@ private extension BackupSettingsViewModel {
totalBytesToUpload: 1_600_000_000,
)
},
lastBackupDate: Date().addingTimeInterval(-1 * .day),
lastBackupSizeBytes: 2_400_000_000,
lastBackupDetails: BackupSettingsStore.LastBackupDetails(
date: Date().addingTimeInterval(-1 * .day),
backupFileSizeBytes: 40_000_000,
backupTotalSizeBytes: 2_400_000_000,
),
shouldAllowBackupUploadsOnCellular: false,
mediaTierCapacityOverflow: mediaTierCapacityOverflow,
hasBackupFailed: hasBackupFailed,

View File

@@ -71,7 +71,7 @@ final class AdHocCallStateObserver {
}
if callLink.adminPasskey == nil, !callLink.isDeleted {
let updateSender = CallLinkUpdateMessageSender(messageSenderJobQueue: messageSenderJobQueue)
updateSender.sendCallLinkUpdateMessage(rootKey: rootKey, adminPasskey: nil, tx: SDSDB.shimOnlyBridge(tx))
updateSender.sendCallLinkUpdateMessage(rootKey: rootKey, adminPasskey: nil, tx: tx)
}
try adHocCallRecordManager.createOrUpdateRecord(
callId: callIdFromEra(eraId),

View File

@@ -38,7 +38,7 @@ class GroupCallRecordRingingCleanupManager {
private let db: any DB
private let interactionStore: InteractionStore
private let groupCallPeekClient: GroupCallPeekClient
private let notificationPresenter: Shims.NotificationPresenter
private let notificationPresenter: NotificationPresenter
private let threadStore: ThreadStore
init(
@@ -55,7 +55,7 @@ class GroupCallRecordRingingCleanupManager {
self.db = db
self.interactionStore = interactionStore
self.groupCallPeekClient = groupCallPeekClient
self.notificationPresenter = Wrappers.NotificationPresenter(notificationPresenter: notificationPresenter)
self.notificationPresenter = notificationPresenter
self.threadStore = threadStore
}
@@ -155,53 +155,13 @@ class GroupCallRecordRingingCleanupManager {
continue
}
self.notificationPresenter.notifyUserGroupCallStarted(
groupCallInteraction: groupCallInteraction,
groupThread: groupThread,
tx: tx
self.notificationPresenter.notifyUser(
forPreviewableInteraction: groupCallInteraction,
thread: groupThread,
wantsSound: true,
transaction: tx
)
}
}
}
}
// MARK: - Shims
private extension GroupCallRecordRingingCleanupManager {
enum Shims {
typealias NotificationPresenter = GroupCallRecordRingingCleanupManager_NotificationPresenter_Shim
}
enum Wrappers {
typealias NotificationPresenter = GroupCallRecordRingingCleanupManager_NotificationPresenter_Wrapper
}
}
private protocol GroupCallRecordRingingCleanupManager_NotificationPresenter_Shim {
func notifyUserGroupCallStarted(
groupCallInteraction: OWSGroupCallMessage,
groupThread: TSGroupThread,
tx: DBWriteTransaction
)
}
private class GroupCallRecordRingingCleanupManager_NotificationPresenter_Wrapper: GroupCallRecordRingingCleanupManager_NotificationPresenter_Shim {
private let notificationPresenter: any NotificationPresenter
init(notificationPresenter: any NotificationPresenter) {
self.notificationPresenter = notificationPresenter
}
func notifyUserGroupCallStarted(
groupCallInteraction: OWSGroupCallMessage,
groupThread: TSGroupThread,
tx: DBWriteTransaction
) {
notificationPresenter.notifyUser(
forPreviewableInteraction: groupCallInteraction,
thread: groupThread,
wantsSound: true,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
}

View File

@@ -82,7 +82,7 @@ final class GroupCallSheetDataSource<Call: GroupCall>: CallDrawerSheetDataSource
)
comparableName = .nameValue(resolvedName)
} else {
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: member.address, tx: SDSDB.shimOnlyBridge(tx))
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: member.address, tx: tx)
resolvedName = displayName.resolvedValue(config: config.displayNameConfig)
comparableName = displayName.comparableValue(config: config)
}
@@ -130,7 +130,7 @@ final class GroupCallSheetDataSource<Call: GroupCall>: CallDrawerSheetDataSource
members += self.ringRtcCall.peekInfo?.joinedMembers.map { aciUuid in
let aci = Aci(fromUUID: aciUuid)
let address = SignalServiceAddress(aci)
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: SDSDB.shimOnlyBridge(tx))
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx)
let isUnknown = switch displayName {
case .nickname, .systemContactName, .profileName, .phoneNumber, .username:
false
@@ -247,7 +247,7 @@ class IndividualCallSheetDataSource: CallDrawerSheetDataSource {
if let remoteServiceId = thread.contactAddress.serviceId {
let remoteDisplayName = SSKEnvironment.shared.contactManagerRef.displayName(
for: thread.contactAddress,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
).resolvedValue()
let remoteComparableName: DisplayName.ComparableValue = .nameValue(remoteDisplayName)
members.append(JoinedMember(

View File

@@ -117,18 +117,18 @@ class CallLinkApprovalRequestDetailsSheet: OWSTableSheetViewController {
)
let (contactTitle, mutualThreads): (NSAttributedString, [TSGroupThread]) = self.deps.db.read { tx in
avatarView.update(SDSDB.shimOnlyBridge(tx)) { config in
avatarView.update(tx) { config in
config.dataSource = .address(self.approvalRequest.address)
}
let isSystemContact = self.deps.contactsManager.fetchSignalAccount(
for: self.approvalRequest.address,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
) != nil
let mutualThreads = TSGroupThread.groupThreads(
with: self.approvalRequest.address,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
.filter(\.groupModel.groupMembership.isLocalUserFullMember)
.filter(\.shouldThreadBeVisible)
@@ -138,7 +138,7 @@ class CallLinkApprovalRequestDetailsSheet: OWSTableSheetViewController {
isNoteToSelf: false,
isSystemContact: isSystemContact,
canTap: true,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
return (contactTitle, mutualThreads)

View File

@@ -78,7 +78,7 @@ class CallLinkApprovalViewModel: ObservableObject {
let names = self.deps.db.read { tx in
self.deps.contactsManager.displayNames(
for: acis.map(SignalServiceAddress.init),
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
}.map { displayName in
displayName.resolvedValue(config: nameConfig, useShortNameIfAvailable: false)

View File

@@ -37,7 +37,7 @@ public class CallLinkProfileKeySharingManager {
let address = SignalServiceAddress(aci)
let isBlocked = blockingManager.isAddressBlocked(
address,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
let isEligible = !isLocal && !isBlocked
@@ -55,7 +55,7 @@ public class CallLinkProfileKeySharingManager {
self.consideredAcis.formUnion(eligibleAcisNotSentProfileKeyYet)
db.asyncWrite { tx in
let profileManager = SSKEnvironment.shared.profileManagerRef
let profileKey = profileManager.localProfileKey(tx: SDSDB.shimOnlyBridge(tx))!
let profileKey = profileManager.localProfileKey(tx: tx)!
for aci in eligibleAcisNotSentProfileKeyYet {
self.sendProfileKey(profileKey, toAci: aci, tx: tx)
}
@@ -67,7 +67,7 @@ public class CallLinkProfileKeySharingManager {
let profileKeyMessage = OWSProfileKeyMessage(
thread: thread,
profileKey: profileKey.serialize(),
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: profileKeyMessage
@@ -75,7 +75,7 @@ public class CallLinkProfileKeySharingManager {
let sendPromise = SSKEnvironment.shared.messageSenderJobQueueRef.add(
.promise,
message: preparedMessage,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
Task { @MainActor in
do {

View File

@@ -299,10 +299,7 @@ final class CallLinkViewController: OWSTableViewController2 {
private func copyCallLink() {
self.persistIfNeeded()
UIPasteboard.general.url = self.callLink.url()
self.presentToast(text: OWSLocalizedString(
"COPIED_TO_CLIPBOARD",
comment: "Indicator that a value has been copied to the clipboard."
))
self.presentToast(text: CommonStrings.copiedToClipboardToast)
}
private func shareCallLinkViaSystem() {

View File

@@ -121,8 +121,6 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
self.toolbarItems = [.flexibleSpace(), toolbarDeleteButton]
}
view.addSubview(tableView)
tableView.autoPinEdgesToSuperviewEdges()
tableView.delegate = self
tableView.allowsSelectionDuringEditing = true
tableView.allowsMultipleSelectionDuringEditing = true
@@ -132,6 +130,14 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
tableView.register(CreateCallLinkCell.self, forCellReuseIdentifier: Self.createCallLinkReuseIdentifier)
tableView.register(CallCell.self, forCellReuseIdentifier: Self.callCellReuseIdentifier)
tableView.dataSource = dataSource
view.addSubview(tableView)
tableView.autoPinHeight(toHeightOf: view)
tableViewHorizontalEdgeConstraints = [
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
]
NSLayoutConstraint.activate(tableViewHorizontalEdgeConstraints)
updateTableViewPaddingIfNeeded()
view.addSubview(emptyStateMessageView)
emptyStateMessageView.autoCenterInSuperview()
@@ -159,6 +165,11 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
isPeekingEnabled = false
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateTableViewPaddingIfNeeded()
}
func updateBarButtonItems() {
if tableView.isEditing {
navigationItem.leftBarButtonItem = cancelMultiselectButton()
@@ -894,7 +905,7 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
forCallRecords: callRecords,
upcomingCallLinkRowId: nil,
deps: capturedDeps,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
},
callViewModelForUpcomingCallLink: { callLinkRowId, tx in
@@ -902,13 +913,13 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
forCallRecords: [],
upcomingCallLinkRowId: callLinkRowId,
deps: capturedDeps,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
},
fetchCallRecordBlock: { callRecordId, tx -> CallRecord? in
return capturedDeps.callRecordStore.fetch(
callRecordId: callRecordId,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
).unwrapped
},
shouldFetchUpcomingCallLinks: !onlyLoadMissedCalls && onlyMatchThreadRowIds == nil
@@ -1471,6 +1482,32 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
let tableView = UITableView(frame: .zero, style: .plain)
/// Set to `true` when call list is displayed in split view controller's "sidebar" on iOS 26 and later.
/// Setting this to `true` would add an extra padding on both sides of the table view.
/// This value is also passed down to table view cells that make their own layout choices based on the value.
private var useSidebarCallListCellAppearance = false {
didSet {
guard oldValue != useSidebarCallListCellAppearance else { return }
tableViewHorizontalEdgeConstraints.forEach {
$0.constant = useSidebarCallListCellAppearance ? 18 : 0
}
tableView.reloadData()
}
}
private var tableViewHorizontalEdgeConstraints: [NSLayoutConstraint] = []
/// iOS 26+: checks if this VC is displayed in the collapsed split view controller and updates `useSidebarCallListCellAppearance` accordingly.
/// Does nothing on prior iOS versions.
private func updateTableViewPaddingIfNeeded() {
guard #available(iOS 26, *) else { return }
if let splitViewController = splitViewController, !splitViewController.isCollapsed {
useSidebarCallListCellAppearance = true
} else {
useSidebarCallListCellAppearance = false
}
}
private static let createCallLinkReuseIdentifier = "createCallLink"
private static let callCellReuseIdentifier = "callCell"
@@ -1483,35 +1520,36 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer
private func buildTableViewCell(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell? {
switch Section(rawValue: indexPath.section) {
case .createCallLink:
if let createCallLinkCell = tableView.dequeueReusableCell(
guard let createCallLinkCell = tableView.dequeueReusableCell(
withIdentifier: Self.createCallLinkReuseIdentifier,
for: indexPath
) as? CreateCallLinkCell {
return createCallLinkCell
}
return nil
) as? CreateCallLinkCell else { return nil }
createCallLinkCell.useSidebarAppearance = useSidebarCallListCellAppearance
return createCallLinkCell
case .existingCalls:
guard
let callCell = tableView.dequeueReusableCell(
withIdentifier: Self.callCellReuseIdentifier
) as? CallCell
else {
return nil
}
guard let callCell = tableView.dequeueReusableCell(
withIdentifier: Self.callCellReuseIdentifier,
for: indexPath
) as? CallCell else { return nil }
callCell.useSidebarAppearance = useSidebarCallListCellAppearance
// These loads should be sufficiently fast that doing them here,
// synchronously, is fine.
self.loadMoreCallsIfNecessary(indexToBeDisplayed: indexPath.row)
loadMoreCallsIfNecessary(indexToBeDisplayed: indexPath.row)
if let viewModel = viewModelLoader.viewModel(at: indexPath.row, sneakyTransactionDb: deps.db) {
callCell.delegate = self
callCell.viewModel = viewModel
self.peekIfActive(viewModel)
peekIfActive(viewModel)
return callCell
}
owsFailDebug("Missing cached view model how did this happen?")
/// Return an empty table cell, rather than a ``CallCell`` that's
/// gonna be incorrectly configured.
case .none:
break
}
@@ -2237,6 +2275,9 @@ private extension CallsListViewController {
}
}
/// If set to `true` background in `selected` state would have rounded corners.
var useSidebarAppearance = false
// MARK: Subviews
private lazy var avatarView = ConversationAvatarView(
@@ -2485,7 +2526,7 @@ private extension CallsListViewController {
var configuration = UIBackgroundConfiguration.clear()
if state.isSelected || state.isHighlighted {
configuration.backgroundColor = Theme.tableCell2SelectedBackgroundColor
if traitCollection.userInterfaceIdiom == .pad {
if useSidebarAppearance {
configuration.cornerRadius = 36
}
} else {
@@ -2513,6 +2554,9 @@ private extension CallsListViewController {
private extension CallsListViewController {
class CreateCallLinkCell: UITableViewCell {
/// If set to `true` background in `selected` state would have rounded corners.
var useSidebarAppearance = false
private enum Constants {
static let iconDimension: CGFloat = 24
static let spacing: CGFloat = 18
@@ -2543,7 +2587,7 @@ private extension CallsListViewController {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .Signal.background
automaticallyUpdatesBackgroundConfiguration = false
let stackView = UIStackView(arrangedSubviews: [iconView, label])
stackView.axis = .horizontal
@@ -2557,5 +2601,18 @@ private extension CallsListViewController {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConfiguration(using state: UICellConfigurationState) {
var configuration = UIBackgroundConfiguration.clear()
if state.isSelected || state.isHighlighted {
configuration.backgroundColor = Theme.tableCell2SelectedBackgroundColor
if useSidebarAppearance {
configuration.cornerRadius = 36
}
} else {
configuration.backgroundColor = .Signal.background
}
backgroundConfiguration = configuration
}
}
}

View File

@@ -1,33 +0,0 @@
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
public import SignalUI
public class GroupCallTooltip: TooltipView {
public class func present(fromView: UIView,
widthReferenceView: UIView,
tailReferenceView: UIView,
wasTappedBlock: (() -> Void)?) -> GroupCallTooltip {
return GroupCallTooltip(fromView: fromView, widthReferenceView: widthReferenceView, tailReferenceView: tailReferenceView, wasTappedBlock: wasTappedBlock)
}
public override func bubbleContentView() -> UIView {
let label = UILabel()
label.text = OWSLocalizedString(
"GROUP_CALL_START_TOOLTIP",
comment: "Tooltip highlighting group calls."
)
label.font = UIFont.dynamicTypeSubheadline
label.textColor = UIColor.ows_white
return horizontalStack(forSubviews: [label])
}
public override var bubbleColor: UIColor { .ows_accentGreen }
public override var tailDirection: TooltipView.TailDirection { .up }
}

View File

@@ -337,8 +337,8 @@ final class GroupCallViewController: UIViewController {
}
static func presentLobby(forGroupId groupId: GroupIdentifier, videoMuted: Bool = false) {
self._presentLobby { viewController, modalViewController in
let result = await self._prepareLobby(
self._presentLobby { viewController, modalViewController -> (() -> Void)? in
return await self._prepareLobby(
from: viewController,
modalViewController: modalViewController,
shouldAskForCameraPermission: !videoMuted,
@@ -347,11 +347,6 @@ final class GroupCallViewController: UIViewController {
return callService.buildAndConnectGroupCall(for: groupId, isVideoMuted: videoMuted)
}
)
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
// Dismiss the group call tooltip
SSKEnvironment.shared.preferencesRef.setWasGroupCallTooltipShown(tx: tx)
}
return result
}
}

View File

@@ -130,7 +130,6 @@ public class CVCell: UICollectionViewCell, CVRootComponentHost {
private func applyLastLayoutAttributes() {
guard let layoutAttributes = self.lastLayoutAttributes else {
Logger.verbose("Missing layoutAttributes.")
return
}

View File

@@ -25,7 +25,7 @@ public class CVViewState: NSObject {
public var bottomBarContainer = UIView.container()
public var requestView: UIView?
public var bannerView: UIView?
public var bannerStackView: UIView?
public var groupNameCollisionFinder: GroupMembershipNameCollisionFinder?
public var isDismissingInteractively = false
@@ -90,10 +90,6 @@ public class CVViewState: NSObject {
public var userHasScrolled = false
public var groupCallTooltip: GroupCallTooltip?
public var groupCallTooltipTailReferenceView: UIView?
public var didAlreadyShowGroupCallTooltipEnoughTimes: Bool
public var hasIncrementedGroupCallTooltipShownCount = false
public var groupCallBarButtonItem: UIBarButtonItem?
public var lastMessageSentDate: Date?
@@ -116,12 +112,13 @@ public class CVViewState: NSObject {
// MARK: - Gestures
public var collectionViewGestureRecongnizersConfigured = false
public let collectionViewTapGestureRecognizer = SingleOrDoubleTapGestureRecognizer()
public let collectionViewLongPressGestureRecognizer = UILongPressGestureRecognizer()
public let collectionViewContextMenuGestureRecognizer = UILongPressGestureRecognizer()
public var collectionViewContextMenuSecondaryClickRecognizer: UITapGestureRecognizer?
public var collectionViewContextMenuSecondaryClickRecognizer = UITapGestureRecognizer()
public let collectionViewPanGestureRecognizer = UIPanGestureRecognizer()
public var collectionViewActiveContextMenuInteraction: ChatHistoryContextMenuInteraction?
public var longPressHandler: CVLongPressHandler?
public var panHandler: CVPanHandler?
@@ -159,13 +156,11 @@ public class CVViewState: NSObject {
public init(
threadUniqueId: String,
conversationStyle: ConversationStyle,
didAlreadyShowGroupCallTooltipEnoughTimes: Bool,
chatColor: ColorOrGradientSetting,
wallpaperViewBuilder: WallpaperViewBuilder?
) {
self.threadUniqueId = threadUniqueId
self.conversationStyle = conversationStyle
self.didAlreadyShowGroupCallTooltipEnoughTimes = didAlreadyShowGroupCallTooltipEnoughTimes
self.chatColor = chatColor
self.wallpaperViewBuilder = wallpaperViewBuilder
}
@@ -204,9 +199,9 @@ extension ConversationViewController {
set { viewState.requestView = newValue }
}
var bannerView: UIView? {
get { viewState.bannerView }
set { viewState.bannerView = newValue }
var bannerStackView: UIView? {
get { viewState.bannerStackView }
set { viewState.bannerStackView = newValue }
}
var isDismissingInteractively: Bool {
@@ -279,6 +274,11 @@ extension ConversationViewController {
// MARK: - Gestures
var collectionViewGestureRecongnizersConfigured: Bool {
get { viewState.collectionViewGestureRecongnizersConfigured }
set { viewState.collectionViewGestureRecongnizersConfigured = newValue }
}
var collectionViewTapGestureRecognizer: SingleOrDoubleTapGestureRecognizer {
viewState.collectionViewTapGestureRecognizer
}
@@ -288,12 +288,9 @@ extension ConversationViewController {
var collectionViewContextMenuGestureRecognizer: UILongPressGestureRecognizer {
viewState.collectionViewContextMenuGestureRecognizer
}
var collectionViewContextMenuSecondaryClickRecognizer: UITapGestureRecognizer? {
get { viewState.collectionViewContextMenuSecondaryClickRecognizer }
set { viewState.collectionViewContextMenuSecondaryClickRecognizer = newValue }
var collectionViewContextMenuSecondaryClickRecognizer: UITapGestureRecognizer {
viewState.collectionViewContextMenuSecondaryClickRecognizer
}
var collectionViewPanGestureRecognizer: UIPanGestureRecognizer {
viewState.collectionViewPanGestureRecognizer
}

View File

@@ -397,6 +397,7 @@ public class CVPollView: ManualStackView {
func configureForRendering(
state: CVPollView.State,
previousPollState: CVPollView.State?,
cellMeasurement: CVCellMeasurement,
componentDelegate: CVComponentDelegate
) {
@@ -418,7 +419,9 @@ public class CVPollView: ManualStackView {
configurator: configurator,
cellMeasurement: cellMeasurement,
pollOption: option,
prevOption: previousPollState?.poll.optionForIndex(optionIndex: option.optionIndex),
totalVoters: poll.totalVoters(),
prevTotalVoters: previousPollState?.poll.totalVoters(),
localUserVoteState: localUserVoteState(localAci: state.localAci, option: option),
pollIsEnded: poll.isEnded,
pendingVotesCount: poll.pendingVotesCount(),
@@ -496,6 +499,7 @@ public class CVPollView: ManualStackView {
let progressBarBackground = UIView()
let progressBarContainer = ManualLayoutView(name: "progressBarContainer")
let generator = UINotificationFeedbackGenerator()
var didAnimate = false
var localUserVoteState: VoteState = .unvote
@@ -503,7 +507,9 @@ public class CVPollView: ManualStackView {
configurator: Configurator,
cellMeasurement: CVCellMeasurement,
pollOption: OWSPollOption,
prevOption: OWSPollOption?,
totalVoters: Int,
prevTotalVoters: Int?,
localUserVoteState: VoteState,
pollIsEnded: Bool,
pendingVotesCount: Int,
@@ -520,7 +526,9 @@ public class CVPollView: ManualStackView {
option: pollOption.text,
index: pollOption.optionIndex,
votes: pollOption.acis.count,
prevVotes: prevOption?.acis.count,
totalVoters: totalVoters,
prevTotalVoters: prevTotalVoters,
pollIsEnded: pollIsEnded,
pendingVotesCount: pendingVotesCount
)
@@ -544,7 +552,9 @@ public class CVPollView: ManualStackView {
private func buildProgressBar(
votes: Int,
prevVotes: Int?,
totalVoters: Int,
prevTotalVoters: Int?,
pollIsEnded: Bool,
foregroundColor: UIColor,
backgroundColor: UIColor,
@@ -582,18 +592,20 @@ public class CVPollView: ManualStackView {
Self.setSubviewFrame(subview: progressBarBackground, frame: subviewFrame)
})
// No need to render progress fill if votes are 0
if votes <= 0 {
return
}
progressBarContainer.addSubview(progressFill, withLayoutBlock: { [weak self] _ in
guard let self = self, let superview = progressFill.superview else {
owsFailDebug("Missing superview.")
return
}
let percent = Float(votes) / Float(totalVoters)
var percent = 0.0 as Float
if totalVoters > 0 {
percent = Float(votes) / Float(totalVoters)
}
var prevPercent = 0.0 as Float
if let prevVotes, let prevTotalVoters, prevTotalVoters > 0 {
prevPercent = Float(prevVotes) / Float(prevTotalVoters)
}
// The progress bar should start under the text, not the checkbox, so we need to shift it
// over the amount of the checkbox width (plus spacing), and remove that offset from the total size.
@@ -601,6 +613,7 @@ public class CVPollView: ManualStackView {
let checkboxOffset = pollIsEnded ? 0 : checkboxWidthWithSpacing
let adjustedContainerWidth = superview.bounds.width - checkboxOffset
let numVotesBarFill = CGFloat(percent) * adjustedContainerWidth
let prevNumVotesBarFill = CGFloat(prevPercent) * adjustedContainerWidth
// Origin references the left. If RTL, we want the origin to be its "finished" point, which is
// the total container size minus the fill size.
@@ -611,11 +624,39 @@ public class CVPollView: ManualStackView {
originX = superview.bounds.origin.x + checkboxOffset
}
let subviewFrame = CGRect(
var subviewFrame = CGRect(
origin: CGPoint(x: originX, y: superview.bounds.origin.y),
size: CGSize(width: numVotesBarFill, height: superview.bounds.height)
)
guard prevVotes != nil else {
// Don't animate if there's no previous state, just set to the final width.
subviewFrame.width = numVotesBarFill
Self.setSubviewFrame(subview: progressFill, frame: subviewFrame)
return
}
Self.setSubviewFrame(subview: progressFill, frame: subviewFrame)
if !didAnimate {
didAnimate = true
DispatchQueue.main.async { [weak self] in
self?.progressFill.frame.width = prevNumVotesBarFill
if prevNumVotesBarFill != numVotesBarFill {
UIView.animate(
withDuration: 0.25,
delay: 0.0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.0,
options: [],
animations: { [weak self] in
self?.progressFill.frame.width = numVotesBarFill
},
completion: nil
)
}
}
}
})
}
@@ -771,7 +812,9 @@ public class CVPollView: ManualStackView {
option: String,
index: UInt32,
votes: Int,
prevVotes: Int?,
totalVoters: Int,
prevTotalVoters: Int?,
pollIsEnded: Bool,
pendingVotesCount: Int
) {
@@ -822,7 +865,9 @@ public class CVPollView: ManualStackView {
buildProgressBar(
votes: votes,
prevVotes: prevVotes,
totalVoters: totalVoters,
prevTotalVoters: prevTotalVoters,
pollIsEnded: pollIsEnded,
foregroundColor: configurator.colorConfigurator.voteProgressForegroundColor,
backgroundColor: configurator.colorConfigurator.voteProgressBackgroundColor,

View File

@@ -0,0 +1,23 @@
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalUI
import UIKit
final class PinnedMessageIconView: ManualLayoutView {
private let imageView = CVImageView()
static let size: CGSize = .square(12)
init() {
super.init(name: "PinnedMessageIconView")
addSubviewToFillSuperviewEdges(imageView)
}
func configure(tintColor: UIColor) {
imageView.image = .pinFill
imageView.tintColor = tintColor
}
}

View File

@@ -39,6 +39,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
let accessibilityLabel: String?
let tapForMoreState: TapForMoreState
let displayEditedLabel: Bool
let isPinnedMessage: Bool
struct Expiration: Equatable {
let expirationTimestamp: UInt64
@@ -67,6 +68,9 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
private var expiration: State.Expiration? {
footerState.expiration
}
private var isPinnedMessage: Bool {
footerState.isPinnedMessage
}
let isOverlayingMedia: Bool
private let isOutsideBubble: Bool
@@ -149,6 +153,12 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
textColor = conversationStyle.bubbleSecondaryTextColor(isIncoming: isIncoming)
}
if isPinnedMessage {
let pinIconView = componentView.pinnedImageView
pinIconView.configure(tintColor: textColor)
innerViews.append(pinIconView)
}
if displayEditedLabel {
let editedLabel = componentView.editedLabel
editedLabelConfig(textColor: textColor).applyForRendering(label: editedLabel)
@@ -259,12 +269,14 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
shouldUseLongFormat: false,
hasBodyAttachments: hasBodyAttachments
)
return State(
timestampText: timestampText,
statusIndicator: nil,
accessibilityLabel: nil,
tapForMoreState: tapForMoreState,
displayEditedLabel: false,
isPinnedMessage: false,
expiration: nil
)
}
@@ -344,6 +356,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
accessibilityLabel: accessibilityLabel,
tapForMoreState: tapForMoreState,
displayEditedLabel: false,
isPinnedMessage: false,
expiration: expiration
)
}
@@ -376,6 +389,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
static func buildState(
interaction: TSInteraction,
tapForMoreState: TapForMoreState,
isPinnedMessage: Bool,
transaction: DBReadTransaction
) -> State {
@@ -452,6 +466,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
accessibilityLabel: accessibilityLabel,
tapForMoreState: tapForMoreState,
displayEditedLabel: displayEditedLabel,
isPinnedMessage: isPinnedMessage,
expiration: expiration
)
}
@@ -576,6 +591,11 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
// We always use a stretching spacer.
outerSubviewInfos.append(ManualStackSubviewInfo.empty)
if footerState.isPinnedMessage {
let pinIconSize = PinnedMessageIconView.size
innerSubviewInfos.append(pinIconSize.asManualSubviewInfo(hasFixedWidth: true))
}
if displayEditedLabel {
let editedLabelConfig = self.editedLabelConfig(textColor: .black)
let editedLabelSize = CVText.measureLabel(config: editedLabelConfig, maxWidth: maxWidth)
@@ -677,6 +697,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
fileprivate let messageTimerView = MessageTimerView()
fileprivate let smsLockIconView = SmsLockIconView()
fileprivate let chatColorView = CVColorOrGradientView()
fileprivate let pinnedImageView = PinnedMessageIconView()
public var isDedicatedCellView = false
@@ -706,6 +727,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
messageTimerView.removeFromSuperview()
smsLockIconView.removeFromSuperview()
pinnedImageView.removeFromSuperview()
chatColorView.reset()
chatColorView.removeFromSuperview()

View File

@@ -1846,7 +1846,13 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
.quotedReply: .quotedReply,
.paymentAttachment: .paymentMessage,
.archivedPaymentAttachment: .paymentMessage,
.poll: .poll
.poll: .poll,
// Bottom buttons, labels, and footers are associated
// with other components and should not have unique long-press actions.
.bottomButtons: .associatedSubcomponent,
.bottomLabel: .associatedSubcomponent,
.footer: .associatedSubcomponent
// TODO: linkPreview?
]
// Recognize the correct message type when tapping next to the message itself

View File

@@ -34,7 +34,12 @@ public class CVComponentPoll: CVComponentBase, CVComponent {
return
}
componentViewPoll.pollView.configureForRendering(state: poll.state, cellMeasurement: cellMeasurement, componentDelegate: componentDelegate)
componentViewPoll.pollView.configureForRendering(
state: poll.state,
previousPollState: poll.prevPollState,
cellMeasurement: cellMeasurement,
componentDelegate: componentDelegate
)
}
public func measure(maxWidth: CGFloat, measurementBuilder: SignalUI.CVCellMeasurement.Builder) -> CGSize {

View File

@@ -337,6 +337,7 @@ public class CVComponentState: Equatable {
struct Poll: Equatable {
let state: CVPollView.State
let prevPollState: CVPollView.State?
}
let poll: Poll?
@@ -348,6 +349,12 @@ public class CVComponentState: Equatable {
let titleSelectionBackgroundColor: UIColor
let action: CVMessageAction?
struct Expiration: Equatable {
let expirationTimestamp: UInt64
let expiresInSeconds: UInt32
}
let expiration: Expiration?
/// Represents users whose names appear in the title. Only applies to
/// system messages in group threads.
let namesInTitle: [ReferencedUser]
@@ -356,7 +363,8 @@ public class CVComponentState: Equatable {
title: NSAttributedString,
titleColor: UIColor,
titleSelectionBackgroundColor: UIColor,
action: CVMessageAction?
action: CVMessageAction?,
expiration: Expiration?
) {
let mutableTitle = NSMutableAttributedString(attributedString: title)
mutableTitle.removeAttribute(
@@ -368,6 +376,7 @@ public class CVComponentState: Equatable {
self.titleColor = titleColor
self.titleSelectionBackgroundColor = titleSelectionBackgroundColor
self.action = action
self.expiration = expiration
self.namesInTitle = {
// Extract the addresses for names in the string. These are only
@@ -1767,7 +1776,21 @@ fileprivate extension CVComponentState.Builder {
localAci: self.localAci
)
self.poll = Poll(state: state)
let prevPollState: CVComponentState.Poll?
if let prevRenderState = itemBuildingContext.prevRenderState,
let prevPollInteraction = prevRenderState.items.first(
where: {
$0.interactionUniqueId == message.uniqueId
}),
let _prevPollState = prevPollInteraction.componentState.poll
{
prevPollState = _prevPollState
} else {
prevPollState = nil
}
// Pass the previously rendered poll so we can animate.
self.poll = Poll(state: state, prevPollState: prevPollState?.state)
if poll.totalVoters() > 0 {
let title = poll.isEnded ? OWSLocalizedString(

View File

@@ -21,6 +21,7 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
typealias Action = CVMessageAction
fileprivate var action: Action? { systemMessage.action }
fileprivate var expiration: CVComponentState.SystemMessage.Expiration? { systemMessage.expiration }
init(itemModel: CVItemModel, systemMessage: CVComponentState.SystemMessage) {
self.systemMessage = systemMessage
@@ -55,6 +56,13 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
layoutMargins: cellLayoutMargins)
}
private var innerHStackConfig: CVStackViewConfig {
return CVStackViewConfig(axis: .horizontal,
alignment: .center,
spacing: 4,
layoutMargins: .zero)
}
private var innerVStackConfig: CVStackViewConfig {
let layoutMargins: UIEdgeInsets
@@ -132,18 +140,31 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
componentView.wasShowingSelectionUI = wasShowingSelectionUI
componentView.hasActionButton = hasActionButton
let innerHStack = componentView.innerHStack
let outerHStack = componentView.outerHStack
let innerVStack = componentView.innerVStack
let outerVStack = componentView.outerVStack
let selectionView = componentView.selectionView
let textLabel = componentView.textLabel
let messageTimerView = componentView.messageTimerView
// Configuring the text label should happen in both reuse and non-reuse
// scenarios
textLabel.configureForRendering(config: textLabelConfig, spoilerAnimationManager: componentDelegate.spoilerState.animationManager)
textLabel.view.accessibilityLabel = textLabelConfig.text.accessibilityDescription
if let expiration = expiration {
messageTimerView.configure(
expirationTimestampMs: expiration.expirationTimestamp,
disappearingMessageInterval: expiration.expiresInSeconds,
tintColor: systemMessage.titleColor
)
}
if isReusing {
innerHStack.configureForReuse(config: innerHStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerHStack)
innerVStack.configureForReuse(config: innerVStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerVStack)
@@ -160,9 +181,17 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
wallpaperBlurView.updateIfNecessary()
}
} else {
var innerVStackViews: [UIView] = [
var innerHStackViews: [UIView] = [
textLabel.view
]
if expiration != nil {
innerHStackViews.append(messageTimerView)
}
var innerVStackViews: [UIView] = [
innerHStack
]
let outerVStackViews = [
innerVStack
]
@@ -212,6 +241,10 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
innerVStackViews.append(button)
}
innerHStack.configure(config: innerHStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerHStack,
subviews: innerHStackViews)
innerVStack.configure(config: innerVStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerVStack,
@@ -370,6 +403,7 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
UIFont.dynamicTypeFootnote
}
private static let measurementKey_innerHStack = "CVComponentSystemMessage.measurementKey_innerHStack"
private static let measurementKey_outerHStack = "CVComponentSystemMessage.measurementKey_outerHStack"
private static let measurementKey_innerVStack = "CVComponentSystemMessage.measurementKey_innerVStack"
private static let measurementKey_outerVStack = "CVComponentSystemMessage.measurementKey_outerVStack"
@@ -399,8 +433,21 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
maxWidth: maxContentWidth
)
var innerHStackSubviewInfos = [ManualStackSubviewInfo]()
innerHStackSubviewInfos.append(textSize.size.asManualSubviewInfo)
if let infoMessage = interaction as? TSInfoMessage, infoMessage.hasPerConversationExpiration {
let timerSize = MessageTimerView.measureSize
innerHStackSubviewInfos.append(timerSize.asManualSubviewInfo(hasFixedWidth: true))
}
let innerHStackMeasurement = ManualStackView.measure(config: innerHStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_innerHStack,
subviewInfos: innerHStackSubviewInfos)
var innerVStackSubviewInfos = [ManualStackSubviewInfo]()
innerVStackSubviewInfos.append(textSize.size.asManualSubviewInfo)
innerVStackSubviewInfos.append(innerHStackMeasurement.measuredSize.asManualSubviewInfo)
if let action = action, !itemViewState.shouldCollapseSystemMessageAction {
let buttonLabelConfig = self.buttonLabelConfig(action: action)
let actionButtonSize = (CVText.measureLabel(config: buttonLabelConfig,
@@ -501,10 +548,12 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
// It could be the entire item or some part thereof.
public class CVComponentViewSystemMessage: NSObject, CVComponentView {
fileprivate let innerHStack = ManualStackView(name: "systemMessage.innerHStack")
fileprivate let outerHStack = ManualStackView(name: "systemMessage.outerHStack")
fileprivate let innerVStack = ManualStackView(name: "systemMessage.innerVStack")
fileprivate let outerVStack = ManualStackView(name: "systemMessage.outerVStack")
fileprivate let selectionView = MessageSelectionView()
fileprivate let messageTimerView = MessageTimerView()
fileprivate var wallpaperBlurView: CVWallpaperBlurView?
fileprivate func ensureWallpaperBlurView() -> CVWallpaperBlurView {
@@ -551,8 +600,12 @@ public class CVComponentSystemMessage: CVComponentBase, CVRootComponent {
outerHStack.reset()
innerVStack.reset()
outerVStack.reset()
innerHStack.reset()
textLabel.reset()
messageTimerView.prepareForReuse()
messageTimerView.removeFromSuperview()
wallpaperBlurView?.removeFromSuperview()
wallpaperBlurView?.resetContentAndConfiguration()
@@ -581,6 +634,7 @@ extension CVComponentSystemMessage {
static func buildComponentState(
title: NSAttributedString,
action: Action?,
expiration: CVComponentState.SystemMessage.Expiration?,
titleColor: UIColor? = nil,
titleSelectionBackgroundColor: UIColor? = nil
) -> CVComponentState.SystemMessage {
@@ -588,7 +642,8 @@ extension CVComponentSystemMessage {
title: title,
titleColor: titleColor ?? defaultTextColor,
titleSelectionBackgroundColor: titleSelectionBackgroundColor ?? defaultSelectionBackgroundColor,
action: action
action: action,
expiration: expiration
)
}
@@ -603,8 +658,9 @@ extension CVComponentSystemMessage {
threadViewModel: threadViewModel,
currentGroupThreadCallGroupId: currentGroupThreadCallGroupId,
transaction: transaction)
let expiration = Self.expiration(forInteraction: interaction, transaction: transaction)
return buildComponentState(title: title, action: action, titleColor: maybeOverrideTitleColor)
return buildComponentState(title: title, action: action, expiration: expiration, titleColor: maybeOverrideTitleColor)
}
private static func title(forInteraction interaction: TSInteraction,
@@ -1031,7 +1087,7 @@ extension CVComponentSystemMessage {
)
labelText.append(String(format: titleFormat, configuration.durationString))
return buildComponentState(title: labelText, action: nil)
return buildComponentState(title: labelText, action: nil, expiration: nil)
}
// MARK: - Actions
@@ -1398,4 +1454,32 @@ extension CVComponentSystemMessage {
return Action(title: title, accessibilityIdentifier: "group_call_button", action: .didTapGroupCall)
}
// MARK: - Expiration
static func expiration(
forInteraction interaction: TSInteraction,
transaction: DBReadTransaction
) -> CVComponentState.SystemMessage.Expiration? {
if let infoMessage = interaction as? TSInfoMessage {
return expiration(forInfoMessage: infoMessage, transaction: transaction)
} else {
// Expiration state not supported.
return nil
}
}
private static func expiration(
forInfoMessage infoMessage: TSInfoMessage,
transaction: DBReadTransaction
) -> CVComponentState.SystemMessage.Expiration? {
guard infoMessage.expiresAt > 0 && infoMessage.expiresInSeconds > 0 else {
return nil
}
return CVComponentState.SystemMessage.Expiration(
expirationTimestamp: infoMessage.expiresAt,
expiresInSeconds: infoMessage.expiresInSeconds
)
}
}

View File

@@ -13,15 +13,13 @@ class OutgoingLinkPreviewView: UIView {
directionalLayoutMargins = .init(top: 0, leading: 12, bottom: 0, trailing: 0)
#if compiler(>=6.2)
if #available(iOS 26, *) {
clipsToBounds = true
cornerConfiguration = .uniformCorners(radius: .containerConcentric(minimum: 12))
}
#endif
let backgroundView = UIView()
backgroundView.backgroundColor = .Signal.secondaryFill.resolvedForInputToolbar()
backgroundView.backgroundColor = .Signal.secondaryFill
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(backgroundView)
NSLayoutConstraint.activate([
@@ -48,7 +46,7 @@ class OutgoingLinkPreviewView: UIView {
override var bounds: CGRect {
didSet {
// Use `cornerConfiguration`.
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable { return }
if #available(iOS 26, *) { return }
// Mask to round corners.
let maskLayer = CAShapeLayer()
@@ -92,7 +90,7 @@ class OutgoingLinkPreviewView: UIView {
traitCollection.userInterfaceStyle == .dark
? UIColor(rgbHex: 0x787880, alpha: 0.4)
: UIColor(rgbHex: 0xF5F5F5, alpha: 0.9)
}.resolvedForInputToolbar()
}
cancelButton.configuration?.background.visualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
cancelButton.tintColor = ConversationInputToolbar.Style.primaryTextColor
cancelButton.configuration?.cornerStyle = .capsule
@@ -154,6 +152,7 @@ class OutgoingLinkPreviewView: UIView {
label.text = text
label.textColor = ConversationInputToolbar.Style.primaryTextColor
label.numberOfLines = 2
label.adjustsFontForContentSizeCategory = true
label.font = .dynamicTypeFootnote.semibold()
label.lineBreakMode = .byTruncatingTail
label.setContentHuggingVerticalHigh()
@@ -166,6 +165,7 @@ class OutgoingLinkPreviewView: UIView {
label.text = text
label.textColor = ConversationInputToolbar.Style.primaryTextColor
label.numberOfLines = 2
label.adjustsFontForContentSizeCategory = true
label.font = .dynamicTypeFootnote
label.lineBreakMode = .byTruncatingTail
label.setContentHuggingVerticalHigh()
@@ -181,6 +181,7 @@ class OutgoingLinkPreviewView: UIView {
label.text = text
label.textColor = ConversationInputToolbar.Style.secondaryTextColor
label.numberOfLines = 1
label.adjustsFontForContentSizeCategory = true
label.font = .dynamicTypeCaption1
label.lineBreakMode = .byTruncatingTail
label.setContentHuggingVerticalHigh()
@@ -294,6 +295,7 @@ class OutgoingLinkPreviewView: UIView {
titleLabel.text = CallStrings.signalCall
titleLabel.textColor = ConversationInputToolbar.Style.primaryTextColor
titleLabel.numberOfLines = 2
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.font = .dynamicTypeFootnote.semibold()
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.setContentHuggingVerticalHigh()
@@ -303,6 +305,7 @@ class OutgoingLinkPreviewView: UIView {
subtitleLabel.text = CallStrings.callLinkDescription
subtitleLabel.textColor = ConversationInputToolbar.Style.primaryTextColor
subtitleLabel.numberOfLines = 2
subtitleLabel.adjustsFontForContentSizeCategory = true
subtitleLabel.font = .dynamicTypeFootnote
subtitleLabel.lineBreakMode = .byTruncatingTail
subtitleLabel.setContentHuggingVerticalHigh()
@@ -317,6 +320,7 @@ class OutgoingLinkPreviewView: UIView {
label.text = text
label.textColor = ConversationInputToolbar.Style.secondaryTextColor
label.numberOfLines = 1
label.adjustsFontForContentSizeCategory = true
label.font = .dynamicTypeCaption1
label.lineBreakMode = .byTruncatingTail
label.setContentHuggingVerticalHigh()

View File

@@ -46,12 +46,15 @@ class QuotedReplyPreview: UIView, QuotedMessageSnippetViewDelegate {
// Background with rounded corners.
let backgroundView: UIView
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable {
if #available(iOS 26, *) {
clipsToBounds = true
cornerConfiguration = .uniformCorners(radius: .containerConcentric(minimum: 12))
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
// Colored overlay on top of blur.
let dimmingView = UIView()
dimmingView.backgroundColor = .Signal.secondaryFill.resolvedForInputToolbar()
dimmingView.backgroundColor = .Signal.secondaryFill
dimmingView.translatesAutoresizingMaskIntoConstraints = false
blurEffectView.contentView.addSubview(dimmingView)
NSLayoutConstraint.activate([
@@ -61,11 +64,6 @@ class QuotedReplyPreview: UIView, QuotedMessageSnippetViewDelegate {
dimmingView.bottomAnchor.constraint(equalTo: blurEffectView.bottomAnchor),
])
#if compiler(>=6.2)
clipsToBounds = true
cornerConfiguration = .uniformCorners(radius: .containerConcentric(minimum: 12))
#endif
contentView = blurEffectView.contentView
backgroundView = blurEffectView
} else {
@@ -77,7 +75,7 @@ class QuotedReplyPreview: UIView, QuotedMessageSnippetViewDelegate {
}
)
backgroundView.layer.mask = maskLayer
backgroundView.backgroundColor = .Signal.secondaryFill.resolvedForInputToolbar()
backgroundView.backgroundColor = .Signal.secondaryFill
}
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(backgroundView)
@@ -395,7 +393,7 @@ private class QuotedMessageSnippetView: UIView {
horizonalStack.spacing = 8
let stripeView = UIView()
stripeView.backgroundColor = .Signal.quaternaryLabel.resolvedForInputToolbar()
stripeView.backgroundColor = .Signal.quaternaryLabel
horizonalStack.addArrangedSubview(stripeView)
#if compiler(>=6.2)
if #available(iOS 26, *) {
@@ -562,10 +560,10 @@ private class QuotedMessageSnippetView: UIView {
} else if attachment.asAnyPointer() != nil {
let refreshIcon = buildImageView(image: UIImage(imageLiteralResourceName: "refresh"))
refreshIcon.contentMode = .scaleAspectFit
refreshIcon.tintColor = .Signal.tertiaryLabel.resolvedForInputToolbar()
refreshIcon.tintColor = .Signal.tertiaryLabel
let containerView = UIView.container()
containerView.backgroundColor = .Signal.tertiaryBackground.resolvedForInputToolbar()
containerView.backgroundColor = .Signal.tertiaryBackground
containerView.addSubview(refreshIcon)
refreshIcon.translatesAutoresizingMaskIntoConstraints = false
containerView.addConstraints([

View File

@@ -16,6 +16,10 @@ extension ConversationInputToolbar {
directionalLayoutMargins = .init(top: 12, leading: 8, bottom: 8, trailing: 8)
if #available(iOS 26, *) {
visualEffectView.contentView.clipsToBounds = true
visualEffectView.contentView.cornerConfiguration = .capsule()
}
addSubview(visualEffectView)
visualEffectView.contentView.addSubview(lockIconView)
visualEffectView.contentView.addSubview(chevronView)
@@ -28,12 +32,6 @@ extension ConversationInputToolbar {
chevronView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
iconSpacingConstraint,
])
#if compiler(>=6.2)
if #available(iOS 26, *) {
visualEffectView.cornerConfiguration = .capsule()
}
#endif
}
required init?(coder aDecoder: NSCoder) {
@@ -45,11 +43,7 @@ extension ConversationInputToolbar {
visualEffectView.frame = bounds
var cantUseCornerConfiguration = true
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable {
cantUseCornerConfiguration = false
}
if cantUseCornerConfiguration {
if #unavailable(iOS 26) {
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(ovalIn: bounds).cgPath
visualEffectView.layer.mask = maskLayer
@@ -82,17 +76,12 @@ extension ConversationInputToolbar {
private lazy var visualEffectView: UIVisualEffectView = {
let visualEffect: UIVisualEffect = {
#if compiler(>=6.2)
if #available(iOS 26, *) {
return UIGlassEffect(style: .regular)
UIGlassEffect(style: .regular)
} else {
return UIBlurEffect(style: .systemThinMaterial)
UIBlurEffect(style: .systemThinMaterial)
}
#else
UIBlurEffect(style: .systemThinMaterial)
#endif
}()
return UIVisualEffectView(effect: visualEffect)
}()
}
@@ -142,9 +131,9 @@ extension ConversationInputToolbar {
voiceMessageInterruptedDraft.audioPlayer.delegate = self
waveformView.thumbColor = Theme.primaryTextColor
waveformView.playedColor = Theme.primaryTextColor
waveformView.unplayedColor = Theme.ternaryTextColor
waveformView.thumbColor = .Signal.label
waveformView.playedColor = .Signal.label
waveformView.unplayedColor = .Signal.tertiaryLabel
waveformView.audioWaveformTask = voiceMessageInterruptedDraft.audioWaveformTask
let stackView = UIStackView(arrangedSubviews: [

View File

@@ -117,12 +117,32 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
name: .OWSApplicationDidBecomeActive,
object: nil
)
if #available(iOS 17, *) {
inputTextView.registerForTraitChanges(
[ UITraitPreferredContentSizeCategory.self ]
) { [weak self] (textView: UITextView, _) in
self?.updateTextViewFontSize()
}
} else {
contentSizeChangeNotificationObserver = NotificationCenter.default.addObserver(
name: UIContentSizeCategory.didChangeNotification
) { [weak self] _ in
self?.updateTextViewFontSize()
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let contentSizeChangeNotificationObserver {
NotificationCenter.default.removeObserver(contentSizeChangeNotificationObserver)
}
}
// MARK: Layout Configuration.
public override var frame: CGRect {
@@ -141,10 +161,37 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
}
public override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
// Suggested sticker panel is placed outside of ConversationInputToolbar
// and need to be removed manually.
if newSuperview == nil, !isStickerPanelHidden {
stickerPanel.removeFromSuperview()
}
}
public override func didMoveToSuperview() {
super.didMoveToSuperview()
guard superview != nil else { return }
// Show suggested stickers for the draft as soon as we are placed in the view hierarchy.
updateSuggestedStickers(animated: false)
// Probably because of a regression in iOS 26 `keyboardLayoutGuide`,
// if first accessed in `calculateCustomKeyboardHeight`, would have an
// incorrect height of 34 dp (amount of bottom safe area).
// Accessing the layout guide before somehow fixes that issue.
if #available(iOS 26, *) {
_ = keyboardLayoutGuide
}
}
func update(conversationStyle: ConversationStyle) {
self.conversationStyle = conversationStyle
if #available(iOS 26, *), let sendButton = trailingEdgeControl as? UIButton {
sendButton.tintColor = conversationStyle.chatColorValue.asSendButtonTintColor().resolvedForInputToolbar()
sendButton.tintColor = conversationStyle.chatColorValue.asSendButtonTintColor()
}
}
@@ -158,7 +205,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
public enum Style {
#if compiler(>=6.2)
@available(iOS 26, *)
static var glassTintColor: UIColor {
UIColor { traitCollection in
@@ -166,7 +212,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
return UIColor(white: 0, alpha: 0.2)
}
return UIColor(white: 1, alpha: 0.12)
}.resolvedForInputToolbar()
}
}
@available(iOS 26, *)
@@ -176,21 +222,24 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
glassEffect.isInteractive = isInteractive
return glassEffect
}
#endif
static var primaryTextColor: UIColor {
.Signal.label.resolvedForInputToolbar()
.Signal.label
}
static var secondaryTextColor: UIColor {
.Signal.secondaryLabel.resolvedForInputToolbar()
.Signal.secondaryLabel
}
static var buttonTintColor: UIColor {
if #available(iOS 26, *) {
return .Signal.label.resolvedForInputToolbar()
return .Signal.label
}
return UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark
? Theme.darkThemeLegacyPrimaryIconColor
: Theme.lightThemeLegacyPrimaryIconColor
}
return Theme.primaryIconColor.resolvedForInputToolbar()
}
}
@@ -289,7 +338,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
return button
}
#if compiler(>=6.2)
@available(iOS 26.0, *)
static func sendButton(
primaryAction: UIAction?,
@@ -361,7 +409,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
configuration: .prominentGlass(),
primaryAction: primaryAction
)
button.tintColor = .Signal.red.resolvedForInputToolbar()
button.tintColor = .Signal.red
button.configuration?.image = UIImage(imageLiteralResourceName: "trash-fill")
button.configuration?.baseForegroundColor = .white
button.configuration?.cornerStyle = .capsule
@@ -373,7 +421,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
])
return button
}
#endif
}
private lazy var inputTextView: ConversationInputTextView = {
@@ -456,7 +503,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
},
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "sendButton")
)
button.tintColor = conversationStyle.bubbleChatColorOutgoing.asSendButtonTintColor().resolvedForInputToolbar()
button.tintColor = conversationStyle.bubbleChatColorOutgoing.asSendButtonTintColor()
return button
}
#endif
@@ -493,6 +540,8 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}()
private var glassContainerView: UIView?
private var legacyBackgroundView: UIView?
private var legacyBackgroundBlurView: UIVisualEffectView?
/// Whole-width container that contains (+) button, text input part and Send button.
private let contentView = UIView()
@@ -518,11 +567,10 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
contentView.semanticContentAttribute = .forceLeftToRight
let contentViewSuperview: UIView
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable {
if #available(iOS 26, *) {
iOS26Layout = true
// Glass Container.
#if compiler(>=6.2)
let glassContainerView = UIVisualEffectView(effect: UIGlassContainerEffect())
addSubview(glassContainerView)
glassContainerView.translatesAutoresizingMaskIntoConstraints = false
@@ -535,28 +583,14 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
contentViewSuperview = glassContainerView.contentView
self.glassContainerView = glassContainerView
#else
contentViewSuperview = self
#endif
} else {
// Background needed on pre-iOS 26 devices.
// The background is stretched to all edges to cover any safe area gaps.
let backgroundView = UIView()
if UIAccessibility.isReduceTransparencyEnabled {
backgroundView.backgroundColor = Theme.toolbarBackgroundColor
backgroundView.backgroundColor = .Signal.background
} else {
backgroundView.backgroundColor = Theme.toolbarBackgroundColor.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
let blurEffectView = UIVisualEffectView(effect: Theme.barBlurEffect)
// Alter the visual effect view's tint to match our background color
// so the input bar, when over a solid color background matching `toolbarBackgroundColor`,
// exactly matches the background color. This is brittle, but there is no way to get
// this behavior from UIVisualEffectView otherwise.
if let tintingView = blurEffectView.subviews.first(where: {
String(describing: type(of: $0)) == "_UIVisualEffectSubview"
}) {
tintingView.backgroundColor = backgroundView.backgroundColor
}
let blurEffectView = UIVisualEffectView(effect: nil) // will be updated later
backgroundView.addSubview(blurEffectView)
blurEffectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@@ -565,6 +599,13 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
blurEffectView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
blurEffectView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
])
// Set background color and visual effect.
updateBackgroundColors(backgroundView: backgroundView, backgroundBlurView: blurEffectView)
// Remember these views so that we can update colors on traitCollection changes.
self.legacyBackgroundView = backgroundView
self.legacyBackgroundBlurView = blurEffectView
}
addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
@@ -681,15 +722,12 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
// Rounded rect background for the text input field:
// Liquid Glass on iOS 26, gray-ish on earlier iOS versions.
let backgroundView: UIView
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable {
#if compiler(>=6.2)
if #available(iOS 26, *) {
let glassEffectView = UIVisualEffectView(effect: Style.glassEffect(isInteractive: true))
glassEffectView.cornerConfiguration = .uniformCorners(radius: 20)
glassEffectView.contentView.addSubview(messageComponentsView)
backgroundView = glassEffectView
#else
backgroundView = UIView()
#endif
messageContentView.addSubview(backgroundView)
} else {
backgroundView = UIView()
@@ -742,10 +780,10 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
stickerButton.trailingAnchor.constraint(equalTo: cameraButton.leadingAnchor),
keyboardButton.trailingAnchor.constraint(equalTo: cameraButton.leadingAnchor),
voiceNoteButton.centerYAnchor.constraint(equalTo: inputTextViewContainer.centerYAnchor),
cameraButton.centerYAnchor.constraint(equalTo: inputTextViewContainer.centerYAnchor),
stickerButton.centerYAnchor.constraint(equalTo: inputTextViewContainer.centerYAnchor),
keyboardButton.centerYAnchor.constraint(equalTo: inputTextViewContainer.centerYAnchor),
voiceNoteButton.bottomAnchor.constraint(equalTo: inputTextViewContainer.bottomAnchor),
cameraButton.bottomAnchor.constraint(equalTo: inputTextViewContainer.bottomAnchor),
stickerButton.bottomAnchor.constraint(equalTo: inputTextViewContainer.bottomAnchor),
keyboardButton.bottomAnchor.constraint(equalTo: inputTextViewContainer.bottomAnchor),
])
} else {
inputTextView.inFieldButtonsAreaWidth = 1 * LayoutMetrics.initialTextBoxHeight
@@ -888,7 +926,14 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
// Voice recording is in progress in "locked" state: show Send button in active state.
// In all other voice note recording states there are no trailing edge controls.
if isShowingVoiceMemoUI {
let showSendButton = voiceMemoRecordingState == .recordingLocked
let showSendButton: Bool = {
switch voiceMemoRecordingState {
case .recordingLocked, .draft:
true
default:
false
}
}()
rightEdgeControlsState = showSendButton ? .sendButton : .hiddenSendButton
}
// Text field has non-whitespace input: show Send button in active state.
@@ -912,10 +957,11 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
// 4. Update middle part: text input field and buttons inside.
//
// Only ever show in-field buttons when there's no Send button visible on the right.
// Only ever show in-field buttons when there's no Send button visible on the right or when
// text input contains newlines (that increases text box's height).
// On iOS 26 there are Camera and Voice Note buttons inside of the text input field:
// those would be hidden to match pre-iOS 26 behavior.
let hideAllTextFieldButtons = rightEdgeControlsState != .default
let hideAllTextFieldButtons = rightEdgeControlsState != .default || inputTextView.untrimmedText.rangeOfCharacter(from: .newlines) != nil
// Sticker/keyboard buttons will also be hidden if there's whitespace-only input.
let textFieldHasAnyInput = !inputTextView.untrimmedText.isEmpty
let hideInputMethodButtons = hideAllTextFieldButtons || textFieldHasAnyInput || hasQuotedMessage
@@ -1128,8 +1174,30 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
inputTextView.scrollToBottom()
}
func updateFontSizes() {
inputTextView.font = .dynamicTypeBody
// Dynamic color and visual effect support for background view(s) on iOS 15-18.
@available(iOS, deprecated: 26)
private func updateBackgroundColors(backgroundView: UIView, backgroundBlurView: UIVisualEffectView) {
let backgroundColor = UIColor.Signal.background
.resolvedColor(with: traitCollection)
.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
backgroundView.backgroundColor = backgroundColor
// Match Theme.barBlurEffect.
backgroundBlurView.effect =
traitCollection.userInterfaceStyle == .dark
? UIBlurEffect(style: .dark)
: UIBlurEffect(style: .light)
// Alter the visual effect view's tint to match our background color
// so the input bar, when over a solid color background matching `toolbarBackgroundColor`,
// exactly matches the background color. This is brittle, but there is no way to get
// this behavior from UIVisualEffectView otherwise.
if let tintingView = backgroundBlurView.subviews.first(where: {
String(describing: type(of: $0)) == "_UIVisualEffectSubview"
}) {
tintingView.backgroundColor = backgroundColor
}
}
// MARK: Right Edge Buttons
@@ -1407,7 +1475,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
}
#if compiler(>=6.2)
@available(iOS 26.0, *)
private class AttachmentButton: UIButton, AttachmentButtonProtocol {
@@ -1443,7 +1510,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
}
}
#endif
// MARK: Message Body
@@ -1515,6 +1581,16 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
inputTextView.undoManager?.removeAllActions()
}
// MARK: Content Size Change Handling
// Unused on iOS 17 and later.
private var contentSizeChangeNotificationObserver: NotificationCenter.Observer?
private func updateTextViewFontSize() {
inputTextView.font = .dynamicTypeBody
updateHeightWithTextView(inputTextView)
}
// MARK: Edit Message
var isEditingMessage: Bool { editTarget != nil }
@@ -1583,6 +1659,17 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
editLabel.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
editLabel.textColor = Style.primaryTextColor
// Font produced via `.semibold()` is no longer dynamic
// and UILabel has to be updated when content size changes.
if #available(iOS 17, *) {
editLabel.registerForTraitChanges(
[ UITraitPreferredContentSizeCategory.self ],
handler: { (label: UILabel, _) in
label.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
}
)
}
let stackView = UIStackView(arrangedSubviews: [editIconView, editLabel, editMessageThumbnailView])
stackView.axis = .horizontal
stackView.alignment = .center
@@ -2033,12 +2120,12 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
static let outerPanelHMargin: CGFloat = if #available(iOS 26, *) { OWSTableViewController2.cellHInnerMargin } else { 0 }
// Corner radius of the glass/blur background.
@available(iOS 26.0, *)
@available(iOS 26, *)
static let backgroundCornerRadius: CGFloat = 26
// Make sure to match parameters from MentionPicker.
static func animationTransform(_ view: UIView) -> CGAffineTransform {
guard #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable else { return .identity }
guard #available(iOS 26, *) else { return .identity }
return .scale(0.9)
}
@@ -2051,15 +2138,11 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
)
}
#if compiler(>=6.2)
static let panelVisualEffect: UIVisualEffect = {
// UIVisualEffect cannot "dematerialize" glass on iOS 26.0: setting `effect` to `nil` simply doesn't work.
// That was fixed in 26.1.
if #available(iOS 26.1, *) { Style.glassEffect() } else { UIBlurEffect(style: .systemMaterial) }
}()
#else
static let panelVisualEffect = UIBlurEffect(style: .systemMaterial)
#endif
}
/// Outermost sticker view placed as a subview of the delegate provided view and takes full width of that.
@@ -2378,7 +2461,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
micIcon.tintColor = .white
let circleView = CircleView(frame: CGRect(origin: .zero, size: .square(circleSize)))
circleView.backgroundColor = .Signal.red.resolvedForInputToolbar()
circleView.backgroundColor = .Signal.red
circleView.addSubview(micIcon)
circleView.translatesAutoresizingMaskIntoConstraints = false
micIcon.translatesAutoresizingMaskIntoConstraints = false
@@ -2420,7 +2503,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
)
button.configuration?.image = UIImage(imageLiteralResourceName: "trash-fill")
button.configuration?.baseForegroundColor = .Signal.red.resolvedForInputToolbar()
button.configuration?.baseForegroundColor = .Signal.red
return button
}()
@@ -2442,7 +2525,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
// Red mic icon
let redMicIconImageView = UIImageView(image: UIImage(imageLiteralResourceName: "mic-fill"))
redMicIconImageView.tintColor = .Signal.red.resolvedForInputToolbar()
redMicIconImageView.tintColor = .Signal.red
redMicIconImageView.autoSetDimensions(to: .square(24))
voiceMemoContentView.addSubview(redMicIconImageView)
@@ -2622,7 +2705,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
}
)
cancelButton.alpha = 0
cancelButton.configuration?.baseForegroundColor = .Signal.red.resolvedForInputToolbar()
cancelButton.configuration?.baseForegroundColor = .Signal.red
cancelButton.configuration?.contentInsets = .init(margin: 8)
cancelButton.configuration?.title = CommonStrings.cancelButton
cancelButton.configuration?.titleTextAttributesTransformer = .defaultFont(.dynamicTypeHeadlineClamped)
@@ -2985,6 +3068,10 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #unavailable(iOS 26), let legacyBackgroundView, let legacyBackgroundBlurView {
updateBackgroundColors(backgroundView: legacyBackgroundView, backgroundBlurView: legacyBackgroundBlurView)
}
// Starting with iOS 17 UIKit messes up keyboard layout guide on rotation if custom keyboard is up.
// That causes the keyboard to overlap text input field and become unaccessible.
// The workaround is to hide the keyboard on rotation.
@@ -3129,17 +3216,6 @@ extension ConversationInputToolbar: ConversationTextViewToolbarDelegate {
}
}
public override func didMoveToSuperview() {
super.didMoveToSuperview()
// Probably because of a regression in iOS 26 `keyboardLayoutGuide`,
// if first accessed in `calculateCustomKeyboardHeight`, would have an
// incorrect height of 34 dp (amount of bottom safe area).
// Accessing the layout guide before somehow fixes that issue.
if #available(iOS 26, *), superview != nil {
_ = keyboardLayoutGuide
}
}
func textViewDidChange(_ textView: UITextView) {
owsAssertDebug(inputToolbarDelegate != nil)
@@ -3252,27 +3328,3 @@ private extension ColorOrGradientValue {
}
}
}
extension UIColor {
private static var lightTraitCollection: UITraitCollection {
UITraitCollection(userInterfaceStyle: .light)
}
private static var darkTraitCollection: UITraitCollection {
UITraitCollection(userInterfaceStyle: .dark)
}
private static var currentThemeTraitCollection: UITraitCollection {
Theme.isDarkThemeEnabled ? darkTraitCollection : lightTraitCollection
}
// Change this to false to resolve each color used to current interface style.
private static let useDynamicColors: Bool = true
func resolvedForInputToolbar() -> UIColor {
guard !Self.useDynamicColors else { return self }
return self.resolvedColor(with: Self.currentThemeTraitCollection)
}
}

View File

@@ -41,7 +41,7 @@ extension ConversationViewController: BodyRangesTextViewDelegate {
public func textViewDidInsertMemoji(_ memojiGlyph: OWSAdaptiveImageGlyph) {
// Note: attachment might be nil or have an error at this point; that's fine.
do throws(SignalAttachmentError) {
self.didPasteAttachments([try SignalAttachment.attachmentFromMemoji(memojiGlyph)].compacted())
self.didPasteAttachments([try SignalAttachment.attachmentFromMemoji(memojiGlyph)])
} catch {
self.showErrorAlert(attachmentError: error)
}

View File

@@ -122,6 +122,7 @@ public extension ConversationViewController {
case .selection:
bottomView = selectionToolbar
case .inputToolbar:
loadInputToolbarIfNeeded()
bottomView = inputToolbar
case .blockingLegacyGroup:
let legacyGroupView = BlockingLegacyGroupView(fromViewController: self)
@@ -161,64 +162,42 @@ public extension ConversationViewController {
updateContentInsets()
}
// This is expensive. We only need to do it if conversationStyle has changed.
//
// TODO: Once conversationStyle is immutable, compare the old and new
// conversationStyle values and exit early if it hasn't changed.
func updateInputToolbar() {
func loadInputToolbarIfNeeded() {
AssertIsOnMainThread()
guard hasViewWillAppearEverBegun else {
return
}
guard hasViewWillAppearEverBegun else { return }
guard inputToolbar == nil else { return }
var messageDraft: MessageBody?
var replyDraft: ThreadReplyInfo?
var voiceMemoDraft: VoiceMessageInterruptedDraft?
var editTarget: TSOutgoingMessage?
if let oldInputToolbar = self.inputToolbar {
// Maintain draft continuity.
messageDraft = oldInputToolbar.messageBodyForSending
replyDraft = oldInputToolbar.draftReply
editTarget = oldInputToolbar.editTarget
voiceMemoDraft = oldInputToolbar.voiceMemoDraft
} else {
SSKEnvironment.shared.databaseStorageRef.read { transaction in
messageDraft = self.thread.currentDraft(transaction: transaction)
voiceMemoDraft = VoiceMessageInterruptedDraft.currentDraft(for: self.thread, transaction: transaction)
if messageDraft != nil || voiceMemoDraft != nil {
replyDraft = DependenciesBridge.shared.threadReplyInfoStore.fetch(for: self.thread.uniqueId, tx: transaction)
}
editTarget = self.thread.editTarget(transaction: transaction)
SSKEnvironment.shared.databaseStorageRef.read { transaction in
messageDraft = thread.currentDraft(transaction: transaction)
voiceMemoDraft = VoiceMessageInterruptedDraft.currentDraft(for: thread, transaction: transaction)
if messageDraft != nil || voiceMemoDraft != nil {
replyDraft = DependenciesBridge.shared.threadReplyInfoStore.fetch(for: thread.uniqueId, tx: transaction)
}
editTarget = thread.editTarget(transaction: transaction)
}
let newInputToolbar = buildInputToolbar(
let inputToolbar = buildInputToolbar(
messageDraft: messageDraft,
draftReply: replyDraft,
voiceMemoDraft: voiceMemoDraft,
editTarget: editTarget
)
let hadFocus = self.inputToolbar?.isInputViewFirstResponder ?? false
self.inputToolbar = newInputToolbar
#if compiler(>=6.2)
// Obscures content underneath bottom bar to improve legibility.
if #available(iOS 26, *) {
let interaction = UIScrollEdgeElementContainerInteraction()
interaction.scrollView = collectionView
interaction.edge = .bottom
newInputToolbar.setScrollEdgeElementContainerInteraction(interaction)
inputToolbar.setScrollEdgeElementContainerInteraction(interaction)
}
#endif
if hadFocus {
self.inputToolbar?.beginEditingMessage()
}
newInputToolbar.updateFontSizes()
updateBottomBar()
self.inputToolbar = inputToolbar
}
func reloadDraft() {
@@ -252,22 +231,6 @@ public extension ConversationViewController {
ensureBottomViewType()
}
func updateInputToolbarLayout(initialLayout: Bool = false) {
AssertIsOnMainThread()
guard hasViewWillAppearEverBegun else {
return
}
guard let inputToolbar = inputToolbar else {
owsFailDebug("Missing inputToolbar.")
return
}
if initialLayout {
inputToolbar.scrollToBottom()
}
}
func popKeyBoard() {
AssertIsOnMainThread()

View File

@@ -261,7 +261,6 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
}
self.updateLastKnownDistanceFromBottom()
self.updateInputToolbarLayout()
self.showMessageRequestDialogIfRequired()
self.configureScrollDownButtons()

View File

@@ -1302,7 +1302,21 @@ extension ConversationViewController: CVComponentDelegate {
}
public func didTapViewVotes(poll: OWSPoll) {
let pollDetails = PollDetailsViewController(poll: poll)
let message: TSMessage? = DependenciesBridge.shared.db.read { tx in
InteractionFinder.fetch(rowId: poll.interactionId, transaction: tx) as? TSMessage
}
guard let message else {
return
}
let pollDetails = PollDetailsViewController(
poll: poll,
message: message,
pollManager: DependenciesBridge.shared.pollMessageManager,
db: DependenciesBridge.shared.db,
databaseChangeObserver: DependenciesBridge.shared.databaseChangeObserver
)
pollDetails.delegate = self
self.present(OWSNavigationController(rootViewController: pollDetails), animated: true)
}

View File

@@ -45,14 +45,10 @@ public extension ConversationViewController {
return
}
let startCallResult = CallStarter(
_ = CallStarter(
groupId: groupId,
context: self.callStarterContext
).startCall(from: self)
if startCallResult.callDidStartOrResume {
removeGroupCallTooltip()
}
}
@objc
@@ -98,93 +94,4 @@ public extension ConversationViewController {
}
}
}
// MARK: - Group Call Tooltip
func showGroupCallTooltipIfNecessary() {
removeGroupCallTooltip()
guard canCall, isGroupConversation else {
return
}
if viewState.didAlreadyShowGroupCallTooltipEnoughTimes {
return
}
// We only want to increment once per CVC lifecycle, since
// we may tear down and rebuild the tooltip multiple times
// as the navbar items change.
if !hasIncrementedGroupCallTooltipShownCount {
SSKEnvironment.shared.preferencesRef.incrementGroupCallTooltipShownCount()
viewState.didAlreadyShowGroupCallTooltipEnoughTimes = SSKEnvironment.shared.databaseStorageRef.read { tx in
SSKEnvironment.shared.preferencesRef.wasGroupCallTooltipShown(withTransaction: tx)
}
hasIncrementedGroupCallTooltipShownCount = true
}
if conversationViewModel.groupCallInProgress {
return
}
let tailReferenceView = UIView()
tailReferenceView.isUserInteractionEnabled = false
view.addSubview(tailReferenceView)
self.groupCallTooltipTailReferenceView = tailReferenceView
let tooltip = GroupCallTooltip.present(fromView: self.view,
widthReferenceView: self.view,
tailReferenceView: tailReferenceView) { [weak self] in
self?.showGroupLobbyOrActiveCall()
}
self.groupCallTooltip = tooltip
// This delay is unfortunate, but the bar button item is not always
// ready to use as a position reference right away after it is set
// on the navigation item. So we wait a short amount of time for it
// to hopefully be ready since there's unfortunately not a simple
// way to monitor when the navigation bar layout has finished (without
// subclassing navigation bar). Since the stakes are low here (the
// tooltip just won't be visible), it's not worth doing that for.
tooltip.isHidden = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.positionGroupCallTooltip()
}
}
func positionGroupCallTooltip() {
guard let groupCallTooltipTailReferenceView = self.groupCallTooltipTailReferenceView,
let groupCallBarButtonItem = self.groupCallBarButtonItem else {
return
}
guard let barButtonView = groupCallBarButtonItem.value(forKey: "view") as? UIView else {
return
}
groupCallTooltipTailReferenceView.frame = view.convert(barButtonView.frame,
from: barButtonView.superview)
groupCallTooltip?.isHidden = false
}
private func removeGroupCallTooltip() {
groupCallTooltip?.removeFromSuperview()
self.groupCallTooltip = nil
groupCallTooltipTailReferenceView?.removeFromSuperview()
self.groupCallTooltipTailReferenceView = nil
}
private var groupCallTooltip: GroupCallTooltip? {
get { viewState.groupCallTooltip }
set { viewState.groupCallTooltip = newValue }
}
private var groupCallTooltipTailReferenceView: UIView? {
get { viewState.groupCallTooltipTailReferenceView }
set { viewState.groupCallTooltipTailReferenceView = newValue }
}
private var hasIncrementedGroupCallTooltipShownCount: Bool {
get { viewState.hasIncrementedGroupCallTooltipShownCount }
set { viewState.hasIncrementedGroupCallTooltipShownCount = newValue }
}
}

View File

@@ -759,45 +759,26 @@ extension ConversationViewController: UIDocumentPickerDelegate {
}
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
Logger.debug("Picked document at url: \(url)")
let contentType: UTType = {
do {
let resourceValues = try url.resourceValues(forKeys: [.contentTypeKey])
guard let contentType = resourceValues.contentType else {
owsFailDebug("Missing contentType.")
return .data
}
return contentType
} catch {
owsFailDebug("Error: \(error)")
return .data
}
}()
let isDirectory: Bool = {
do {
let resourceValues = try url.resourceValues(forKeys: Set([
.isDirectoryKey
]))
guard let isDirectory = resourceValues.isDirectory else {
owsFailDebug("Missing isDirectory.")
return false
}
return isDirectory
} catch {
owsFailDebug("Error: \(error)")
return false
}
}()
if isDirectory {
Logger.info("User picked directory.")
let resourceValues: URLResourceValues?
do {
resourceValues = try url.resourceValues(forKeys: [.contentTypeKey, .isDirectoryKey])
} catch {
owsFailDebug("couldn't get resourceValues: \(error)")
resourceValues = nil
}
if resourceValues?.isDirectory == true {
DispatchQueue.main.async {
OWSActionSheets.showActionSheet(title: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE",
comment: "Alert title when picking a document fails because user picked a directory/bundle"),
message: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY",
comment: "Alert body when picking a document fails because user picked a directory/bundle"))
OWSActionSheets.showActionSheet(
title: OWSLocalizedString(
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE",
comment: "Alert title when picking a document fails because user picked a directory/bundle"
),
message: OWSLocalizedString(
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY",
comment: "Alert body when picking a document fails because user picked a directory/bundle",
),
)
}
return
}
@@ -807,37 +788,39 @@ extension ConversationViewController: UIDocumentPickerDelegate {
return filename
}
owsFailDebug("Unable to determine filename")
return OWSLocalizedString("ATTACHMENT_DEFAULT_FILENAME",
comment: "Generic filename for an attachment with no known name")
return OWSLocalizedString("ATTACHMENT_DEFAULT_FILENAME", comment: "Generic filename for an attachment with no known name")
}()
func buildDataSource() -> DataSource? {
do {
return try DataSourcePath(fileUrl: url, shouldDeleteOnDeallocation: false)
} catch {
owsFailDebug("Error: \(error).")
return nil
}
}
guard let dataSource = buildDataSource() else {
let dataSource: DataSourcePath
do {
dataSource = try DataSourcePath(fileUrl: url, shouldDeleteOnDeallocation: false)
} catch {
owsFailDebug("couldn't build data source: \(error)")
DispatchQueue.main.async {
OWSActionSheets.showActionSheet(title: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE",
comment: "Alert title when picking a document fails for an unknown reason"))
OWSActionSheets.showActionSheet(
title: OWSLocalizedString(
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE",
comment: "Alert title when picking a document fails for an unknown reason",
),
)
}
return
}
dataSource.sourceFilename = filename
// Although we want to be able to send higher quality attachments through the document picker
// it's more important that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov)
if SignalAttachment.isVideoThatNeedsCompression(dataSource: dataSource, dataUTI: contentType.identifier) {
self.showApprovalDialogAfterProcessingVideoURL(url, filename: filename)
let contentTypeIdentifier = (resourceValues?.contentType ?? .data).identifier
// Although we want to be able to send higher quality attachments through
// the document picker, it's more important that we ensure the sent format
// is one all clients can accept (e.g., *not* QuickTime .mov).
if SignalAttachment.videoUTISet.contains(contentTypeIdentifier) {
self.showApprovalDialogAfterProcessingVideo(dataSource: dataSource)
return
}
let attachment: SignalAttachment
do throws(SignalAttachmentError) {
attachment = try SignalAttachment.attachment(dataSource: dataSource, dataUTI: contentType.identifier)
attachment = try SignalAttachment.attachment(dataSource: dataSource, dataUTI: contentTypeIdentifier)
} catch {
DispatchQueue.main.async {
self.showErrorAlert(attachmentError: error)
@@ -848,7 +831,7 @@ extension ConversationViewController: UIDocumentPickerDelegate {
showApprovalDialog(forAttachments: [attachment])
}
private func showApprovalDialogAfterProcessingVideoURL(_ movieURL: URL, filename: String?) {
private func showApprovalDialogAfterProcessingVideo(dataSource: DataSourcePath) {
AssertIsOnMainThread()
ModalActivityIndicatorViewController.present(
@@ -856,15 +839,7 @@ extension ConversationViewController: UIDocumentPickerDelegate {
canCancel: true,
asyncBlock: { modalActivityIndicator in
do {
let dataSource = try DataSourcePath(
fileUrl: movieURL,
shouldDeleteOnDeallocation: false,
)
dataSource.sourceFilename = filename
let attachment = try await SignalAttachment.compressVideoAsMp4(
dataSource: dataSource,
dataUTI: UTType.mpeg4Movie.identifier,
)
let attachment = try await SignalAttachment.compressVideoAsMp4(dataSource: dataSource)
modalActivityIndicator.dismissIfNotCanceled(completionIfNotCanceled: {
self.showApprovalDialog(forAttachments: [attachment])
})

View File

@@ -11,7 +11,9 @@ public class CVAccessibilityCustomAction: UIAccessibilityCustomAction {
}
extension ConversationViewController: UIGestureRecognizerDelegate {
func createGestureRecognizers() {
func configureGestureRecognizersIfNeeded() {
guard !collectionViewGestureRecongnizersConfigured else { return }
collectionViewTapGestureRecognizer.setTapDelegate(self)
collectionViewTapGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(collectionViewTapGestureRecognizer)
@@ -25,12 +27,10 @@ extension ConversationViewController: UIGestureRecognizerDelegate {
collectionViewContextMenuGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(collectionViewContextMenuGestureRecognizer)
let collectionViewContextMenuSecondaryClickRecognizer = UITapGestureRecognizer()
collectionViewContextMenuSecondaryClickRecognizer.addTarget(self, action: #selector(handleSecondaryClickGesture))
collectionViewContextMenuSecondaryClickRecognizer.buttonMaskRequired = [.secondary]
collectionViewContextMenuSecondaryClickRecognizer.delegate = self
collectionView.addGestureRecognizer(collectionViewContextMenuSecondaryClickRecognizer)
self.collectionViewContextMenuSecondaryClickRecognizer = collectionViewContextMenuSecondaryClickRecognizer
collectionViewPanGestureRecognizer.addTarget(self, action: #selector(handlePanGesture))
collectionViewPanGestureRecognizer.delegate = self
@@ -48,6 +48,8 @@ extension ConversationViewController: UIGestureRecognizerDelegate {
if let interactivePopGestureRecognizer = navigationController?.interactivePopGestureRecognizer {
collectionViewPanGestureRecognizer.require(toFail: interactivePopGestureRecognizer)
}
collectionViewGestureRecongnizersConfigured = true
}
// TODO: Revisit
@@ -308,6 +310,7 @@ public struct CVLongPressHandler {
case paymentMessage
case poll
case bodyText(item: CVTextLabel.Item)
case associatedSubcomponent
}
let gestureLocation: GestureLocation
@@ -355,6 +358,17 @@ public struct CVLongPressHandler {
delegate.didLongPressPoll(cell, itemViewModel: itemViewModel, shouldAllowReply: shouldAllowReply)
case .bodyText:
break
case .associatedSubcomponent:
// Bottom buttons, labels, and footers are considered separate subcomponents,
// but may be associated with another subcomponent type.
if let message = itemViewModel.interaction as? TSMessage, message.isPoll {
delegate.didLongPressPoll(cell, itemViewModel: itemViewModel, shouldAllowReply: shouldAllowReply)
return
}
// Default
delegate.didLongPressTextViewItem(cell,
itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply)
}
}

View File

@@ -17,7 +17,7 @@ extension ConversationViewController {
let cellCenterPoint = cell.frame.center
let screenPoint = self.collectionView .convert(cellCenterPoint, from: cell)
var presentImmediately = false
if let secondaryClickRecognizer = collectionViewContextMenuSecondaryClickRecognizer, secondaryClickRecognizer.state == .ended {
if collectionViewContextMenuSecondaryClickRecognizer.state == .ended {
presentImmediately = true
}
interaction.initiateContextMenuGesture(locationInView: screenPoint, presentImmediately: presentImmediately)

View File

@@ -209,9 +209,7 @@ extension ConversationViewController: MessageActionsDelegate {
func messageActionsForwardItem(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
ForwardMessageViewController.present(forItemViewModels: [itemViewModel],
from: self,
delegate: self)
ForwardMessageViewController.present(forItemViewModel: itemViewModel, from: self, delegate: self)
}
func messageActionsStartedSelect(initialItem itemViewModel: CVItemViewModelImpl) {

View File

@@ -96,7 +96,7 @@ extension ConversationViewController {
var newInsets = oldInsets
newInsets.bottom = bottomBarContainer.frame.height - collectionView.safeAreaInsets.bottom
newInsets.top = (bannerView?.height ?? 0)
newInsets.top = (bannerStackView?.height ?? 0)
let wasScrolledToBottom = self.isScrolledToBottom
@@ -462,13 +462,13 @@ extension ConversationViewController: MessageEditHistoryViewDelegate {
extension ConversationViewController: LongTextViewDelegate {
public func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController) {
func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController) {
Logger.info("")
navigationController?.popToViewController(self, animated: true)
}
public func expandTruncatedTextOrPresentLongTextView(_ itemViewModel: CVItemViewModelImpl) {
func expandTruncatedTextOrPresentLongTextView(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
guard let displayableBodyText = itemViewModel.displayableBodyText else {

View File

@@ -297,26 +297,14 @@ extension ConversationViewController {
}
func didTapDeleteSelectedItems() {
let db = DependenciesBridge.shared.db
let selectionItems = self.selectionState.selectionItems
guard !selectionItems.isEmpty else {
owsFailDebug("Invalid selection.")
return
}
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: self
) { interactionDeleteManager, _ in
self.presentDeleteSelectedMessagesActionSheet(
selectionItems: selectionItems,
interactionDeleteManager: interactionDeleteManager
)
}
}
private func presentDeleteSelectedMessagesActionSheet(
selectionItems: [CVSelectionItem],
interactionDeleteManager: InteractionDeleteManager
) {
let alert = ActionSheetController(
title: nil,
message: String.localizedStringWithFormat(
@@ -342,22 +330,22 @@ extension ConversationViewController {
) { [weak self] modalActivityIndicator in
guard let self = self else { return }
DispatchQueue.main.async {
Self.deleteSelectedItems(
await db.awaitableWrite { tx in
self.deleteSelectedItemsForMe(
selectionItems: selectionItems,
thread: self.thread,
interactionDeleteManager: interactionDeleteManager
tx: tx,
)
}
modalActivityIndicator.dismiss {
self.uiMode = .normal
}
modalActivityIndicator.dismiss {
self.uiMode = .normal
}
}
}
alert.addAction(deleteForMeAction)
let canDeleteForEveryone: Bool = SSKEnvironment.shared.databaseStorageRef.read { tx in
let canDeleteForEveryone: Bool = db.read { tx in
selectionItems.allSatisfy { selectionItem in
TSOutgoingMessage.anyFetchOutgoingMessage(
uniqueId: selectionItem.interactionId,
@@ -378,11 +366,10 @@ extension ConversationViewController {
canCancel: false
) { @MainActor [weak self] modalActivityIndicator in
guard let self else { return }
let thread = self.thread
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
Self.deleteSelectedItemsForEveryone(
await db.awaitableWrite { tx in
self.deleteSelectedItemsForEveryone(
selectionItems: selectionItems,
thread: thread,
thread: self.thread,
tx: tx
)
}
@@ -399,30 +386,30 @@ extension ConversationViewController {
present(alert, animated: true)
}
private static func deleteSelectedItems(
private func deleteSelectedItemsForMe(
selectionItems: [CVSelectionItem],
thread: TSThread,
interactionDeleteManager: InteractionDeleteManager
tx: DBWriteTransaction,
) {
SSKEnvironment.shared.databaseStorageRef.write { tx in
let interactionsToDelete = selectionItems.compactMap { item in
TSInteraction.anyFetch(
uniqueId: item.interactionId,
transaction: tx
)
}
let interactionDeleteManager = DependenciesBridge.shared.interactionDeleteManager
interactionDeleteManager.delete(
interactions: interactionsToDelete,
sideEffects: .custom(
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: thread)
),
tx: tx
let interactionsToDelete = selectionItems.compactMap { item in
TSInteraction.anyFetch(
uniqueId: item.interactionId,
transaction: tx
)
}
interactionDeleteManager.delete(
interactions: interactionsToDelete,
sideEffects: .custom(
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: thread)
),
tx: tx
)
}
private static func deleteSelectedItemsForEveryone(
private func deleteSelectedItemsForEveryone(
selectionItems: [CVSelectionItem],
thread: TSThread,
tx: DBWriteTransaction
@@ -532,20 +519,9 @@ extension ConversationViewController {
}
func didTapDeleteAll() {
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: self
) { [weak self] _, threadSoftDeleteManager in
guard let self else { return }
let db = DependenciesBridge.shared.db
let threadSoftDeleteManager = DependenciesBridge.shared.threadSoftDeleteManager
self.presentDeleteAllConfirmationSheet(
threadSoftDeleteManager: threadSoftDeleteManager
)
}
}
private func presentDeleteAllConfirmationSheet(
threadSoftDeleteManager: any ThreadSoftDeleteManager
) {
let thread = self.thread
let alert = ActionSheetController(title: nil, message: OWSLocalizedString("DELETE_ALL_MESSAGES_IN_CONVERSATION_ALERT_BODY", comment: "action sheet body"))
alert.addAction(OWSActionSheets.cancelAction)
@@ -554,7 +530,7 @@ extension ConversationViewController {
guard let self = self else { return }
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] modalActivityIndicator in
guard let self = self else { return }
SSKEnvironment.shared.databaseStorageRef.write {
db.write {
threadSoftDeleteManager.removeAllInteractions(
thread: thread,
sendDeleteForMeSyncMessage: true,

View File

@@ -184,7 +184,6 @@ extension ConversationViewController {
}
navigationItem.rightBarButtonItems = barButtons
showGroupCallTooltipIfNecessary()
return
}
}

View File

@@ -99,7 +99,6 @@ public final class ConversationViewController: OWSViewController {
for: thread, chatColor: chatColor, wallpaperViewBuilder: wallpaperViewBuilder
)
let conversationViewModel = ConversationViewModel.load(for: thread, tx: tx)
let didAlreadyShowGroupCallTooltipEnoughTimes = SSKEnvironment.shared.preferencesRef.wasGroupCallTooltipShown(withTransaction: tx)
let cvc = ConversationViewController(
appReadiness: appReadiness,
@@ -107,7 +106,6 @@ public final class ConversationViewController: OWSViewController {
conversationViewModel: conversationViewModel,
action: action,
conversationStyle: conversationStyle,
didAlreadyShowGroupCallTooltipEnoughTimes: didAlreadyShowGroupCallTooltipEnoughTimes,
loadAroundMessageId: loadAroundMessageId,
scrollToMessageId: scrollToMessageId,
oldestUnreadMessage: oldestUnreadMessage,
@@ -135,7 +133,6 @@ public final class ConversationViewController: OWSViewController {
conversationViewModel: ConversationViewModel,
action: ConversationViewAction,
conversationStyle: ConversationStyle,
didAlreadyShowGroupCallTooltipEnoughTimes: Bool,
loadAroundMessageId: String?,
scrollToMessageId: String?,
oldestUnreadMessage: TSInteraction?,
@@ -150,7 +147,6 @@ public final class ConversationViewController: OWSViewController {
self.viewState = CVViewState(
threadUniqueId: threadViewModel.threadRecord.uniqueId,
conversationStyle: conversationStyle,
didAlreadyShowGroupCallTooltipEnoughTimes: didAlreadyShowGroupCallTooltipEnoughTimes,
chatColor: chatColor,
wallpaperViewBuilder: wallpaperViewBuilder
)
@@ -338,17 +334,9 @@ public final class ConversationViewController: OWSViewController {
super.viewWillAppear(animated)
if self.inputToolbar == nil {
// This will create the input toolbar for the first time.
// It's important that we do this at the "last moment" to
// avoid expensive work that delays CVC presentation.
self.applyTheme()
owsAssertDebug(self.inputToolbar != nil)
configureGestureRecognizersIfNeeded()
self.createGestureRecognizers()
} else {
self.ensureBannerState()
}
ensureBannerState()
self.isViewVisible = true
self.viewWillAppearForLoad()
@@ -361,7 +349,8 @@ public final class ConversationViewController: OWSViewController {
self.updateNavigationTitle()
self.ensureBottomViewType()
self.updateInputToolbarLayout(initialLayout: true)
inputToolbar?.scrollToBottom()
self.refreshCallState()
self.showMessageRequestDialogIfRequired()
@@ -447,7 +436,6 @@ public final class ConversationViewController: OWSViewController {
// Clear the "on open" state after the view has been presented.
self.actionOnOpen = .none
self.updateInputToolbarLayout()
self.configureScrollDownButtons()
inputToolbar?.viewDidAppear()
@@ -519,8 +507,6 @@ public final class ConversationViewController: OWSViewController {
inputToolbar.ensureTextViewHeight()
updateContentInsets()
self.positionGroupCallTooltip()
}
public override var shouldAutorotate: Bool {
@@ -538,16 +524,6 @@ public final class ConversationViewController: OWSViewController {
Logger.info("didChangePreferredContentSize")
resetForSizeOrOrientationChange()
guard hasViewWillAppearEverBegun else {
return
}
guard let inputToolbar = inputToolbar else {
owsFailDebug("Missing inputToolbar.")
return
}
inputToolbar.updateFontSizes()
}
public override func themeDidChange() {
@@ -584,8 +560,6 @@ public final class ConversationViewController: OWSViewController {
self.updateNavigationTitle()
self.updateNavigationBarSubtitleLabel()
self.updateInputToolbar()
self.updateInputToolbarLayout()
self.updateBarButtonItems()
self.ensureBannerState()
@@ -688,7 +662,6 @@ public final class ConversationViewController: OWSViewController {
}
updateContentInsetsDebounced()
updateInputToolbarLayout()
viewSafeAreaInsetsDidChangeForLoad()
updateConversationStyle()
}

View File

@@ -240,6 +240,11 @@ struct CVItemModelBuilder: CVItemBuilding {
tapForMoreState = .none
}
var isPinnedMessage: Bool = false
if let interactionId = interaction.grdbId?.int64Value {
isPinnedMessage = threadViewModel.pinnedMessageIds.contains(interactionId)
}
if let paymentMessage = interaction as? OWSPaymentMessage {
itemViewState.footerState = CVComponentFooter.buildPaymentState(
interaction: interaction,
@@ -251,6 +256,7 @@ struct CVItemModelBuilder: CVItemBuilding {
itemViewState.footerState = CVComponentFooter.buildState(
interaction: interaction,
tapForMoreState: tapForMoreState,
isPinnedMessage: isPinnedMessage,
transaction: transaction
)
}

View File

@@ -19,7 +19,7 @@ struct CVLoadContext: CVItemBuildingContext {
let viewStateSnapshot: CVViewStateSnapshot
let spoilerState: SpoilerRenderState
let messageLoader: MessageLoader
let prevRenderState: CVRenderState
let prevRenderState: CVRenderState?
let transaction: DBReadTransaction
let avatarBuilder: CVAvatarBuilder
let localAci: Aci
@@ -63,6 +63,7 @@ protocol CVItemBuildingContext {
var transaction: DBReadTransaction { get }
var avatarBuilder: CVAvatarBuilder { get }
var localAci: Aci { get }
var prevRenderState: CVRenderState? { get }
}
// MARK: -
@@ -78,6 +79,7 @@ extension CVItemBuildingContext {
// MARK: -
struct CVItemBuildingContextImpl: CVItemBuildingContext {
let prevRenderState: CVRenderState?
let threadViewModel: ThreadViewModel
let viewStateSnapshot: CVViewStateSnapshot
let transaction: DBReadTransaction

View File

@@ -345,6 +345,7 @@ public class CVLoader: NSObject {
)
let avatarBuilder = CVAvatarBuilder(transaction: transaction)
let itemBuildingContext = CVItemBuildingContextImpl(
prevRenderState: nil,
threadViewModel: threadViewModel,
viewStateSnapshot: viewStateSnapshot,
transaction: transaction,
@@ -408,6 +409,7 @@ public class CVLoader: NSObject {
)
let avatarBuilder = CVAvatarBuilder(transaction: transaction)
let itemBuildingContext = CVItemBuildingContextImpl(
prevRenderState: nil,
threadViewModel: threadViewModel,
viewStateSnapshot: viewStateSnapshot,
transaction: transaction,

View File

@@ -366,7 +366,7 @@ class MessageLoader {
extension InteractionReadCache: MessageLoaderInteractionFetcher {
func fetchInteractions(for uniqueIds: [String], tx: DBReadTransaction) -> [String: TSInteraction] {
return getInteractionsIfInCache(for: Array(uniqueIds), transaction: SDSDB.shimOnlyBridge(tx))
return getInteractionsIfInCache(for: Array(uniqueIds), transaction: tx)
}
}
@@ -376,7 +376,7 @@ class SDSInteractionFetcherImpl: MessageLoaderInteractionFetcher {
func fetchInteractions(for uniqueIds: [String], tx: DBReadTransaction) -> [String: TSInteraction] {
let fetchedInteractions = InteractionFinder.interactions(
withInteractionIds: Set(uniqueIds),
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
return Dictionary(uniqueKeysWithValues: fetchedInteractions.lazy.map { ($0.uniqueId, $0) })
}
@@ -399,7 +399,7 @@ class ConversationViewBatchFetcher: MessageLoaderBatchFetcher {
try interactionFinder.fetchUniqueIdsForConversationView(
rowIdFilter: filter,
limit: limit,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
}
}

View File

@@ -31,19 +31,13 @@ public extension TSInteraction {
thread: associatedThread,
hasLinkedDevices: hasLinkedDevices,
forceDarkTheme: forceDarkTheme,
interactionDeleteManager: DependenciesBridge.shared.interactionDeleteManager
)
} else {
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: fromViewController
) { [weak self] interactionDeleteManager, _ in
self?.presentDeletionActionSheetForNotNoteToSelf(
fromViewController: fromViewController,
thread: associatedThread,
forceDarkTheme: forceDarkTheme,
interactionDeleteManager: interactionDeleteManager
)
}
presentDeletionActionSheetForNotNoteToSelf(
fromViewController: fromViewController,
thread: associatedThread,
forceDarkTheme: forceDarkTheme,
)
}
}
@@ -52,7 +46,6 @@ public extension TSInteraction {
thread: TSThread,
hasLinkedDevices: Bool,
forceDarkTheme: Bool,
interactionDeleteManager: any InteractionDeleteManager
) {
let deleteMessageHeaderText = OWSLocalizedString(
"DELETE_FOR_ME_NOTE_TO_SELF_ACTION_SHEET_HEADER",
@@ -89,7 +82,6 @@ public extension TSInteraction {
actionSheet.addAction(deleteForMeAction(
title: deleteActionTitle,
thread: thread,
interactionDeleteManager: interactionDeleteManager
))
actionSheet.addAction(.cancel)
@@ -100,7 +92,6 @@ public extension TSInteraction {
fromViewController: UIViewController,
thread: TSThread,
forceDarkTheme: Bool,
interactionDeleteManager: any InteractionDeleteManager
) {
let actionSheetController = ActionSheetController(
message: OWSLocalizedString(
@@ -115,7 +106,6 @@ public extension TSInteraction {
actionSheetController.addAction(deleteForMeAction(
title: CommonStrings.deleteForMeButton,
thread: thread,
interactionDeleteManager: interactionDeleteManager
))
if
@@ -192,15 +182,17 @@ public extension TSInteraction {
private func deleteForMeAction(
title: String,
thread: TSThread,
interactionDeleteManager: any InteractionDeleteManager
) -> ActionSheetAction {
let db = DependenciesBridge.shared.db
let interactionDeleteManager = DependenciesBridge.shared.interactionDeleteManager
return ActionSheetAction(
title: CommonStrings.deleteForMeButton,
style: .destructive
) { [weak self] _ in
guard let self else { return }
SSKEnvironment.shared.databaseStorageRef.asyncWrite { tx in
db.asyncWrite { tx in
guard
let freshSelf = TSInteraction.anyFetch(uniqueId: self.uniqueId, transaction: tx),
let freshThread = TSThread.anyFetch(uniqueId: thread.uniqueId, transaction: tx)

View File

@@ -1,120 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalUI
import SignalServiceKit
/// Responsible for optionally showing informational UX before a deletion occurs
/// that will sync across devices.
///
/// As part of the introduction of "delete syncs" (powered by `DeleteForMe` sync
/// messages), we want to show users a one-time pop-up explaining that their
/// deletes will now sync before we do the deletion. This type handles checking
/// if the pop-up should be shown, and if so, doing so.
final class DeleteForMeInfoSheetCoordinator {
typealias DeletionBlock = (InteractionDeleteManager, ThreadSoftDeleteManager) -> Void
private enum StoreKeys {
static let hasShownDeleteForMeInfoSheet = "hasShownDeleteForMeInfoSheet"
}
private let db: any DB
private let deviceStore: OWSDeviceStore
private let interactionDeleteManager: InteractionDeleteManager
private let keyValueStore: KeyValueStore
private let threadSoftDeleteManager: ThreadSoftDeleteManager
init(
db: any DB,
deviceStore: OWSDeviceStore,
interactionDeleteManager: InteractionDeleteManager,
threadSoftDeleteManager: ThreadSoftDeleteManager
) {
self.db = db
self.deviceStore = deviceStore
self.interactionDeleteManager = interactionDeleteManager
self.keyValueStore = KeyValueStore(collection: "DeleteForMeInfoSheetCoordinator")
self.threadSoftDeleteManager = threadSoftDeleteManager
}
static func fromGlobals() -> DeleteForMeInfoSheetCoordinator {
return DeleteForMeInfoSheetCoordinator(
db: DependenciesBridge.shared.db,
deviceStore: DependenciesBridge.shared.deviceStore,
interactionDeleteManager: DependenciesBridge.shared.interactionDeleteManager,
threadSoftDeleteManager: DependenciesBridge.shared.threadSoftDeleteManager
)
}
func coordinateDelete(
fromViewController: UIViewController,
deletionBlock: @escaping DeletionBlock
) {
guard shouldShowInfoSheet() else {
deletionBlock(interactionDeleteManager, threadSoftDeleteManager)
return
}
let imageName = Theme.isDarkThemeEnabled ? "delete-sync-dark" : "delete-sync-light"
let heroSheet = HeroSheetViewController(
hero: .image(UIImage(named: imageName)!),
title: OWSLocalizedString(
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_TITLE",
comment: "Title for an info sheet explaining that deletes are now synced across devices."
),
body: OWSLocalizedString(
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_SUBTITLE",
comment: "Subtitle for an info sheet explaining that deletes are now synced across devices."
),
primaryButton: .init(title: OWSLocalizedString(
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_BUTTON",
comment: "Label for a button in an info sheet confirming that deletes are now synced across devices."
)) { sheet in
sheet.dismiss(animated: true) {
self.db.write { tx in
self.keyValueStore.setBool(
true,
key: StoreKeys.hasShownDeleteForMeInfoSheet,
transaction: tx
)
}
deletionBlock(
self.interactionDeleteManager,
self.threadSoftDeleteManager
)
}
}
)
fromViewController.present(heroSheet, animated: true)
}
#if USE_DEBUG_UI
func forceEnableInfoSheet(tx: DBWriteTransaction) {
keyValueStore.removeValue(
forKey: StoreKeys.hasShownDeleteForMeInfoSheet,
transaction: tx
)
}
#endif
private func shouldShowInfoSheet() -> Bool {
return db.read { tx -> Bool in
guard deviceStore.hasLinkedDevices(tx: tx) else {
// No devices with which to sync!
return false
}
guard keyValueStore.getBool(StoreKeys.hasShownDeleteForMeInfoSheet, transaction: tx) != true else {
// Already shown!
return false
}
return true
}
}
}

View File

@@ -16,28 +16,30 @@ extension DeviceTransferService {
do {
let database: DeviceTransferProtoFile = try {
let file = SSKEnvironment.shared.databaseStorageRef.grdbStorage.databaseFilePath
guard let size = OWSFileSystem.fileSize(ofPath: file), size.uint64Value > 0 else {
throw OWSAssertionError("Failed to calculate size of database \(file)")
let size = try OWSFileSystem.fileSize(ofPath: file)
guard size > 0 else {
throw OWSAssertionError("database is empty")
}
estimatedTotalSize += size.uint64Value
estimatedTotalSize += size
let fileBuilder = DeviceTransferProtoFile.builder(
identifier: DeviceTransferService.databaseIdentifier,
relativePath: try pathRelativeToAppSharedDirectory(file),
estimatedSize: size.uint64Value
estimatedSize: size,
)
return fileBuilder.buildInfallibly()
}()
let wal: DeviceTransferProtoFile = try {
let file = SSKEnvironment.shared.databaseStorageRef.grdbStorage.databaseWALFilePath
guard let size = OWSFileSystem.fileSize(ofPath: file), size.uint64Value > 0 else {
throw OWSAssertionError("Failed to calculate size of database wal \(file)")
let size = try OWSFileSystem.fileSize(ofPath: file)
guard size > 0 else {
throw OWSAssertionError("database wal is empty")
}
estimatedTotalSize += size.uint64Value
estimatedTotalSize += size
let fileBuilder = DeviceTransferProtoFile.builder(
identifier: DeviceTransferService.databaseWALIdentifier,
relativePath: try pathRelativeToAppSharedDirectory(file),
estimatedSize: size.uint64Value
estimatedSize: size,
)
return fileBuilder.buildInfallibly()
}()
@@ -60,20 +62,18 @@ extension DeviceTransferService {
}
for file in filesToTransfer {
guard let size = OWSFileSystem.fileSize(ofPath: file) else {
throw OWSAssertionError("Failed to calculate size of file \(file)")
}
let size = try OWSFileSystem.fileSize(ofPath: file)
guard size.uint64Value > 0 else {
guard size > 0 else {
owsFailDebug("skipping empty file \(file)")
continue
}
estimatedTotalSize += size.uint64Value
estimatedTotalSize += size
let fileBuilder = DeviceTransferProtoFile.builder(
identifier: UUID().uuidString,
relativePath: try pathRelativeToAppSharedDirectory(file),
estimatedSize: size.uint64Value
estimatedSize: size,
)
manifestBuilder.addFiles(fileBuilder.buildInfallibly())
}
@@ -151,13 +151,13 @@ extension DeviceTransferService {
stopTransfer()
return owsFailDebug("Received manifest in unexpected state \(transferState)")
}
guard let fileSize = OWSFileSystem.fileSize(of: localURL) else {
guard let fileSize = (try? OWSFileSystem.fileSize(of: localURL)) else {
stopTransfer()
return owsFailDebug("Missing manifest file.")
}
// Not sure why this limit exists in the first place, but 1Gb should be
// plenty high for file descriptors.
guard fileSize.uint64Value < 1024 * 1024 * 1024 else {
guard fileSize < 1024 * 1024 * 1024 else {
stopTransfer()
return owsFailDebug("Unexpectedly received a very large manifest \(fileSize)")
}

View File

@@ -29,7 +29,7 @@ class StaleProfileFetcher {
return []
}
var staleServiceIds = [ServiceId]()
Self.enumerateMissingAndStaleUserProfiles(now: Date(), tx: SDSDB.shimOnlyBridge(tx)) { userProfile in
Self.enumerateMissingAndStaleUserProfiles(now: Date(), tx: tx) { userProfile in
switch userProfile.internalAddress {
case .localUser:
// Ignore the local user.

View File

@@ -8,83 +8,12 @@ public import SignalServiceKit
extension ProvisioningCoordinatorImpl {
public enum Shims {
public typealias MessageFactory = _ProvisioningCoordinator_MessageFactoryShim
public typealias ProfileManager = _ProvisioningCoordinator_ProfileManagerShim
public typealias PushRegistrationManager = _ProvisioningCoordinator_PushRegistrationManagerShim
public typealias ReceiptManager = _ProvisioningCoordinator_ReceiptManagerShim
public typealias SyncManager = _ProvisioningCoordinator_SyncManagerShim
public typealias UDManager = _ProvisioningCoordinator_UDManagerShim
}
public enum Wrappers {
public typealias MessageFactory = _ProvisioningCoordinator_MessageFactoryWrapper
public typealias ProfileManager = _ProvisioningCoordinator_ProfileManagerWrapper
public typealias PushRegistrationManager = _ProvisioningCoordinator_PushRegistrationManagerWrapper
public typealias ReceiptManager = _ProvisioningCoordinator_ReceiptManagerWrapper
public typealias SyncManager = _ProvisioningCoordinator_SyncManagerWrapper
public typealias UDManager = _ProvisioningCoordinator_UDManagerWrapper
}
}
// MARK: MessageFactory
public protocol _ProvisioningCoordinator_MessageFactoryShim {
func insertInfoMessage(
into thread: TSThread,
messageType: TSInfoMessageType,
tx: DBWriteTransaction
)
}
public class _ProvisioningCoordinator_MessageFactoryWrapper: _ProvisioningCoordinator_MessageFactoryShim {
public init() {}
public func insertInfoMessage(
into thread: TSThread,
messageType: TSInfoMessageType,
tx: DBWriteTransaction
) {
let infoMessage = TSInfoMessage(thread: thread, messageType: messageType)
infoMessage.anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
}
}
// MARK: ProfileManager
public protocol _ProvisioningCoordinator_ProfileManagerShim {
func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile?
func setLocalProfileKey(
_ key: Aes256Key,
userProfileWriter: UserProfileWriter,
tx: DBWriteTransaction
)
}
public class _ProvisioningCoordinator_ProfileManagerWrapper: _ProvisioningCoordinator_ProfileManagerShim {
private let profileManager: OWSProfileManager
public init(_ profileManager: OWSProfileManager) {
self.profileManager = profileManager
}
public func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile? {
return profileManager.localUserProfile(tx: SDSDB.shimOnlyBridge(tx))
}
public func setLocalProfileKey(
_ key: Aes256Key,
userProfileWriter: UserProfileWriter,
tx: DBWriteTransaction
) {
profileManager.setLocalProfileKey(
key,
userProfileWriter: userProfileWriter,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
}
@@ -128,57 +57,6 @@ public class _ProvisioningCoordinator_ReceiptManagerWrapper: _ProvisioningCoordi
}
public func setAreReadReceiptsEnabled(_ areEnabled: Bool, tx: DBWriteTransaction) {
receiptManager.setAreReadReceiptsEnabled(areEnabled, transaction: SDSDB.shimOnlyBridge(tx))
}
}
// MARK: SyncManager
public protocol _ProvisioningCoordinator_SyncManagerShim {
func sendKeysSyncRequestMessage(tx: DBWriteTransaction)
func sendInitialSyncRequestsAwaitingCreatedThreadOrdering(
timeout: TimeInterval
) async throws -> [String]
}
public class _ProvisioningCoordinator_SyncManagerWrapper: _ProvisioningCoordinator_SyncManagerShim {
private let syncManager: SyncManagerProtocol
public init(_ syncManager: SyncManagerProtocol) {
self.syncManager = syncManager
}
public func sendKeysSyncRequestMessage(tx: DBWriteTransaction) {
syncManager.sendKeysSyncRequestMessage(transaction: SDSDB.shimOnlyBridge(tx))
}
public func sendInitialSyncRequestsAwaitingCreatedThreadOrdering(
timeout: TimeInterval
) async throws -> [String] {
return try await syncManager
.sendInitialSyncRequestsAwaitingCreatedThreadOrdering(
timeoutSeconds: timeout
)
.awaitable()
}
}
// MARK: - UDManager
public protocol _ProvisioningCoordinator_UDManagerShim {
func shouldAllowUnrestrictedAccessLocal(tx: DBReadTransaction) -> Bool
}
public class _ProvisioningCoordinator_UDManagerWrapper: _ProvisioningCoordinator_UDManagerShim {
private let manager: OWSUDManager
public init(_ manager: OWSUDManager) { self.manager = manager }
public func shouldAllowUnrestrictedAccessLocal(tx: DBReadTransaction) -> Bool {
return manager.shouldAllowUnrestrictedAccessLocal(transaction: SDSDB.shimOnlyBridge(tx))
receiptManager.setAreReadReceiptsEnabled(areEnabled, transaction: tx)
}
}

View File

@@ -14,10 +14,9 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
private let identityManager: OWSIdentityManager
private let linkAndSyncManager: LinkAndSyncManager
private let accountKeyStore: AccountKeyStore
private let messageFactory: Shims.MessageFactory
private let networkManager: any NetworkManagerProtocol
private let preKeyManager: PreKeyManager
private let profileManager: Shims.ProfileManager
private let profileManager: ProfileManager
private let pushRegistrationManager: Shims.PushRegistrationManager
private let receiptManager: Shims.ReceiptManager
private let registrationStateChangeManager: RegistrationStateChangeManager
@@ -26,10 +25,10 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
private let signalService: OWSSignalServiceProtocol
private let storageServiceManager: StorageServiceManager
private let svr: SecureValueRecovery
private let syncManager: Shims.SyncManager
private let syncManager: SyncManagerProtocol
private let threadStore: ThreadStore
private let tsAccountManager: TSAccountManager
private let udManager: Shims.UDManager
private let udManager: OWSUDManager
init(
chatConnectionManager: ChatConnectionManager,
@@ -37,10 +36,9 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
identityManager: OWSIdentityManager,
linkAndSyncManager: LinkAndSyncManager,
accountKeyStore: AccountKeyStore,
messageFactory: Shims.MessageFactory,
networkManager: any NetworkManagerProtocol,
preKeyManager: PreKeyManager,
profileManager: Shims.ProfileManager,
profileManager: ProfileManager,
pushRegistrationManager: Shims.PushRegistrationManager,
receiptManager: Shims.ReceiptManager,
registrationStateChangeManager: RegistrationStateChangeManager,
@@ -49,17 +47,16 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
signalService: OWSSignalServiceProtocol,
storageServiceManager: StorageServiceManager,
svr: SecureValueRecovery,
syncManager: Shims.SyncManager,
syncManager: SyncManagerProtocol,
threadStore: ThreadStore,
tsAccountManager: TSAccountManager,
udManager: Shims.UDManager
udManager: OWSUDManager
) {
self.chatConnectionManager = chatConnectionManager
self.db = db
self.identityManager = identityManager
self.linkAndSyncManager = linkAndSyncManager
self.accountKeyStore = accountKeyStore
self.messageFactory = messageFactory
self.networkManager = networkManager
self.preKeyManager = preKeyManager
self.profileManager = profileManager
@@ -369,7 +366,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.profileManager.setLocalProfileKey(
provisionMessage.profileKey,
userProfileWriter: .linking,
tx: tx
transaction: tx
)
self.tsAccountManager.setRegistrationId(aciRegistrationId, for: .aci, tx: tx)
@@ -416,7 +413,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.profileManager.setLocalProfileKey(
Aes256Key.generateRandom(),
userProfileWriter: .linking,
tx: tx
transaction: tx
)
self.svr.clearKeys(transaction: tx)
@@ -576,7 +573,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
let orderedThreadIds: [String]
do {
orderedThreadIds = try await syncManager
.sendInitialSyncRequestsAwaitingCreatedThreadOrdering(timeout: 60)
.sendInitialSyncRequestsAwaitingCreatedThreadOrdering(timeoutSeconds: 60).awaitable()
} catch {
throw .genericError(error)
}
@@ -590,7 +587,8 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
owsFailDebug("thread was unexpectedly nil")
continue
}
self.messageFactory.insertInfoMessage(into: thread, messageType: .syncedThread, tx: tx)
let infoMessage = TSInfoMessage(thread: thread, messageType: .syncedThread)
infoMessage.anyInsert(transaction: tx)
}
}
}
@@ -705,7 +703,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
tx: DBReadTransaction
) -> AccountAttributes {
let udAccessKey = SMKUDAccessKey(profileKey: profileKey).keyData.base64EncodedString()
let allowUnrestrictedUD = udManager.shouldAllowUnrestrictedAccessLocal(tx: tx)
let allowUnrestrictedUD = udManager.shouldAllowUnrestrictedAccessLocal(transaction: tx)
// Linked-device provisioning uses the same AccountAttributes object as
// primary-device registration; however, the reglock token is ignored by

View File

@@ -9,12 +9,10 @@ public import SignalServiceKit
extension ProvisioningManager {
public enum Shims {
public typealias ReceiptManager = _ProvisioningManager_ReceiptManagerShim
public typealias ProfileManager = _ProvisioningManager_ProfileManagerShim
}
public enum Wrappers {
public typealias ReceiptManager = _ProvisioningManager_ReceiptManagerWrapper
public typealias ProfileManager = _ProvisioningManager_ProfileManagerWrapper
}
}
@@ -30,23 +28,7 @@ public class _ProvisioningManager_ReceiptManagerWrapper: _ProvisioningManager_Re
}
public func areReadReceiptsEnabled(tx: DBReadTransaction) -> Bool {
return OWSReceiptManager.areReadReceiptsEnabled(transaction: SDSDB.shimOnlyBridge(tx))
}
}
public protocol _ProvisioningManager_ProfileManagerShim {
func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile?
}
public class _ProvisioningManager_ProfileManagerWrapper: _ProvisioningManager_ProfileManagerShim {
private let profileManager: ProfileManagerProtocol
public init(_ profileManager: ProfileManagerProtocol) {
self.profileManager = profileManager
}
public func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile? {
return profileManager.localUserProfile(tx: SDSDB.shimOnlyBridge(tx))
return OWSReceiptManager.areReadReceiptsEnabled(transaction: tx)
}
}
@@ -54,7 +36,6 @@ public class _ProvisioningManager_ProfileManagerWrapper: _ProvisioningManager_Pr
extension ProvisioningManager {
public enum Mocks {
public typealias ReceiptManager = _ProvisioningManager_ReceiptManagerMock
public typealias ProfileManager = _ProvisioningManager_ProfileManagerMock
}
}
@@ -63,9 +44,4 @@ public class _ProvisioningManager_ReceiptManagerMock: _ProvisioningManager_Recei
public func areReadReceiptsEnabled(tx: DBReadTransaction) -> Bool { areReadReceiptsEnabledValue }
}
public class _ProvisioningManager_ProfileManagerMock: _ProvisioningManager_ProfileManagerShim {
public var localUserProfile: OWSUserProfile?
public func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile? { localUserProfile }
}
#endif

View File

@@ -15,7 +15,7 @@ public class ProvisioningManager {
private let deviceProvisioningService: DeviceProvisioningService
private let identityManager: OWSIdentityManager
private let linkAndSyncManager: LinkAndSyncManager
private let profileManager: Shims.ProfileManager
private let profileManager: ProfileManager
private let receiptManager: Shims.ReceiptManager
private let tsAccountManager: TSAccountManager
@@ -26,7 +26,7 @@ public class ProvisioningManager {
deviceProvisioningService: DeviceProvisioningService,
identityManager: OWSIdentityManager,
linkAndSyncManager: LinkAndSyncManager,
profileManager: Shims.ProfileManager,
profileManager: ProfileManager,
receiptManager: Shims.ReceiptManager,
tsAccountManager: TSAccountManager
) {

View File

@@ -89,8 +89,9 @@ public class QuickRestoreManager {
let lastBackupTime: UInt64?
let lastBackupSizeBytes: UInt64?
if backupTier != nil {
lastBackupTime = backupSettingsStore.lastBackupDate(tx: tx)?.ows_millisecondsSince1970
lastBackupSizeBytes = backupSettingsStore.lastBackupSizeBytes(tx: tx)
let lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
lastBackupTime = lastBackupDetails?.date.ows_millisecondsSince1970
lastBackupSizeBytes = lastBackupDetails?.backupTotalSizeBytes
} else {
lastBackupTime = nil
lastBackupSizeBytes = nil

View File

@@ -40,10 +40,9 @@ class ProvisioningController: NSObject {
identityManager: DependenciesBridge.shared.identityManager,
linkAndSyncManager: DependenciesBridge.shared.linkAndSyncManager,
accountKeyStore: DependenciesBridge.shared.accountKeyStore,
messageFactory: ProvisioningCoordinatorImpl.Wrappers.MessageFactory(),
networkManager: SSKEnvironment.shared.networkManagerRef,
preKeyManager: DependenciesBridge.shared.preKeyManager,
profileManager: ProvisioningCoordinatorImpl.Wrappers.ProfileManager(SSKEnvironment.shared.profileManagerImplRef),
profileManager: SSKEnvironment.shared.profileManagerImplRef,
pushRegistrationManager: ProvisioningCoordinatorImpl.Wrappers.PushRegistrationManager(AppEnvironment.shared.pushRegistrationManagerRef),
receiptManager: ProvisioningCoordinatorImpl.Wrappers.ReceiptManager(SSKEnvironment.shared.receiptManagerRef),
registrationStateChangeManager: DependenciesBridge.shared.registrationStateChangeManager,
@@ -52,10 +51,10 @@ class ProvisioningController: NSObject {
signalService: SSKEnvironment.shared.signalServiceRef,
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
svr: DependenciesBridge.shared.svr,
syncManager: ProvisioningCoordinatorImpl.Wrappers.SyncManager(SSKEnvironment.shared.syncManagerRef),
syncManager: SSKEnvironment.shared.syncManagerRef,
threadStore: ThreadStoreImpl(),
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
udManager: ProvisioningCoordinatorImpl.Wrappers.UDManager(SSKEnvironment.shared.udManagerRef)
udManager: SSKEnvironment.shared.udManagerRef
)
}()

View File

@@ -149,11 +149,11 @@ public class _RegistrationCoordinator_ExperienceManagerWrapper: _RegistrationCoo
public init() {}
public func clearIntroducingPinsExperience(_ tx: DBWriteTransaction) {
ExperienceUpgradeManager.clearExperienceUpgrade(.introducingPins, transaction: SDSDB.shimOnlyBridge(tx))
ExperienceUpgradeManager.clearExperienceUpgrade(.introducingPins, transaction: tx)
}
public func enableAllGetStartedCards(_ tx: DBWriteTransaction) {
GetStartedBannerViewController.enableAllCards(writeTx: SDSDB.shimOnlyBridge(tx))
GetStartedBannerViewController.enableAllCards(writeTx: tx)
}
}
@@ -246,27 +246,27 @@ public class _RegistrationCoordinator_OWS2FAManagerWrapper: _RegistrationCoordin
public init(_ manager: OWS2FAManager) { self.manager = manager }
public func pinCode(_ tx: DBReadTransaction) -> String? {
return manager.pinCode(transaction: SDSDB.shimOnlyBridge(tx))
return manager.pinCode(transaction: tx)
}
public func clearLocalPinCode(_ tx: DBWriteTransaction) {
return manager.clearLocalPinCode(transaction: SDSDB.shimOnlyBridge(tx))
return manager.clearLocalPinCode(transaction: tx)
}
public func isReglockEnabled(_ tx: DBReadTransaction) -> Bool {
return manager.isRegistrationLockV2Enabled(transaction: SDSDB.shimOnlyBridge(tx))
return manager.isRegistrationLockV2Enabled(transaction: tx)
}
public func markPinEnabled(pin: String, resetReminderInterval: Bool, tx: DBWriteTransaction) {
manager.markEnabled(
pin: pin,
resetReminderInterval: resetReminderInterval,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
}
public func markRegistrationLockEnabled(_ tx: DBWriteTransaction) {
manager.markRegistrationLockV2Enabled(transaction: SDSDB.shimOnlyBridge(tx))
manager.markRegistrationLockV2Enabled(transaction: tx)
}
}
@@ -296,7 +296,7 @@ public class _RegistrationCoordinator_ProfileManagerWrapper: _RegistrationCoordi
public init(_ manager: ProfileManager) { self.manager = manager }
public func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile? {
return manager.localUserProfile(tx: SDSDB.shimOnlyBridge(tx))
return manager.localUserProfile(tx: tx)
}
public func updateLocalProfile(
@@ -316,7 +316,7 @@ public class _RegistrationCoordinator_ProfileManagerWrapper: _RegistrationCoordi
unsavedRotatedProfileKey: nil,
userProfileWriter: .registration,
authedAccount: authedAccount,
tx: SDSDB.shimOnlyBridge(tx)
tx: tx
)
}
@@ -414,11 +414,11 @@ public class _RegistrationCoordinator_ReceiptManagerWrapper: _RegistrationCoordi
public init(_ manager: OWSReceiptManager) { self.manager = manager }
public func setAreReadReceiptsEnabled(_ areEnabled: Bool, _ tx: DBWriteTransaction) {
manager.setAreReadReceiptsEnabled(areEnabled, transaction: SDSDB.shimOnlyBridge(tx))
manager.setAreReadReceiptsEnabled(areEnabled, transaction: tx)
}
public func setAreStoryViewedReceiptsEnabled(_ areEnabled: Bool, _ tx: DBWriteTransaction) {
StoryManager.setAreViewReceiptsEnabled(areEnabled, transaction: SDSDB.shimOnlyBridge(tx))
StoryManager.setAreViewReceiptsEnabled(areEnabled, transaction: tx)
}
}
@@ -514,7 +514,7 @@ public class _RegistrationCoordinator_UDManagerWrapper: _RegistrationCoordinator
public init(_ manager: OWSUDManager) { self.manager = manager }
public func shouldAllowUnrestrictedAccessLocal(transaction: DBReadTransaction) -> Bool {
return manager.shouldAllowUnrestrictedAccessLocal(transaction: SDSDB.shimOnlyBridge(transaction))
return manager.shouldAllowUnrestrictedAccessLocal(transaction: transaction)
}
}

View File

@@ -6,7 +6,7 @@
import SignalServiceKit
import SignalUI
class RegistrationLoadingViewController: OWSViewController {
class RegistrationLoadingViewController: OWSViewController, OWSNavigationChildController {
enum RegistrationLoadingMode {
case generic
case submittingPhoneNumber(e164: String)
@@ -47,6 +47,14 @@ class RegistrationLoadingViewController: OWSViewController {
owsFail("This should not be called")
}
// MARK: OWSNavigationChildController
public var preferredNavigationBarStyle: OWSNavigationBarStyle { .solid }
public var navbarBackgroundColorOverride: UIColor? { .clear }
public var prefersNavigationBarHidden: Bool { true }
// MARK: - Rendering
private let spinnerView: AnimatedProgressView

View File

@@ -31,7 +31,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>7.86</string>
<string>7.87</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -70,7 +70,7 @@ final class FullTextSearchOptimizer {
let mergeResult = try await db.awaitableWrite { tx -> SqliteUtil.Fts5.MergeResult in
return try SqliteUtil.Fts5.merge(
db: SDSDB.shimOnlyBridge(tx).database,
db: tx.database,
ftsTableName: FullTextSearchIndexer.ftsTableName,
numberOfPages: Constants.numberOfPagesToMergeAtATime,
isFirstBatch: isFirstBatch

View File

@@ -1,80 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.895752 2.342712 cm
0.000000 0.000000 0.000000 scn
9.210154 18.209368 m
9.210154 19.280308 10.468915 19.855375 11.278470 19.154280 c
19.132828 12.352206 l
19.708405 11.853743 19.708405 10.960849 19.132828 10.462384 c
11.278470 3.660312 l
10.468915 2.959219 9.210153 3.534283 9.210153 4.605223 c
9.210153 7.065169 l
6.939192 6.937289 5.450091 6.225606 4.515990 5.266563 c
3.510902 4.234637 3.085680 2.852089 3.085680 1.411121 c
3.085680 0.978930 2.784561 0.673292 2.499216 0.546316 c
2.210415 0.417801 1.712402 0.393576 1.436146 0.842747 c
0.525255 2.323795 0.000000 4.067596 0.000000 5.932551 c
0.000000 10.994320 4.302614 15.103045 9.210154 15.608706 c
9.210154 18.209368 l
h
f
n
Q
endstream
endobj
3 0 obj
751
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000841 00000 n
0000000863 00000 n
0000001036 00000 n
0000001110 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1169
%%EOF

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "forward-fill.pdf",
"filename" : "pin-slash.pdf",
"idiom" : "universal"
}
],

Binary file not shown.

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "pin.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "pollStop-light.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.250000 16.000000 cm
0.000000 0.000000 0.000000 scn
7.375000 -1.750000 m
7.375000 -1.128680 7.878680 -0.625000 8.500000 -0.625000 c
13.000000 -0.625000 l
13.621321 -0.625000 14.125000 -1.128680 14.125000 -1.750000 c
14.125000 -6.250000 l
14.125000 -6.871321 13.621321 -7.375000 13.000000 -7.375000 c
8.500000 -7.375000 l
7.878680 -7.375000 7.375000 -6.871321 7.375000 -6.250000 c
7.375000 -1.750000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.250000 1.250000 cm
0.000000 0.000000 0.000000 scn
10.750000 21.500000 m
4.812939 21.500000 0.000000 16.687061 0.000000 10.750000 c
0.000000 4.812939 4.812939 0.000000 10.750000 0.000000 c
16.687061 0.000000 21.500000 4.812939 21.500000 10.750000 c
21.500000 16.687061 16.687061 21.500000 10.750000 21.500000 c
h
1.500000 10.750000 m
1.500000 15.858634 5.641366 20.000000 10.750000 20.000000 c
15.858634 20.000000 20.000000 15.858634 20.000000 10.750000 c
20.000000 5.641366 15.858634 1.500000 10.750000 1.500000 c
5.641366 1.500000 1.500000 5.641366 1.500000 10.750000 c
h
f*
n
Q
endstream
endobj
3 0 obj
1098
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001188 00000 n
0000001211 00000 n
0000001384 00000 n
0000001458 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1517
%%EOF

View File

@@ -303,11 +303,15 @@ class DeleteAccountConfirmationViewController: OWSTableViewController2 {
progressView.startAnimating { overlayView.alpha = 1 }
do {
try await self.deleteDonationSubscriptionIfNecessary()
try await self.leaveGroups()
try await self.unregisterAccount()
try await { () async throws -> Never in
try await self.deleteDonationSubscriptionIfNecessary()
try await self.deleteBackupIfNecessary()
try await self.leaveGroups()
try await self.unregisterAccount()
resetAppDataAndExit()
}()
} catch {
owsFailDebug("Failed to unregister \(error)")
owsFailDebug("Failed to delete account! \(error)")
progressView.stopAnimating(success: false) {
overlayView.alpha = 0
@@ -326,6 +330,46 @@ class DeleteAccountConfirmationViewController: OWSTableViewController2 {
}
}
private func deleteBackupIfNecessary() async throws {
let backupKeyService = DependenciesBridge.shared.backupKeyService
let backupSettingsStore = BackupSettingsStore()
let db = DependenciesBridge.shared.db
let logger = PrefixedLogger(prefix: "[Backups]")
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let (localIdentifiers, currentBackupPlan): (
LocalIdentifiers?,
BackupPlan,
) = db.read { tx in
return (
tsAccountManager.localIdentifiers(tx: tx),
backupSettingsStore.backupPlan(tx: tx),
)
}
guard let localIdentifiers else {
return
}
switch currentBackupPlan {
case .disabled:
logger.info("Backups disabled: skipping delete.")
return
case .disabling:
// If we're disabling then BackupDisablingManager is actively trying
// to delete our remote backup, too. Might as well try here too.
break
case .free, .paid, .paidExpiringSoon, .paidAsTester:
break
}
logger.info("Attempting to delete Backups!")
try await backupKeyService.deleteBackupKey(
localIdentifiers: localIdentifiers,
auth: .implicit(),
)
}
private func deleteDonationSubscriptionIfNecessary() async throws {
let activeSubscriptionId = SSKEnvironment.shared.databaseStorageRef.read {
DonationSubscriptionManager.getSubscriberID(transaction: $0)
@@ -381,12 +425,19 @@ class DeleteAccountConfirmationViewController: OWSTableViewController2 {
}
}
private func unregisterAccount() async throws -> Never {
private func unregisterAccount() async throws {
Logger.info("Unregistering...")
try await DependenciesBridge.shared.registrationStateChangeManager.unregisterFromService()
}
var hasEnteredLocalNumber: Bool {
private func resetAppDataAndExit() -> Never {
let keyFetcher = SSKEnvironment.shared.databaseStorageRef.keyFetcher
SignalApp.resetAppDataAndExit(keyFetcher: keyFetcher)
}
// MARK: -
private var hasEnteredLocalNumber: Bool {
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
guard let localNumber = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.phoneNumber else {
owsFailDebug("local number unexpectedly nil")
@@ -404,6 +455,8 @@ class DeleteAccountConfirmationViewController: OWSTableViewController2 {
}
}
// MARK: - CountryCodeViewControllerDelegate
extension DeleteAccountConfirmationViewController: CountryCodeViewControllerDelegate {
public func countryCodeViewController(_ vc: CountryCodeViewController, didSelectCountry country: PhoneNumberCountry) {
updateCountry(country)
@@ -431,6 +484,8 @@ extension DeleteAccountConfirmationViewController: CountryCodeViewControllerDele
}
}
// MARK: - UITextFieldDelegate
extension DeleteAccountConfirmationViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
didTapDelete()

View File

@@ -169,48 +169,44 @@ class ChatsSettingsViewController: OWSTableViewController2 {
// MARK: -
private func didTapClearHistory() {
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: self
) { _, threadSoftDeleteManager in
let primaryConfirmDeletionTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION",
comment: "Alert message before user confirms clearing history"
)
let secondaryConfirmDeletionTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_SECONDARY_TITLE",
comment: "Secondary alert title before user confirms clearing history"
)
let secondaryConfirmDeletionMessage = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_SECONDARY_MESSAGE",
comment: "Secondary alert message before user confirms clearing history"
)
let confirmDeletionButtonTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON",
comment: "Confirmation text for button which deletes all message, calling, attachments, etc."
)
let primaryConfirmDeletionTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION",
comment: "Alert message before user confirms clearing history"
)
let secondaryConfirmDeletionTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_SECONDARY_TITLE",
comment: "Secondary alert title before user confirms clearing history"
)
let secondaryConfirmDeletionMessage = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_SECONDARY_MESSAGE",
comment: "Secondary alert message before user confirms clearing history"
)
let confirmDeletionButtonTitle = OWSLocalizedString(
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON",
comment: "Confirmation text for button which deletes all message, calling, attachments, etc."
)
// Show two layers of confirmation here this is a maximally
// destructive action.
// Show two layers of confirmation here this is a maximally
// destructive action.
OWSActionSheets.showConfirmationAlert(
title: primaryConfirmDeletionTitle,
proceedTitle: confirmDeletionButtonTitle,
proceedStyle: .destructive,
) { [weak self] _ in
OWSActionSheets.showConfirmationAlert(
title: primaryConfirmDeletionTitle,
title: secondaryConfirmDeletionTitle,
message: secondaryConfirmDeletionMessage,
proceedTitle: confirmDeletionButtonTitle,
proceedStyle: .destructive,
proceedStyle: .destructive
) { [weak self] _ in
OWSActionSheets.showConfirmationAlert(
title: secondaryConfirmDeletionTitle,
message: secondaryConfirmDeletionMessage,
proceedTitle: confirmDeletionButtonTitle,
proceedStyle: .destructive
) { [weak self] _ in
self?.clearHistoryBehindSpinner(threadSoftDeleteManager: threadSoftDeleteManager)
}
self?.clearHistoryBehindSpinner()
}
}
}
private func clearHistoryBehindSpinner(
threadSoftDeleteManager: any ThreadSoftDeleteManager
) {
private func clearHistoryBehindSpinner() {
let threadSoftDeleteManager = DependenciesBridge.shared.threadSoftDeleteManager
ModalActivityIndicatorViewController.present(
fromViewController: self,
canCancel: false,

View File

@@ -8,31 +8,49 @@ import GRDB
import SignalServiceKit
import SignalUI
private func folderSizeRecursive(ofPath dirPath: String) -> UInt64? {
do {
let filePaths = try OWSFileSystem.recursiveFilesInDirectory(dirPath)
var sum: UInt64 = 0
for filePath in filePaths {
sum += try OWSFileSystem.fileSize(ofPath: filePath)
}
return sum
} catch {
Logger.error("Couldn't fetch file sizes \(error)")
return nil
}
}
private func folderSizeRecursive(of dirUrl: URL) -> UInt64? {
return folderSizeRecursive(ofPath: dirUrl.path)
}
class InternalDiskUsageViewController: OWSTableViewController2 {
struct DiskUsage {
let dbSize = UInt(SSKEnvironment.shared.databaseStorageRef.databaseFileSize)
let dbWalSize = UInt(SSKEnvironment.shared.databaseStorageRef.databaseWALFileSize)
let dbShmSize = UInt(SSKEnvironment.shared.databaseStorageRef.databaseSHMFileSize)
let attachmentSize = OWSFileSystem.folderSizeRecursive(of: AttachmentStream.attachmentsDirectory())?.uintValue
let emojiCacheSize = OWSFileSystem.folderSizeRecursive(of: Emoji.cacheUrl)?.uintValue
let stickerCacheSize = OWSFileSystem.folderSizeRecursive(of: StickerManager.cacheDirUrl())?.uintValue
let dbSize = SSKEnvironment.shared.databaseStorageRef.databaseFileSize
let dbWalSize = SSKEnvironment.shared.databaseStorageRef.databaseWALFileSize
let dbShmSize = SSKEnvironment.shared.databaseStorageRef.databaseSHMFileSize
let attachmentSize = folderSizeRecursive(of: AttachmentStream.attachmentsDirectory())
let emojiCacheSize = folderSizeRecursive(of: Emoji.cacheUrl)
let stickerCacheSize = folderSizeRecursive(of: StickerManager.cacheDirUrl())
let avatarCacheSize =
(OWSFileSystem.folderSizeRecursive(of: AvatarBuilder.avatarCacheDirectory)?.uintValue ?? 0)
+ (OWSFileSystem.folderSizeRecursive(ofPath: OWSUserProfile.sharedDataProfileAvatarsDirPath)?.uintValue ?? 0)
+ (OWSFileSystem.folderSizeRecursive(ofPath: OWSUserProfile.legacyProfileAvatarsDirPath)?.uintValue ?? 0)
let voiceMessageCacheSize = OWSFileSystem.folderSizeRecursive(of: VoiceMessageInterruptedDraftStore.draftVoiceMessageDirectory)?.uintValue
let librarySize = OWSFileSystem.folderSizeRecursive(ofPath: OWSFileSystem.appLibraryDirectoryPath())?.uintValue ?? 0
let libraryCachesSize = OWSFileSystem.folderSizeRecursive(ofPath: OWSFileSystem.cachesDirectoryPath())?.uintValue ?? 0
let documentsSize = OWSFileSystem.folderSizeRecursive(ofPath: OWSFileSystem.appDocumentDirectoryPath())?.uintValue
let sharedDataSize = OWSFileSystem.folderSizeRecursive(ofPath: OWSFileSystem.appSharedDataDirectoryPath())?.uintValue
(folderSizeRecursive(of: AvatarBuilder.avatarCacheDirectory) ?? 0)
+ (folderSizeRecursive(ofPath: OWSUserProfile.sharedDataProfileAvatarsDirPath) ?? 0)
+ (folderSizeRecursive(ofPath: OWSUserProfile.legacyProfileAvatarsDirPath) ?? 0)
let voiceMessageCacheSize = folderSizeRecursive(of: VoiceMessageInterruptedDraftStore.draftVoiceMessageDirectory)
let librarySize = folderSizeRecursive(ofPath: OWSFileSystem.appLibraryDirectoryPath()) ?? 0
let libraryCachesSize = folderSizeRecursive(ofPath: OWSFileSystem.cachesDirectoryPath()) ?? 0
let documentsSize = folderSizeRecursive(ofPath: OWSFileSystem.appDocumentDirectoryPath())
let sharedDataSize = folderSizeRecursive(ofPath: OWSFileSystem.appSharedDataDirectoryPath())
let bundleSize = OWSFileSystem.folderSizeRecursive(ofPath: Bundle.main.bundlePath)?.uintValue
let tmpSize = OWSFileSystem.folderSizeRecursive(ofPath: OWSTemporaryDirectory())?.uintValue
let bundleSize = folderSizeRecursive(ofPath: Bundle.main.bundlePath)
let tmpSize = folderSizeRecursive(ofPath: OWSTemporaryDirectory())
}
let diskUsage: DiskUsage
let orphanedAttachmentByteCount: UInt
let orphanedAttachmentByteCount: UInt64
public nonisolated static func build() async -> InternalDiskUsageViewController {
await Task.yield()
@@ -49,7 +67,7 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
private init(
diskUsage: DiskUsage,
orphanedAttachmentByteCount: UInt,
orphanedAttachmentByteCount: UInt64,
) {
self.diskUsage = diskUsage
self.orphanedAttachmentByteCount = orphanedAttachmentByteCount
@@ -69,7 +87,7 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
let byteCountFormatter = ByteCountFormatter()
let knownFilesSize: UInt = [UInt?](
let knownFilesSize: UInt64 = [UInt64?](
arrayLiteral:
diskUsage.dbSize,
diskUsage.dbWalSize,
@@ -83,11 +101,11 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
)
.compacted()
.reduce(0, +)
var totalFilesystemSize: UInt = [UInt?](
var totalFilesystemSize: UInt64 = [UInt64?](
arrayLiteral:
diskUsage.librarySize,
diskUsage.documentsSize,
diskUsage.sharedDataSize
diskUsage.sharedDataSize,
)
.compacted()
.reduce(0, +)
@@ -107,15 +125,15 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
diskUsageSection.add(.copyableItem(label: "Ancillary files size", value: byteCountFormatter.string(for: totalFilesystemSize - knownFilesSize)))
if TSConstants.isUsingProductionService {
let stagingSharedDataSize = OWSFileSystem.folderSizeRecursive(
let stagingSharedDataSize = folderSizeRecursive(
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsStaging().applicationGroup)!.path
)?.uintValue
)
diskUsageSection.add(.copyableItem(label: "Staging app group size", value: byteCountFormatter.string(for: stagingSharedDataSize)))
totalFilesystemSize += stagingSharedDataSize ?? 0
} else {
let prodSharedDataSize = OWSFileSystem.folderSizeRecursive(
let prodSharedDataSize = folderSizeRecursive(
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsProduction().applicationGroup)!.path
)?.uintValue
)
diskUsageSection.add(.copyableItem(label: "Prod app group size", value: byteCountFormatter.string(for: prodSharedDataSize)))
totalFilesystemSize += prodSharedDataSize ?? 0
}
@@ -135,7 +153,7 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
self.contents = contents
}
private static func orphanAttachmentByteCount() -> UInt {
private static func orphanAttachmentByteCount() -> UInt64 {
var attachmentDirFiles = Set(try! OWSFileSystem.recursiveFilesInDirectory(AttachmentStream.attachmentsDirectory().path))
DependenciesBridge.shared.db.read { tx in
let cursor = try! Attachment.Record
@@ -156,9 +174,9 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
if attachmentDirFiles.isEmpty {
return 0
}
var byteCount: UInt = 0
var byteCount: UInt64 = 0
for file in attachmentDirFiles {
byteCount += OWSFileSystem.fileSize(ofPath: file)?.uintValue ?? 0
byteCount += (try? OWSFileSystem.fileSize(ofPath: file)) ?? 0
}
return byteCount
}

View File

@@ -128,12 +128,10 @@ class InternalSettingsViewController: OWSTableViewController2 {
let backupsSection = OWSTableSection(title: "Backups")
let (
lastBackupFileSizeBytes,
) = DependenciesBridge.shared.db.read { tx in
return (
BackupSettingsStore().lastBackupFileSizeBytes(tx: tx)
)
let backupSettingsStore = BackupSettingsStore()
let db = DependenciesBridge.shared.db
let lastBackupDetails = db.read { tx in
return backupSettingsStore.lastBackupDetails(tx: tx)
}
if mode != .registration {
@@ -187,7 +185,7 @@ class InternalSettingsViewController: OWSTableViewController2 {
})
backupsSection.add(.copyableItem(
label: "Last Backup chats/messages file size",
value: lastBackupFileSizeBytes.flatMap { ByteCountFormatter().string(for: $0) }
value: lastBackupDetails.flatMap { ByteCountFormatter().string(for: $0.backupFileSizeBytes) }
))
if backupsSection.items.isEmpty.negated {

View File

@@ -359,7 +359,7 @@ extension LinkedDevicesViewModel: LinkDeviceViewControllerDelegate {
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
ExperienceUpgradeManager.clearExperienceUpgrade(
.newLinkedDeviceNotification,
transaction: SDSDB.shimOnlyBridge(tx)
transaction: tx
)
}
}

View File

@@ -187,7 +187,7 @@ class AttachmentFormatPickerView: UIView {
private static var contactCases: [AttachmentType] {
var casesToExclude: [AttachmentType] = [.poll]
if !SUIEnvironment.shared.paymentsRef.shouldShowPaymentsUI {
if !SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled {
casesToExclude.append(.payment)
}

View File

@@ -38,6 +38,11 @@ class AttachmentKeyboard: CustomKeyboard {
private lazy var limitedPhotoPermissionsView = LimitedPhotoPermissionsView()
private var topInset: CGFloat {
guard #available(iOS 26, *) else { return 12 }
return traitCollection.verticalSizeClass == .compact ? 20 : 36
}
// MARK: -
init(delegate: AttachmentKeyboardDelegate?) {
@@ -45,14 +50,7 @@ class AttachmentKeyboard: CustomKeyboard {
super.init()
let topInset: CGFloat
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable {
topInset = 40
backgroundColor = .clear
} else {
topInset = 12
backgroundColor = UIColor.Signal.background
}
backgroundColor = if #available(iOS 26, *) { .clear } else { .Signal.background }
let stackView = UIStackView(arrangedSubviews: [
limitedPhotoPermissionsView,
@@ -63,9 +61,21 @@ class AttachmentKeyboard: CustomKeyboard {
stackView.setCustomSpacing(12, after: limitedPhotoPermissionsView)
limitedPhotoPermissionsView.isHiddenInStackView = true
contentView.addSubview(stackView)
stackView.autoPinWidthToSuperview()
stackView.autoPinEdge(toSuperviewEdge: .top, withInset: topInset)
stackView.autoPinEdge(toSuperviewSafeArea: .bottom)
stackView.translatesAutoresizingMaskIntoConstraints = false
let topEdgeConstraint = stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: topInset)
NSLayoutConstraint.activate([
topEdgeConstraint,
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
])
// Variable top inset on iOS 26.
if #available(iOS 26, *) {
registerForTraitChanges([ UITraitVerticalSizeClass.self ]) { (self: Self, _) in
topEdgeConstraint.constant = self.topInset
}
}
}
required init?(coder aDecoder: NSCoder) {

View File

@@ -29,23 +29,12 @@ class LimitedPhotoPermissionsView: UIView {
CurrentAppContext().openSystemSettings()
}
var buttonConfig = UIButton.Configuration.gray()
buttonConfig.cornerStyle = .capsule
buttonConfig.baseForegroundColor = UIColor.Signal.label
buttonConfig.baseBackgroundColor = UIColor.Signal.tertiaryFill
var titleAttributedString = AttributedString(
OWSLocalizedString(
"ATTACHMENT_KEYBOARD_BUTTON_MANAGE",
comment: "Button in chat attachment panel that allows to select photos/videos Signal has access to."
)
)
titleAttributedString.font = UIFont.dynamicTypeFootnoteClamped.medium()
buttonConfig.attributedTitle = titleAttributedString
let button = UIButton(configuration: buttonConfig)
let button = UIButton(configuration: .smallSecondary(title: OWSLocalizedString(
"ATTACHMENT_KEYBOARD_BUTTON_MANAGE",
comment: "Button in chat attachment panel that allows to select photos/videos Signal has access to."
)))
button.menu = UIMenu(children: [settingsAction, selectMoreAction])
button.showsMenuAsPrimaryAction = true
button.titleLabel?.font = .dynamicTypeFootnoteClamped
return button
}()

View File

@@ -66,7 +66,6 @@ class RecentPhotosCollectionView: UICollectionView {
// That delegate method is necessary to allow custom size for "manage access" helper UI.
setCollectionViewLayout(RecentPhotosCollectionView.collectionViewLayout(itemSize: cellSize), animated: false)
reloadData() // Needed in order to reload photos with better quality on size change.
scrollToFirstItem()
}
}
@@ -127,7 +126,6 @@ class RecentPhotosCollectionView: UICollectionView {
func prepareForPresentation() {
UIView.performWithoutAnimation {
self.alpha = 0
self.scrollToFirstItem()
}
}
@@ -137,11 +135,6 @@ class RecentPhotosCollectionView: UICollectionView {
}
}
private func scrollToFirstItem() {
guard numberOfSections > 0, numberOfItems(inSection: 0) > 0 else { return }
scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
}
// Background view
private var hasPhotos: Bool {
@@ -180,10 +173,16 @@ class RecentPhotosCollectionView: UICollectionView {
}
private func noPhotosView() -> UIView {
let titleLabel = titleLabel(text: OWSLocalizedString(
let titleLabel = UILabel()
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.font = .dynamicTypeHeadlineClamped
titleLabel.textColor = .Signal.label
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
titleLabel.text = OWSLocalizedString(
"ATTACHMENT_KEYBOARD_NO_MEDIA_TITLE",
comment: "First block of text in chat attachment panel when there's no recent photos to show."
))
)
let bodyLabel = textLabel(text: OWSLocalizedString(
"ATTACHMENT_KEYBOARD_NO_MEDIA_BODY",
comment: "Second block of text in chat attachment panel when there's no recent photos to show."
@@ -200,14 +199,16 @@ class RecentPhotosCollectionView: UICollectionView {
"ATTACHMENT_KEYBOARD_NO_PHOTO_ACCESS",
comment: "Text in chat attachment panel explaining that user needs to give Signal permission to access photos."
))
let button = button(title: OWSLocalizedString(
"ATTACHMENT_KEYBOARD_OPEN_SETTINGS",
comment: "Button in chat attachment panel to let user open Settings app and give Signal persmission to access photos."
))
button.block = {
let openAppSettingsUrl = URL(string: UIApplication.openSettingsURLString)!
UIApplication.shared.open(openAppSettingsUrl)
}
let button = UIButton(
configuration: .smallSecondary(title: OWSLocalizedString(
"ATTACHMENT_KEYBOARD_OPEN_SETTINGS",
comment: "Button in chat attachment panel to let user open Settings app and give Signal persmission to access photos."
)),
primaryAction: UIAction { _ in
let openAppSettingsUrl = URL(string: UIApplication.openSettingsURLString)!
UIApplication.shared.open(openAppSettingsUrl)
}
)
let stackView = UIStackView(arrangedSubviews: [ textLabel, button ])
stackView.axis = .vertical
stackView.spacing = 16
@@ -215,46 +216,17 @@ class RecentPhotosCollectionView: UICollectionView {
return stackView
}
private func titleLabel(text: String) -> UILabel {
let label = UILabel()
label.font = .dynamicTypeHeadlineClamped
label.textColor = Theme.isDarkThemeEnabled ? .ows_gray20 : UIColor(rgbHex: 0x434343).withAlphaComponent(0.8)
label.textAlignment = .center
label.numberOfLines = 2
label.text = text
return label
}
private func textLabel(text: String) -> UILabel {
let label = UILabel()
label.adjustsFontForContentSizeCategory = true
label.font = .dynamicTypeSubheadlineClamped
label.lineBreakMode = .byWordWrapping
label.textColor = Theme.isDarkThemeEnabled ? .ows_gray25 : .ows_blackAlpha50
label.textColor = .Signal.secondaryLabel
label.textAlignment = .center
label.numberOfLines = 0
label.text = text
return label
}
private func button(title: String) -> OWSButton {
let button = OWSButton()
let backgroundColor = Theme.isDarkThemeEnabled ? UIColor(white: 1, alpha: 0.16) : UIColor(white: 0, alpha: 0.08)
button.setBackgroundImage(UIImage.image(color: backgroundColor), for: .normal)
let highlightedBgColor = Theme.isDarkThemeEnabled ? UIColor(white: 1, alpha: 0.26) : UIColor(white: 0, alpha: 0.18)
button.setBackgroundImage(UIImage.image(color: highlightedBgColor), for: .highlighted)
button.ows_contentEdgeInsets = UIEdgeInsets(top: 7, leading: 16, bottom: 7, trailing: 16)
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 32).isActive = true
button.layer.masksToBounds = true
button.layer.cornerRadius = 16
button.setTitle(title, for: .normal)
button.setTitleColor(Theme.isDarkThemeEnabled ? .ows_gray05 : .black, for: .normal)
button.titleLabel?.font = .dynamicTypeSubheadlineClamped.semibold()
return button
}
}
extension RecentPhotosCollectionView: PhotoLibraryDelegate {
@@ -376,11 +348,12 @@ private class RecentPhotoCell: UICollectionViewCell {
contentView.addSubview(loadingIndicator)
loadingIndicator.autoCenterInSuperview()
#if compiler(>=6.2)
if #available(iOS 26, *) {
contentView.cornerConfiguration = .uniformCorners(radius: .fixed(36))
updateCornerRadius()
registerForTraitChanges([ UITraitVerticalSizeClass.self ]) { (self: Self, _) in
self.updateCornerRadius()
}
}
#endif
}
@available(*, unavailable, message: "Unimplemented")
@@ -390,13 +363,19 @@ private class RecentPhotoCell: UICollectionViewCell {
override var bounds: CGRect {
didSet {
if #available(iOS 26, *), BuildFlags.iOS26SDKIsAvailable { return }
updateCornerRadius()
guard #unavailable(iOS 26) else { return }
updateCornerRadiusLegacy()
}
}
@available(iOS, deprecated: 26)
@available(iOS 26, *)
private func updateCornerRadius() {
let cornerRadius: CGFloat = traitCollection.verticalSizeClass == .compact ? 20 : 36
contentView.cornerConfiguration = .uniformCorners(radius: .fixed(cornerRadius))
}
@available(iOS, deprecated: 26)
private func updateCornerRadiusLegacy() {
let cellSize = min(bounds.width, bounds.height)
guard cellSize > 0 else { return }
contentView.layer.cornerRadius = (cellSize * 13 / 84).rounded()

View File

@@ -41,12 +41,6 @@ class DebugUIPrompts: DebugUIPage {
let flipCamTooltipManager = FlipCameraTooltipManager(db: db)
flipCamTooltipManager.markTooltipAsUnread()
}),
OWSTableItem(title: "Enable DeleteForMeSyncMessage info sheet", actionBlock: {
db.write { tx in
DeleteForMeInfoSheetCoordinator.fromGlobals().forceEnableInfoSheet(tx: tx)
}
}),
]
return OWSTableSection(title: name, items: items)
}

View File

@@ -7,7 +7,7 @@ import Foundation
import SignalUI
import SignalServiceKit
class DonateChoosePaymentMethodSheet: OWSTableSheetViewController {
class DonateChoosePaymentMethodSheet: StackSheetViewController {
enum DonationMode {
case oneTime
case monthly
@@ -91,46 +91,44 @@ class DonateChoosePaymentMethodSheet: OWSTableSheetViewController {
super.init()
}
// MARK: - Updating table contents
public override func tableContents() -> OWSTableContents {
let infoStackView: UIView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 6
if let assets = badge.assets {
let badgeImageView = UIImageView(image: assets.universal160)
badgeImageView.autoSetDimensions(to: CGSize(square: 80))
stackView.addArrangedSubview(badgeImageView)
stackView.setCustomSpacing(12, after: badgeImageView)
}
let titleLabel = UILabel.title2Label(text: titleText)
stackView.addArrangedSubview(titleLabel)
if let bodyText {
let bodyLabel = UILabel.explanationTextLabel(text: bodyText)
stackView.addArrangedSubview(bodyLabel)
}
return stackView
}()
let section = OWSTableSection(items: [.init(customCellBlock: {
let cell = OWSTableItem.newCell()
cell.contentView.addSubview(infoStackView)
infoStackView.autoPinEdgesToSuperviewMargins()
return cell
})])
section.hasBackground = false
section.shouldDisableCellSelection = true
return OWSTableContents(sections: [section])
override var stackViewInsets: UIEdgeInsets {
.init(top: 32, leading: 0, bottom: 0, trailing: 0)
}
public override func tableFooterView() -> UIView? {
// MARK: - Updating table contents
override func viewDidLoad() {
super.viewDidLoad()
stackView.addArrangedSubviews([headerStack(), buttonsStack()])
stackView.spacing = 24
}
public func headerStack() -> UIStackView {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 6
if let assets = badge.assets {
let badgeImageView = UIImageView(image: assets.universal160)
badgeImageView.autoSetDimensions(to: CGSize(square: 80))
stackView.addArrangedSubview(badgeImageView)
stackView.setCustomSpacing(12, after: badgeImageView)
}
let titleLabel = UILabel.title2Label(text: titleText)
stackView.addArrangedSubview(titleLabel)
if let bodyText {
let bodyLabel = UILabel.explanationTextLabel(text: bodyText)
stackView.addArrangedSubview(bodyLabel)
}
return stackView
}
private func buttonsStack() -> UIView {
let paymentMethods: [DonationPaymentMethod]
let applePayFirstRegions = PhoneNumberRegions(arrayLiteral: "1")
@@ -223,7 +221,6 @@ class DonateChoosePaymentMethodSheet: OWSTableSheetViewController {
action: @escaping () -> Void
) -> UIButton {
var configuration: UIButton.Configuration
#if compiler(>=6.2)
if #available(iOS 26, *) {
configuration = UIButton.Configuration.glass()
} else {
@@ -232,12 +229,6 @@ class DonateChoosePaymentMethodSheet: OWSTableSheetViewController {
configuration.baseForegroundColor = .label
configuration.baseBackgroundColor = .Signal.secondaryGroupedBackground
}
#else
configuration = .bordered()
configuration.background.cornerRadius = 12
configuration.baseForegroundColor = .label
configuration.baseBackgroundColor = .Signal.secondaryGroupedBackground
#endif
configuration.title = title
configuration.titleTextAttributesTransformer = .defaultFont(.dynamicTypeHeadlineClamped)
@@ -254,17 +245,12 @@ class DonateChoosePaymentMethodSheet: OWSTableSheetViewController {
private func createCreditOrDebitCardButton() -> UIButton {
var configuration: UIButton.Configuration
#if compiler(>=6.2)
if #available(iOS 26, *) {
configuration = .prominentGlass()
} else {
configuration = .borderedProminent()
configuration.background.cornerRadius = 12
}
#else
configuration = .borderedProminent()
configuration.background.cornerRadius = 12
#endif
configuration.title = OWSLocalizedString(
"DONATE_CHOOSE_CREDIT_OR_DEBIT_CARD_AS_PAYMENT_METHOD",

View File

@@ -7,8 +7,7 @@ import SignalServiceKit
import SignalUI
protocol ForwardMessageDelegate: AnyObject {
func forwardMessageFlowDidComplete(items: [ForwardMessageItem],
recipientThreads: [TSThread])
func forwardMessageFlowDidComplete(items: [ForwardMessageItem], recipientThreads: [TSThread])
func forwardMessageFlowDidCancel()
}
@@ -18,7 +17,6 @@ class ForwardMessageViewController: OWSNavigationController {
weak var forwardMessageDelegate: ForwardMessageDelegate?
typealias Item = ForwardMessageItem
private typealias Content = ForwardMessageContent
private var content: Content
@@ -69,18 +67,17 @@ class ForwardMessageViewController: OWSNavigationController {
}
class func present(
forItemViewModels itemViewModels: [CVItemViewModelImpl],
forItemViewModel itemViewModel: CVItemViewModelImpl,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate,
) {
do {
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { transaction in
try Content.build(itemViewModels: itemViewModels, transaction: transaction)
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { tx in
return try Content.build(itemViewModel: itemViewModel, tx: tx)
}
present(content: content, from: fromViewController, delegate: delegate)
} catch {
ForwardMessageViewController.showAlertForForwardError(error: error,
forwardedInteractionCount: itemViewModels.count)
ForwardMessageViewController.showAlertForForwardError(error: error, forwardedInteractionCount: 1)
}
}
@@ -90,13 +87,12 @@ class ForwardMessageViewController: OWSNavigationController {
delegate: ForwardMessageDelegate,
) {
do {
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { transaction in
try Content.build(selectionItems: selectionItems, transaction: transaction)
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { tx in
try Content.build(selectionItems: selectionItems, tx: tx)
}
present(content: content, from: fromViewController, delegate: delegate)
} catch {
ForwardMessageViewController.showAlertForForwardError(error: error,
forwardedInteractionCount: selectionItems.count)
ForwardMessageViewController.showAlertForForwardError(error: error, forwardedInteractionCount: selectionItems.count)
}
}
@@ -107,18 +103,11 @@ class ForwardMessageViewController: OWSNavigationController {
delegate: ForwardMessageDelegate
) {
do {
let builder = Item.Builder(interaction: message)
builder.attachments = try attachmentStreams.map { attachmentStream in
try DependenciesBridge.shared.attachmentCloner.cloneAsSignalAttachment(
attachment: attachmentStream
)
let attachments = try attachmentStreams.map { attachmentStream in
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream)
}
let item: Item = builder.build()
present(
content: .single(item: item),
content: ForwardMessageContent(allItems: [ForwardMessageItem(interaction: message, attachments: attachments)]),
from: fromViewController,
delegate: delegate
)
@@ -135,7 +124,8 @@ class ForwardMessageViewController: OWSNavigationController {
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate
) {
let builder = Item.Builder()
var attachments: [SignalAttachment] = []
var textAttachment: TextAttachment?
switch storyMessage.attachment {
case .media:
let attachment: ReferencedAttachmentStream? = SSKEnvironment.shared.databaseStorageRef.read { tx in
@@ -154,10 +144,8 @@ class ForwardMessageViewController: OWSNavigationController {
guard let attachment else {
throw OWSAssertionError("Missing attachment stream for forwarded story message")
}
let signalAttachment = try DependenciesBridge.shared.attachmentCloner.cloneAsSignalAttachment(
attachment: attachment
)
builder.attachments = [signalAttachment]
let signalAttachment = try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachment)
attachments = [signalAttachment]
} catch let error {
ForwardMessageViewController.showAlertForForwardError(
error: error,
@@ -165,10 +153,14 @@ class ForwardMessageViewController: OWSNavigationController {
)
return
}
case .text(let textAttachment):
builder.textAttachment = textAttachment
case .text(let _textAttachment):
textAttachment = _textAttachment
}
present(content: .single(item: builder.build()), from: fromViewController, delegate: delegate)
present(
content: ForwardMessageContent(allItems: [ForwardMessageItem(attachments: attachments, textAttachment: textAttachment)]),
from: fromViewController,
delegate: delegate,
)
}
class func present(
@@ -177,25 +169,17 @@ class ForwardMessageViewController: OWSNavigationController {
delegate: ForwardMessageDelegate
) {
present(
content: .single(item: ForwardMessageItem.Item(
interaction: nil,
attachments: nil,
contactShare: nil,
messageBody: messageBody,
linkPreviewDraft: nil,
stickerMetadata: nil,
stickerAttachment: nil,
textAttachment: nil
)),
content: ForwardMessageContent(allItems: [ForwardMessageItem(messageBody: messageBody)]),
from: fromViewController,
delegate: delegate
)
}
private class func present(content: Content,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate) {
private class func present(
content: Content,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate,
) {
let sheet = ForwardMessageViewController(content: content)
sheet.forwardMessageDelegate = delegate
fromViewController.present(sheet, animated: true) {
@@ -335,7 +319,7 @@ extension ForwardMessageViewController {
}
}
private func send(item: Item, toOutgoingMessageRecipientThreads outgoingMessageRecipientThreads: [TSThread]) async throws {
private func send(item: ForwardMessageItem, toOutgoingMessageRecipientThreads outgoingMessageRecipientThreads: [TSThread]) async throws {
if let stickerMetadata = item.stickerMetadata {
let stickerInfo = stickerMetadata.stickerInfo
if StickerManager.isStickerInstalled(stickerInfo: stickerInfo) {
@@ -355,13 +339,13 @@ extension ForwardMessageViewController {
await enqueueMessageViaThreadUtil(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
self.send(contactShare: contactShare.copyForResending(), thread: recipientThread)
}
} else if let attachments = item.attachments, !attachments.isEmpty {
} else if !item.attachments.isEmpty {
// TODO: What about link previews in this case?
let conversations = selectedConversations
_ = try await AttachmentMultisend.sendApprovedMedia(
conversations: conversations,
approvedMessageBody: item.messageBody,
approvedAttachments: attachments
approvedAttachments: item.attachments,
).enqueuedPromise.awaitable()
} else if let textAttachment = item.textAttachment {
// TODO: we want to reuse the uploaded link preview image attachment instead of re-uploading
@@ -477,7 +461,7 @@ extension ForwardMessageViewController: ConversationPickerDelegate {
extension ForwardMessageViewController {
static func finalizeForward(
items: [Item],
items: [ForwardMessageItem],
recipientThreads: [TSThread],
fromViewController: UIViewController,
) {
@@ -540,11 +524,9 @@ extension ForwardMessageViewController {
// MARK: -
struct ForwardMessageItem {
fileprivate typealias Item = ForwardMessageItem
let interaction: TSInteraction?
let attachments: [SignalAttachment]?
let attachments: [SignalAttachment]
let contactShare: ContactShareViewModel?
let messageBody: MessageBody?
let linkPreviewDraft: OWSLinkPreviewDraft?
@@ -552,46 +534,24 @@ struct ForwardMessageItem {
let stickerAttachment: AttachmentStream?
let textAttachment: TextAttachment?
fileprivate class Builder {
let interaction: TSInteraction?
var attachments: [SignalAttachment]?
var contactShare: ContactShareViewModel?
var messageBody: MessageBody?
var linkPreviewDraft: OWSLinkPreviewDraft?
var stickerMetadata: (any StickerMetadata)?
var stickerAttachment: AttachmentStream?
var textAttachment: TextAttachment?
init(interaction: TSInteraction? = nil) {
self.interaction = interaction
}
func build() -> ForwardMessageItem {
ForwardMessageItem(
interaction: interaction,
attachments: attachments,
contactShare: contactShare,
messageBody: messageBody,
linkPreviewDraft: linkPreviewDraft,
stickerMetadata: stickerMetadata,
stickerAttachment: stickerAttachment,
textAttachment: textAttachment
)
}
}
var isEmpty: Bool {
if let attachments = attachments,
!attachments.isEmpty {
return false
}
if contactShare != nil ||
messageBody != nil ||
stickerMetadata != nil {
return false
}
return true
fileprivate init(
interaction: TSInteraction? = nil,
attachments: [SignalAttachment] = [],
contactShare: ContactShareViewModel? = nil,
messageBody: MessageBody? = nil,
linkPreviewDraft: OWSLinkPreviewDraft? = nil,
stickerMetadata: (any StickerMetadata)? = nil,
stickerAttachment: AttachmentStream? = nil,
textAttachment: TextAttachment? = nil,
) {
self.interaction = interaction
self.attachments = attachments
self.contactShare = contactShare
self.messageBody = messageBody
self.linkPreviewDraft = linkPreviewDraft
self.stickerMetadata = stickerMetadata
self.stickerAttachment = stickerAttachment
self.textAttachment = textAttachment
}
fileprivate static func build(
@@ -599,34 +559,32 @@ struct ForwardMessageItem {
componentState: CVComponentState,
selectionType: CVSelectionType,
transaction: DBReadTransaction
) throws -> Item {
let builder = Builder(interaction: interaction)
let shouldHaveText = (selectionType == .allContent ||
selectionType == .secondaryContent)
let shouldHaveAttachments = (selectionType == .allContent ||
selectionType == .primaryContent)
) throws -> Self {
let shouldHaveText = (selectionType == .allContent || selectionType == .secondaryContent)
let shouldHaveAttachments = (selectionType == .allContent || selectionType == .primaryContent)
guard shouldHaveText || shouldHaveAttachments else {
throw ForwardError.invalidInteraction
}
if shouldHaveText,
let displayableBodyText = componentState.displayableBodyText,
!displayableBodyText.fullTextValue.isEmpty {
var messageBody: MessageBody?
var linkPreviewDraft: OWSLinkPreviewDraft?
if
shouldHaveText,
let displayableBodyText = componentState.displayableBodyText,
!displayableBodyText.fullTextValue.isEmpty
{
switch displayableBodyText.fullTextValue {
case .text(let text):
builder.messageBody = MessageBody(text: text, ranges: .empty)
messageBody = MessageBody(text: text, ranges: .empty)
case .attributedText(let text):
builder.messageBody = MessageBody(text: text.string, ranges: .empty)
messageBody = MessageBody(text: text.string, ranges: .empty)
case .messageBody(let hydratedBody):
builder.messageBody = hydratedBody.asMessageBodyForForwarding(preservingAllMentions: true)
messageBody = hydratedBody.asMessageBodyForForwarding(preservingAllMentions: true)
}
if let linkPreview = componentState.linkPreviewModel, let message = interaction as? TSMessage {
builder.linkPreviewDraft = Self.tryToCloneLinkPreview(
linkPreviewDraft = Self.tryToCloneLinkPreview(
linkPreview: linkPreview,
parentMessage: message,
transaction: transaction
@@ -634,9 +592,13 @@ struct ForwardMessageItem {
}
}
var attachments: [SignalAttachment] = []
var contactShare: ContactShareViewModel?
var stickerMetadata: (any StickerMetadata)?
var stickerAttachment: AttachmentStream?
if shouldHaveAttachments {
if let oldContactShare = componentState.contactShareModel {
builder.contactShare = oldContactShare.copyForRendering()
contactShare = oldContactShare.copyForRendering()
}
var attachmentStreams = [ReferencedAttachmentStream]()
@@ -648,28 +610,33 @@ struct ForwardMessageItem {
attachmentStreams.append(attachmentStream)
}
if !attachmentStreams.isEmpty {
builder.attachments = try attachmentStreams.map { attachmentStream in
try DependenciesBridge.shared.attachmentCloner.cloneAsSignalAttachment(
attachment: attachmentStream
)
}
attachments = try attachmentStreams.map { attachmentStream in
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream)
}
if let stickerMetadata = componentState.stickerMetadata {
builder.stickerMetadata = stickerMetadata
if let stickerAttachment = componentState.stickerAttachment {
builder.stickerAttachment = stickerAttachment
}
}
stickerMetadata = componentState.stickerMetadata
stickerAttachment = (stickerMetadata != nil) ? componentState.stickerAttachment : nil
}
let item = builder.build()
guard !item.isEmpty else {
let isEmpty: Bool = (
attachments.isEmpty
&& contactShare == nil
&& messageBody == nil
&& stickerMetadata == nil
)
guard !isEmpty else {
throw ForwardError.invalidInteraction
}
return item
return Self(
interaction: interaction,
attachments: attachments,
contactShare: contactShare,
messageBody: messageBody,
linkPreviewDraft: linkPreviewDraft,
stickerMetadata: stickerMetadata,
stickerAttachment: stickerAttachment,
)
}
private static func tryToCloneLinkPreview(
@@ -739,25 +706,13 @@ struct ForwardMessageItem {
// MARK: -
private enum ForwardMessageContent {
typealias Item = ForwardMessageItem
case single(item: Item)
case multiple(items: [Item])
var allItems: [Item] {
switch self {
case .single(let item):
return [item]
case .multiple(let items):
return items
}
}
private struct ForwardMessageContent {
let allItems: [ForwardMessageItem]
var canSendToStories: Bool {
allItems.allSatisfy { item in
if let attachments = item.attachments {
return attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo })
return allItems.allSatisfy { item in
if !item.attachments.isEmpty {
return item.attachments.allSatisfy({ $0.dataSource.isValidImage || $0.dataSource.isValidVideo })
} else if item.textAttachment != nil {
return true
} else if item.messageBody != nil {
@@ -769,58 +724,51 @@ private enum ForwardMessageContent {
}
var canSendToNonStories: Bool {
allItems.allSatisfy { $0.textAttachment == nil }
}
private static func build(items: [Item]) -> ForwardMessageContent {
if items.count == 1, let item = items.first {
return .single(item: item)
} else {
return .multiple(items: items)
}
return allItems.allSatisfy { $0.textAttachment == nil }
}
static func build(
itemViewModels: [CVItemViewModelImpl],
transaction: DBReadTransaction
) throws -> ForwardMessageContent {
let items: [Item] = try itemViewModels.map { itemViewModel in
try Item.build(interaction: itemViewModel.interaction,
componentState: itemViewModel.renderItem.componentState,
selectionType: .allContent,
transaction: transaction)
}
return build(items: items)
itemViewModel: CVItemViewModelImpl,
tx: DBReadTransaction
) throws -> Self {
return Self(allItems: [try ForwardMessageItem.build(
interaction: itemViewModel.interaction,
componentState: itemViewModel.renderItem.componentState,
selectionType: .allContent,
transaction: tx,
)])
}
static func build(
selectionItems: [CVSelectionItem],
transaction: DBReadTransaction
) throws -> ForwardMessageContent {
let items: [Item] = try selectionItems.map { selectionItem in
tx: DBReadTransaction
) throws -> Self {
let items = try selectionItems.map { (selectionItem) throws -> ForwardMessageItem in
let interactionId = selectionItem.interactionId
guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId,
transaction: transaction) else {
guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId, transaction: tx) else {
throw ForwardError.missingInteraction
}
let componentState = try buildComponentState(interaction: interaction,
transaction: transaction)
return try Item.build(interaction: interaction,
componentState: componentState,
selectionType: selectionItem.selectionType,
transaction: transaction)
let componentState = try buildComponentState(interaction: interaction, tx: tx)
return try ForwardMessageItem.build(
interaction: interaction,
componentState: componentState,
selectionType: selectionItem.selectionType,
transaction: tx,
)
}
return build(items: items)
return Self(allItems: items)
}
private static func buildComponentState(interaction: TSInteraction,
transaction: DBReadTransaction) throws -> CVComponentState {
private static func buildComponentState(
interaction: TSInteraction,
tx: DBReadTransaction,
) throws(ForwardError) -> CVComponentState {
guard let componentState = CVLoader.buildStandaloneComponentState(
interaction: interaction,
spoilerState: SpoilerRenderState(), // Nothing revealed, doesn't matter.
transaction: transaction
transaction: tx,
) else {
throw ForwardError.invalidInteraction
throw .invalidInteraction
}
return componentState
}

View File

@@ -25,6 +25,9 @@ class CLVTableDataSource: NSObject {
var renderState: CLVRenderState = .empty
/// Used to let chat list cells know when they should use rounded corners for background in `selected` state,
var useSideBarChatListCellAppearance: Bool = false
/// While table view selection is changing, i.e., between
/// `tableView(_:willSelectRowAt:)` and `tableView(_:didSelectRowAt:)`,
/// records the identifier of the newly selected thread, or `nil` if
@@ -609,6 +612,7 @@ extension CLVTableDataSource: UITableViewDataSource {
}
cell.configure(cellContentToken: contentToken, spoilerAnimationManager: viewState.spoilerAnimationManager)
cell.useSidebarAppearance = useSideBarChatListCellAppearance
if isConversationActive(threadUniqueId: contentToken.thread.uniqueId) {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)

View File

@@ -36,6 +36,9 @@ class ChatListCell: UITableViewCell, ReusableTableViewCell {
}
}
/// If set to `true` background in `selected` state would have rounded corners.
var useSidebarAppearance = false
private var cvViews: [CVView] {
[
nameLabel,
@@ -192,7 +195,7 @@ class ChatListCell: UITableViewCell, ReusableTableViewCell {
var configuration = UIBackgroundConfiguration.clear()
if state.isSelected || state.isHighlighted {
configuration.backgroundColor = Theme.tableCell2SelectedBackgroundColor
if traitCollection.userInterfaceIdiom == .pad {
if useSidebarAppearance {
configuration.cornerRadius = 24
}
} else {

View File

@@ -17,6 +17,16 @@ final class ChatListContainerView: UIView {
private var observation: NSKeyValueObservation?
private var _filterControl: ChatListFilterControl?
/// Set an extra padding on both sides of the table view.
/// This is used when chat list is displayed in split view controller's "sidebar".
var tableViewHorizontalInset: CGFloat = 0 {
didSet {
guard oldValue != tableViewHorizontalInset else { return }
tableViewHorizontalEdgeConstraints.forEach { $0.constant = tableViewHorizontalInset }
}
}
private var tableViewHorizontalEdgeConstraints: [NSLayoutConstraint] = []
var filterControl: ChatListFilterControl? {
_filterControl
}
@@ -29,7 +39,12 @@ final class ChatListContainerView: UIView {
super.init(frame: .zero)
addSubview(tableView)
tableView.autoPinEdgesToSuperviewEdges()
tableView.autoPinHeight(toHeightOf: self)
tableViewHorizontalEdgeConstraints = [
tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: tableViewHorizontalInset),
trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: tableViewHorizontalInset),
]
NSLayoutConstraint.activate(tableViewHorizontalEdgeConstraints)
let filterControl = ChatListFilterControl(container: self, scrollView: tableView)
_filterControl = filterControl

View File

@@ -299,16 +299,9 @@ extension ChatListViewController {
return
}
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: self
) { [weak self] _, threadSoftDeleteManager in
self?.showDeleteAllActionSheet(
threadSoftDeleteManager: threadSoftDeleteManager
)
}
}
let db = DependenciesBridge.shared.db
let threadSoftDeleteManager = DependenciesBridge.shared.threadSoftDeleteManager
private func showDeleteAllActionSheet(threadSoftDeleteManager: any ThreadSoftDeleteManager) {
/// We need to grab these now, since they'll be `nil`-ed out when we
/// show the modal spinner below.
let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? []
@@ -338,7 +331,7 @@ extension ChatListViewController {
// We want to protect this whole operation with a single write
// transaction, to ensure the contents of the threads don't
// change as we're deleting them.
SSKEnvironment.shared.databaseStorageRef.write { transaction in
db.write { transaction in
self.performOn(indexPaths: selectedIndexPaths) { threadViewModels in
threadSoftDeleteManager.softDelete(
threads: threadViewModels.map { $0.threadRecord },

View File

@@ -99,8 +99,8 @@ extension ChatListViewController {
)
NotificationCenter.default.addObserver(
self,
selector: #selector(backupDidRun),
name: .backupExportJobDidRun,
selector: #selector(lastBackupDetailsDidChange),
name: .lastBackupDetailsDidChange,
object: nil
)
NotificationCenter.default.addObserver(
@@ -157,7 +157,7 @@ extension ChatListViewController {
// MARK: -
@objc
private func backupDidRun(_ notification: NSNotification) {
private func lastBackupDetailsDidChange(_ notification: NSNotification) {
AssertIsOnMainThread()
updateBackupFailureAlertsWithSneakyTransaction()
}

View File

@@ -42,9 +42,10 @@ extension ChatListViewController: OWSNavigationChildController {
}
extension ChatListViewController: UISearchResultsUpdating {
public func updateSearchResults(for searchController: UISearchController) {
AssertIsOnMainThread()
guard isSearching else { return }
viewState.searchResultsController.searchText = searchText
}
}

View File

@@ -96,6 +96,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
updateExpirationReminderView()
updatePaymentReminderView()
updateUsernameReminderView()
updateTableViewPaddingIfNeeded()
observeNotifications()
DependenciesBridge.shared.db.read { tx in
self.viewState.backupDownloadProgressViewState.refetchDBState(tx: tx)
@@ -341,11 +342,13 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
// transition occurs. For iPhone, this moment is _during_ the
// transition. We reload in the right places accordingly.
if UIDevice.current.isIPad {
updateTableViewPaddingIfNeeded()
reloadTableDataAndResetCellContentCache()
}
coordinator.animate { context in
if !UIDevice.current.isIPad {
self.updateTableViewPaddingIfNeeded()
self.reloadTableDataAndResetCellContentCache()
}
@@ -718,6 +721,24 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
}
}
/// iOS 26+: checks if this VC is displayed in the collapsed split view controller
/// and updates `containerView.tableViewHorizontalInset` accordingly.
/// Does nothing on prior iOS versions.
private func updateTableViewPaddingIfNeeded() {
guard #available(iOS 26, *) else { return }
let useSidebarChatListCellAppearance: Bool
if let splitViewController = splitViewController, !splitViewController.isCollapsed {
useSidebarChatListCellAppearance = true
} else {
useSidebarChatListCellAppearance = false
}
// We need to add/remove horizotal padding around table view...
containerView.tableViewHorizontalInset = useSidebarChatListCellAppearance ? 16 : 0
// ... and tell ChatListCell to use rounded corners if there is non-zero padding.
tableDataSource.useSideBarChatListCellAppearance = useSidebarChatListCellAppearance
}
// MARK: UI Helpers
private var getStartedBanner: GetStartedBannerViewController?

View File

@@ -4,24 +4,21 @@
//
import BonMot
import Foundation
public import SignalServiceKit
import SignalServiceKit
import SignalUI
/* From BonMot 6.0.0: If you're targeting iOS 15 or higher, you may want to check out [AttributedString](https://developer.apple.com/documentation/foundation/attributedstring) instead.
If you're an existing user of BonMot using Xcode 13, you may want to add the following `typealias` somewhere in your project to avoid a conflict with `Foundation.StringStyle`: */
typealias StringStyle = BonMot.StringStyle
public protocol ConversationSearchViewDelegate: AnyObject {
protocol ConversationSearchViewDelegate: AnyObject {
func conversationSearchViewWillBeginDragging()
func conversationSearchDidSelectRow()
}
public class ConversationSearchViewController: UITableViewController {
class ConversationSearchViewController: OWSViewController {
// MARK: -
public weak var delegate: ConversationSearchViewDelegate?
weak var delegate: ConversationSearchViewDelegate?
private var hasEverAppeared = false
private var lastReloadDate: Date?
@@ -29,25 +26,6 @@ public class ConversationSearchViewController: UITableViewController {
private lazy var spoilerAnimationManager = SpoilerAnimationManager()
public var searchText = "" {
didSet {
guard searchText != oldValue else { return }
AssertIsOnMainThread()
// Use a slight delay to debounce updates.
refreshSearchResults()
}
}
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty {
didSet {
AssertIsOnMainThread()
updateSeparators()
}
}
private var lastSearchText: String?
enum SearchSection: Int {
case noResults
case contactThreads
@@ -58,28 +36,20 @@ public class ConversationSearchViewController: UITableViewController {
private var hasThemeChanged = false
class var matchSnippetStyle: StringStyle {
StringStyle(
.color(Theme.secondaryTextAndIconColor),
.xmlRules([
.style(FullTextSearchIndexer.matchTag, StringStyle(.font(UIFont.dynamicTypeSubheadline.semibold())))
])
)
// MARK: View Controller
override init() {
super.init()
}
// MARK: View Lifecycle
init() {
super.init(style: .grouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .Signal.background
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = .Signal.background
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
tableView.separatorColor = .clear
@@ -90,28 +60,27 @@ public class ConversationSearchViewController: UITableViewController {
tableView.register(ChatListCell.self, forCellReuseIdentifier: ChatListCell.reuseIdentifier)
tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: ContactTableViewCell.reuseIdentifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.autoPinHeight(toHeightOf: view)
tableViewHorizontalEdgeConstraints = [
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
]
NSLayoutConstraint.activate(tableViewHorizontalEdgeConstraints)
updateTableViewPaddingIfNeeded()
DependenciesBridge.shared.databaseChangeObserver.appendDatabaseChangeDelegate(self)
NotificationCenter.default.addObserver(self,
selector: #selector(themeDidChange),
name: .themeDidChange,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(blockListDidChange),
name: BlockingManager.blockListDidChange,
object: nil)
applyTheme()
updateSeparators()
}
private func reloadTableData() {
self.lastReloadDate = Date()
self.cellContentCache.clear()
self.tableView.reloadData()
}
public override func viewDidAppear(_ animated: Bool) {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard hasThemeChanged else {
@@ -119,16 +88,18 @@ public class ConversationSearchViewController: UITableViewController {
}
hasThemeChanged = false
applyTheme()
reloadTableData()
self.hasEverAppeared = true
}
@objc
internal func themeDidChange(notification: NSNotification) {
AssertIsOnMainThread()
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateTableViewPaddingIfNeeded()
}
override func themeDidChange() {
super.themeDidChange()
applyTheme()
reloadTableData()
hasThemeChanged = true
@@ -136,29 +107,151 @@ public class ConversationSearchViewController: UITableViewController {
@objc
private func blockListDidChange(_ notification: NSNotification) {
AssertIsOnMainThread()
refreshSearchResults()
}
private func applyTheme() {
AssertIsOnMainThread()
// MARK: Table View
self.view.backgroundColor = Theme.backgroundColor
self.tableView.backgroundColor = Theme.backgroundColor
private let tableView = UITableView(frame: .zero, style: .grouped)
/// Set to `true` when list is displayed in split view controller's "sidebar" on iOS 26 and later.
/// Setting this to `true` would add an extra padding on both sides of the table view.
/// This value is also passed down to table view cells that make their own layout choices based on the value.
private var useSidebarTableViewCellAppearance = false {
didSet {
guard oldValue != useSidebarTableViewCellAppearance else { return }
tableViewHorizontalEdgeConstraints.forEach {
$0.constant = useSidebarTableViewCellAppearance ? 16 : 0
}
tableView.reloadData()
}
}
private var tableViewHorizontalEdgeConstraints: [NSLayoutConstraint] = []
/// iOS 26+: checks if this VC is displayed in the collapsed split view controller and updates `useSidebarCallListCellAppearance` accordingly.
/// Does nothing on prior iOS versions.
private func updateTableViewPaddingIfNeeded() {
guard #available(iOS 26, *) else { return }
if let splitViewController = presentingViewController?.splitViewController, !splitViewController.isCollapsed {
useSidebarTableViewCellAppearance = true
} else {
useSidebarTableViewCellAppearance = false
}
}
private func reloadTableData() {
lastReloadDate = Date()
cellContentCache.clear()
tableView.reloadData()
}
private func updateSeparators() {
AssertIsOnMainThread()
self.tableView.separatorStyle = (searchResultSet.isEmpty
? UITableViewCell.SeparatorStyle.none
: UITableViewCell.SeparatorStyle.singleLine)
tableView.separatorStyle = searchResultSet.isEmpty ? .none : .singleLine
}
// MARK: UITableViewDelegate
// MARK: Search
public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
// Search is triggered with this is set externally by ChatListViewController.
var searchText = "" {
didSet {
guard searchText != oldValue else { return }
// Use a slight delay to debounce updates.
refreshSearchResults()
}
}
private var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty {
didSet {
updateSeparators()
}
}
private var lastSearchText: String?
private var refreshTimer: Timer?
private func refreshSearchResults() {
AssertIsOnMainThread()
guard !searchResultSet.isEmpty else {
// To avoid incorrectly showing the "no results" state,
// always search immediately if the current result set is empty.
refreshTimer?.invalidate()
refreshTimer = nil
updateSearchResults(searchText: searchText)
return
}
if refreshTimer != nil {
// Don't start a new refresh timer if there's already one active.
return
}
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.updateSearchResults(searchText: strongSelf.searchText)
strongSelf.refreshTimer = nil
}
}
private var currentSearchTask: Task<Void, Never>?
private func updateSearchResults(searchText: String) {
let searchText = searchText.stripped
let lastSearchText = self.lastSearchText
self.lastSearchText = searchText
if searchText != lastSearchText {
// The query has changed; perform a search.
} else if tableView.visibleCells.contains(where: { $0 is ChatListCell }) {
// The database may have been updated, and that'll lead to a duplicate
// query for the same search text. In that case, perform a search if
// there's a cell that needs to be updated.
} else {
// Nothing has changed, so don't perform a search.
return
}
currentSearchTask?.cancel()
currentSearchTask = Task {
let searchResultSet: HomeScreenSearchResultSet
do throws(CancellationError) {
searchResultSet = try await fetchSearchResults(searchText: searchText)
if Task.isCancelled {
throw CancellationError()
}
} catch {
return
}
self.searchResultSet = searchResultSet
self.reloadTableData()
}
}
private nonisolated func fetchSearchResults(searchText: String) async throws(CancellationError) -> HomeScreenSearchResultSet {
if searchText.isEmpty {
return .empty
}
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
return try databaseStorage.read { tx throws(CancellationError) in
return try FullTextSearcher.shared.searchForHomeScreen(searchText: searchText, tx: tx)
}
}
}
// MARK: -
extension ConversationSearchViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
owsFailDebug("unknown section selected.")
return nil
@@ -167,12 +260,13 @@ public class ConversationSearchViewController: UITableViewController {
switch searchSection {
case .noResults:
return nil
case .contactThreads, .groupThreads, .contacts, .messages:
return indexPath
}
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
@@ -228,35 +322,24 @@ public class ConversationSearchViewController: UITableViewController {
}
}
public override func tableView(
_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath
) {
AssertIsOnMainThread()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? ChatListCell else { return }
guard let cell = cell as? ChatListCell else {
return
}
cell.isCellVisible = true
}
public override func tableView(
_ tableView: UITableView,
didEndDisplaying cell: UITableViewCell,
forRowAt indexPath: IndexPath
) {
AssertIsOnMainThread()
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? ChatListCell else { return }
guard let cell = cell as? ChatListCell else {
return
}
cell.isCellVisible = false
}
}
// MARK: UITableViewDataSource
// MARK: -
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
extension ConversationSearchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let searchSection = SearchSection(rawValue: section) else {
owsFailDebug("unknown section: \(section)")
return 0
@@ -276,9 +359,7 @@ public class ConversationSearchViewController: UITableViewController {
}
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
AssertIsOnMainThread()
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
owsFailDebug("Invalid section: \(indexPath.section).")
return UITableView.automaticDimension
@@ -288,7 +369,7 @@ public class ConversationSearchViewController: UITableViewController {
case .noResults, .contacts:
return UITableView.automaticDimension
case .contactThreads, .groupThreads, .messages:
guard let configuration = self.cellConfiguration(searchSection: searchSection, row: indexPath.row) else {
guard let configuration = cellConfiguration(searchSection: searchSection, row: indexPath.row) else {
owsFailDebug("Missing configuration.")
return UITableView.automaticDimension
}
@@ -298,7 +379,6 @@ public class ConversationSearchViewController: UITableViewController {
}
private func cellContentToken(forConfiguration configuration: ChatListCell.Configuration, useCache: Bool = true) -> CLVCellContentToken {
AssertIsOnMainThread()
// If we have an existing CLVCellContentToken, use it.
// Cell measurement/arrangement is expensive.
@@ -314,8 +394,7 @@ public class ConversationSearchViewController: UITableViewController {
return cellContentToken
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
return UITableViewCell()
}
@@ -332,41 +411,50 @@ public class ConversationSearchViewController: UITableViewController {
return UITableViewCell()
}
OWSTableItem.configureCell(cell)
let searchText = self.searchResultSet.searchText
cell.configure(searchText: searchText)
cell.configure(searchText: searchResultSet.searchText)
return cell
case .contacts:
guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier) as? ContactTableViewCell else {
owsFailDebug("cell was unexpectedly nil")
return UITableViewCell()
}
guard let searchResult = self.searchResultSet.contactResults[safe: indexPath.row] else {
guard let searchResult = searchResultSet.contactResults[safe: indexPath.row] else {
owsFailDebug("searchResult was unexpectedly nil")
return UITableViewCell()
}
cell.configureWithSneakyTransaction(address: searchResult.recipientAddress, localUserDisplayMode: .noteToSelf)
return cell
case .contactThreads, .groupThreads, .messages:
guard let cell = tableView.dequeueReusableCell(withIdentifier: ChatListCell.reuseIdentifier) as? ChatListCell else {
owsFailDebug("cell was unexpectedly nil")
return UITableViewCell()
}
guard let configuration = self.cellConfiguration(searchSection: searchSection, row: indexPath.row) else {
guard let configuration = cellConfiguration(searchSection: searchSection, row: indexPath.row) else {
owsFailDebug("Missing configuration.")
return UITableViewCell()
}
let cellContentToken = cellContentToken(forConfiguration: configuration)
cell.configure(cellContentToken: cellContentToken, spoilerAnimationManager: spoilerAnimationManager)
cell.useSidebarAppearance = useSidebarTableViewCellAppearance
return cell
}
}
private func cellConfiguration(searchSection: SearchSection, row: Int) -> ChatListCell.Configuration? {
AssertIsOnMainThread()
private class var matchSnippetStyle: StringStyle {
StringStyle(
.color(.Signal.secondaryLabel),
.xmlRules([
.style(FullTextSearchIndexer.matchTag, StringStyle(.font(UIFont.dynamicTypeSubheadline.semibold())))
])
)
}
private func cellConfiguration(searchSection: SearchSection, row: Int) -> ChatListCell.Configuration? {
let lastReloadDate: Date? = {
guard self.hasEverAppeared else {
return nil
@@ -378,8 +466,9 @@ public class ConversationSearchViewController: UITableViewController {
case .noResults:
owsFailDebug("Invalid section.")
return nil
case .contactThreads:
guard let searchResult = self.searchResultSet.contactThreadResults[safe: row] else {
guard let searchResult = searchResultSet.contactThreadResults[safe: row] else {
owsFailDebug("searchResult was unexpectedly nil")
return nil
}
@@ -387,8 +476,9 @@ public class ConversationSearchViewController: UITableViewController {
threadViewModel: searchResult.threadViewModel,
lastReloadDate: lastReloadDate
)
case .groupThreads:
guard let searchResult = self.searchResultSet.groupThreadResults[safe: row] else {
guard let searchResult = searchResultSet.groupThreadResults[safe: row] else {
owsFailDebug("searchResult was unexpectedly nil")
return nil
}
@@ -404,11 +494,13 @@ public class ConversationSearchViewController: UITableViewController {
overrideSnippet: overrideSnippet,
overrideDate: nil
)
case .contacts:
owsFailDebug("Invalid section.")
return nil
case .messages:
guard let searchResult = self.searchResultSet.messageResults[safe: row] else {
guard let searchResult = searchResultSet.messageResults[safe: row] else {
owsFailDebug("searchResult was unexpectedly nil")
return nil
}
@@ -435,50 +527,7 @@ public class ConversationSearchViewController: UITableViewController {
}
}
public override func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
public override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
UIView()
}
public override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
.leastNonzeroMagnitude
}
public override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard nil != self.tableView(tableView, titleForHeaderInSection: section) else {
return .leastNonzeroMagnitude
}
return UITableView.automaticDimension
}
public override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let title = self.tableView(tableView, titleForHeaderInSection: section) else {
return UIView()
}
let textView = LinkingTextView()
textView.textColor = Theme.isDarkThemeEnabled ? UIColor.ows_gray05 : UIColor.ows_gray90
textView.font = UIFont.dynamicTypeHeadlineClamped
textView.text = title
let hInset = OWSTableViewController2.cellOuterInset(in: view)
var textContainerInset = UIEdgeInsets(
top: 14,
left: hInset,
bottom: 8,
right: hInset
)
textContainerInset.left += tableView.safeAreaInsets.left
textContainerInset.right += tableView.safeAreaInsets.right
textView.textContainerInset = textContainerInset
return textView
}
public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
private func titleForHeaderInSection(_ section: Int) -> String? {
guard let searchSection = SearchSection(rawValue: section) else {
owsFailDebug("unknown section: \(section)")
return nil
@@ -487,130 +536,104 @@ public class ConversationSearchViewController: UITableViewController {
switch searchSection {
case .noResults:
return nil
case .contactThreads:
if searchResultSet.contactThreadResults.count > 0 {
return OWSLocalizedString("SEARCH_SECTION_CONVERSATIONS", comment: "section header for search results that match existing 1:1 chats")
} else {
return nil
}
guard searchResultSet.contactThreadResults.count > 0 else { return nil }
return OWSLocalizedString(
"SEARCH_SECTION_CONVERSATIONS",
comment: "section header for search results that match existing 1:1 chats"
)
case .groupThreads:
if searchResultSet.groupThreadResults.count > 0 {
return OWSLocalizedString("SEARCH_SECTION_GROUPS", comment: "section header for search results that match existing groups")
} else {
return nil
}
guard searchResultSet.groupThreadResults.count > 0 else { return nil }
return OWSLocalizedString(
"SEARCH_SECTION_GROUPS",
comment: "section header for search results that match existing groups"
)
case .contacts:
if searchResultSet.contactResults.count > 0 {
return OWSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "section header for search results that match a contact who doesn't have an existing conversation")
} else {
return nil
}
guard searchResultSet.contactResults.count > 0 else { return nil }
return OWSLocalizedString(
"SEARCH_SECTION_CONTACTS",
comment: "section header for search results that match a contact who doesn't have an existing conversation"
)
case .messages:
if searchResultSet.messageResults.count > 0 {
return OWSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "section header for search results that match a message in a conversation")
} else {
return nil
}
guard searchResultSet.messageResults.count > 0 else { return nil }
return OWSLocalizedString(
"SEARCH_SECTION_MESSAGES",
comment: "section header for search results that match a message in a conversation"
)
}
}
public override func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard nil != titleForHeaderInSection(section) else {
return .leastNonzeroMagnitude
}
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let title = titleForHeaderInSection(section) else {
return UIView()
}
let label = UILabel()
label.textColor = .Signal.label
label.font = .dynamicTypeHeadline
label.text = title
label.translatesAutoresizingMaskIntoConstraints = false
let headerView = UITableViewHeaderFooterView(reuseIdentifier: nil)
headerView.directionalLayoutMargins.top = 14
headerView.directionalLayoutMargins.bottom = 8
headerView.contentView.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: headerView.contentView.layoutMarginsGuide.topAnchor),
label.leadingAnchor.constraint(equalTo: headerView.contentView.layoutMarginsGuide.leadingAnchor),
label.trailingAnchor.constraint(equalTo: headerView.contentView.layoutMarginsGuide.trailingAnchor),
label.bottomAnchor.constraint(equalTo: headerView.contentView.layoutMarginsGuide.bottomAnchor),
])
return headerView
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
// Without returning a footer with a non-zero height, Grouped
// table view will use a default spacing between sections. We
// do not want that spacing so we use the smallest possible height.
return .leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
UIView()
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return trailingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath))
}
public override func tableView(_ tableView: UITableView,
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return leadingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath))
}
// MARK: Update Search Results
var refreshTimer: Timer?
private func refreshSearchResults() {
AssertIsOnMainThread()
guard !searchResultSet.isEmpty else {
// To avoid incorrectly showing the "no results" state,
// always search immediately if the current result set is empty.
refreshTimer?.invalidate()
refreshTimer = nil
updateSearchResults(searchText: searchText)
return
}
if refreshTimer != nil {
// Don't start a new refresh timer if there's already one active.
return
}
refreshTimer?.invalidate()
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.updateSearchResults(searchText: strongSelf.searchText)
strongSelf.refreshTimer = nil
}
}
private var currentSearchTask: Task<Void, Never>?
private func updateSearchResults(searchText: String) {
let searchText = searchText.stripped
let lastSearchText = self.lastSearchText
self.lastSearchText = searchText
if searchText != lastSearchText {
// The query has changed; perform a search.
} else if tableView.visibleCells.contains(where: { $0 is ChatListCell }) {
// The database may have been updated, and that'll lead to a duplicate
// query for the same search text. In that case, perform a search if
// there's a cell that needs to be updated.
} else {
// Nothing has changed, so don't perform a search.
return
}
self.currentSearchTask?.cancel()
self.currentSearchTask = Task {
let searchResultSet: HomeScreenSearchResultSet
do throws(CancellationError) {
searchResultSet = try await fetchSearchResults(searchText: searchText)
if Task.isCancelled {
throw CancellationError()
}
} catch {
return
}
self.searchResultSet = searchResultSet
self.reloadTableData()
}
}
private nonisolated func fetchSearchResults(searchText: String) async throws(CancellationError) -> HomeScreenSearchResultSet {
if searchText.isEmpty {
return .empty
}
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
return try databaseStorage.read { tx throws(CancellationError) in
return try FullTextSearcher.shared.searchForHomeScreen(searchText: searchText, tx: tx)
}
}
// MARK: -
private func getThreadViewModelFor(indexPath: IndexPath) -> ThreadViewModel? {
if let searchSection = SearchSection(rawValue: indexPath.section) {
if searchSection == .contactThreads {
return searchResultSet.contactThreadResults[indexPath.row].threadViewModel
} else if searchSection == .groupThreads {
return searchResultSet.groupThreadResults[indexPath.row].threadViewModel
}
guard let searchSection = SearchSection(rawValue: indexPath.section) else { return nil }
if searchSection == .contactThreads {
return searchResultSet.contactThreadResults[indexPath.row].threadViewModel
} else if searchSection == .groupThreads {
return searchResultSet.groupThreadResults[indexPath.row].threadViewModel
}
return nil
}
@@ -619,7 +642,8 @@ public class ConversationSearchViewController: UITableViewController {
// MARK: - UIScrollViewDelegate
extension ConversationSearchViewController {
public override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
delegate?.conversationSearchViewWillBeginDragging()
}
}
@@ -627,43 +651,51 @@ extension ConversationSearchViewController {
// MARK: -
class EmptySearchResultCell: UITableViewCell {
static let reuseIdentifier = "EmptySearchResultCell"
let messageLabel: UILabel
let activityIndicator = UIActivityIndicatorView(style: .large)
private let messageLabel = UILabel()
private let activityIndicator = UIActivityIndicatorView(style: .large)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
self.messageLabel = UILabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
selectionStyle = .none
automaticallyUpdatesBackgroundConfiguration = false
messageLabel.textAlignment = .center
messageLabel.numberOfLines = 3
contentView.addSubview(messageLabel)
messageLabel.autoSetDimension(.height, toSize: 150)
messageLabel.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
messageLabel.autoVCenterInSuperview()
messageLabel.autoHCenterInSuperview()
messageLabel.setContentHuggingHigh()
messageLabel.setCompressionResistanceHigh()
messageLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(activityIndicator)
activityIndicator.autoCenterInSuperview()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageLabel.heightAnchor.constraint(equalToConstant: 150),
messageLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.layoutMarginsGuide.topAnchor),
messageLabel.centerYAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerYAnchor),
messageLabel.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.layoutMarginsGuide.leadingAnchor),
messageLabel.centerXAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerXAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerYAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(searchText: String) {
override func updateConfiguration(using state: UICellConfigurationState) {
var configuration = UIBackgroundConfiguration.clear()
configuration.backgroundColor = .Signal.background
backgroundConfiguration = configuration
}
func configure(searchText: String) {
if searchText.isEmpty {
activityIndicator.color = Theme.primaryIconColor
activityIndicator.isHidden = false
@@ -681,7 +713,7 @@ class EmptySearchResultCell: UITableViewCell {
)
messageLabel.text = String(format: format, searchText)
messageLabel.textColor = Theme.primaryTextColor
messageLabel.textColor = .Signal.label
messageLabel.font = UIFont.dynamicTypeBody
}
}

View File

@@ -97,23 +97,11 @@ extension ThreadSwipeHandler where Self: UIViewController {
image: "trash-fill",
title: CommonStrings.deleteButton
) { [weak self] completion in
guard let self else {
completion(false)
return
}
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
fromViewController: self
) { [weak self] _, threadSoftDeleteManager in
guard let self else { return }
self.deleteThreadWithConfirmation(
threadViewModel: threadViewModel,
threadSoftDeleteManager: threadSoftDeleteManager,
closeConversationBlock: closeConversationBlock
)
completion(false)
}
self?.deleteThreadWithConfirmation(
threadViewModel: threadViewModel,
closeConversationBlock: closeConversationBlock
)
completion(false)
}
let archiveAction = ContextualActionBuilder.makeContextualAction(
@@ -144,10 +132,11 @@ extension ThreadSwipeHandler where Self: UIViewController {
fileprivate func deleteThreadWithConfirmation(
threadViewModel: ThreadViewModel,
threadSoftDeleteManager: any ThreadSoftDeleteManager,
closeConversationBlock: (() -> Void)?
) {
AssertIsOnMainThread()
let db = DependenciesBridge.shared.db
let threadSoftDeleteManager = DependenciesBridge.shared.threadSoftDeleteManager
let alert = ActionSheetController(title: OWSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE",
comment: "Title for the 'conversation delete confirmation' alert."),
@@ -166,7 +155,7 @@ extension ThreadSwipeHandler where Self: UIViewController {
) { [weak self] modal in
guard let self else { return }
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
await db.awaitableWrite { tx in
threadSoftDeleteManager.softDelete(
threads: [threadViewModel.threadRecord],
sendDeleteForMeSyncMessage: true,

View File

@@ -78,6 +78,7 @@ class ConversationSplitViewController: UISplitViewController, ConversationSplit
delegate = self
preferredDisplayMode = .oneBesideSecondary
presentsWithGesture = false
preferredPrimaryColumnWidthFraction = 0.42
NotificationCenter.default.addObserver(self, selector: #selector(applyTheme), name: .themeDidChange, object: nil)
NotificationCenter.default.addObserver(

View File

@@ -183,28 +183,33 @@ class StoryPageViewController: UIPageViewController {
private struct MuteStatus {
let isMuted: Bool
let shouldInitialRingerStateSetMuteState: Bool
let appForegroundTime: Date
}
// Once unmuted, stays that way until the app is backgrounded.
private static var muteStatus: MuteStatus?
private static var muteStatus: MuteStatus? {
didSet {
if muteStatus != nil, muteStatusObserver == nil {
muteStatusObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: nil,
using: { _ in muteStatus = nil },
)
}
}
}
private static var muteStatusObserver: (any NSObjectProtocol)?
private var isMuted: Bool {
get {
let appForegroundTime = CurrentAppContext().appForegroundTime
if
let muteStatus = Self.muteStatus,
// Mute status is only valid for one foregroundind session,
// dedupe by timestamp.
muteStatus.appForegroundTime == appForegroundTime
{
if let muteStatus = Self.muteStatus {
return muteStatus.isMuted
}
// Start muted, but let the ringer change the setting.
let muteStatus = MuteStatus(
isMuted: true,
shouldInitialRingerStateSetMuteState: true,
appForegroundTime: CurrentAppContext().appForegroundTime
)
Self.muteStatus = muteStatus
return muteStatus.isMuted
@@ -213,7 +218,6 @@ class StoryPageViewController: UIPageViewController {
Self.muteStatus = MuteStatus(
isMuted: newValue,
shouldInitialRingerStateSetMuteState: false,
appForegroundTime: CurrentAppContext().appForegroundTime
)
viewControllers?.forEach {
($0 as? StoryContextViewController)?.updateMuteState()

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