mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Compare commits
103 Commits
32aa4a0689
...
9ea0a9c0f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea0a9c0f4 | ||
|
|
1490787671 | ||
|
|
da818b8e0d | ||
|
|
cfc4533ad0 | ||
|
|
d0147ed416 | ||
|
|
3af9aa593d | ||
|
|
c0f3f876df | ||
|
|
f8dca3c504 | ||
|
|
276b1d0c8f | ||
|
|
69cb1b2840 | ||
|
|
308a438b2c | ||
|
|
8bdf99ccc1 | ||
|
|
d392b640d8 | ||
|
|
9c15eb5a6b | ||
|
|
87243e9ea3 | ||
|
|
77b195e7cd | ||
|
|
b2b202bc71 | ||
|
|
dd556d0417 | ||
|
|
940312cae5 | ||
|
|
7ff19e7bba | ||
|
|
1e5f1890c7 | ||
|
|
f02d7b03c6 | ||
|
|
75f6e77aa8 | ||
|
|
d6d35606a7 | ||
|
|
ec0cd23b53 | ||
|
|
e234f56623 | ||
|
|
58d781f1bf | ||
|
|
ac7243c25d | ||
|
|
ab616d0cef | ||
|
|
0f0e7de2df | ||
|
|
d727d0091c | ||
|
|
58207219e5 | ||
|
|
1385a10b9a | ||
|
|
2e50d797ba | ||
|
|
e20ad23c7b | ||
|
|
c0a267b030 | ||
|
|
4d0a45136d | ||
|
|
47afe79d2f | ||
|
|
fb6c6863b1 | ||
|
|
d4141a3cfb | ||
|
|
7c3a5c98a7 | ||
|
|
8c3fef27e1 | ||
|
|
2b07e73614 | ||
|
|
d7bf01bec9 | ||
|
|
2e1587acf7 | ||
|
|
1cde05f9b1 | ||
|
|
bb6ad0eb95 | ||
|
|
006267443e | ||
|
|
c2500f6dc9 | ||
|
|
2e348030e9 | ||
|
|
6e1186fc19 | ||
|
|
6d9f97ec69 | ||
|
|
bb82459818 | ||
|
|
d639897f31 | ||
|
|
9f00ab0b93 | ||
|
|
68adce5056 | ||
|
|
5acdd04d9e | ||
|
|
0d455ecbbf | ||
|
|
1ccfb3966a | ||
|
|
cf3a2fb3a4 | ||
|
|
c38b968d9f | ||
|
|
ab01da1436 | ||
|
|
1152c38d50 | ||
|
|
9c217d29f1 | ||
|
|
34e8f4633d | ||
|
|
061f0f0674 | ||
|
|
38c64bd997 | ||
|
|
4991bb753e | ||
|
|
2fc6fb8994 | ||
|
|
95c882099f | ||
|
|
fde498d01e | ||
|
|
83688f3e62 | ||
|
|
993d630c5c | ||
|
|
88228a5502 | ||
|
|
d5fa2dd912 | ||
|
|
c2ca54fdca | ||
|
|
2606068db4 | ||
|
|
fb019581e6 | ||
|
|
1e416c46f4 | ||
|
|
5868ce3518 | ||
|
|
444edd9150 | ||
|
|
1716016d28 | ||
|
|
5f99c76b01 | ||
|
|
8581b1f541 | ||
|
|
19bdd8cd05 | ||
|
|
816fe08c43 | ||
|
|
9efc1cea74 | ||
|
|
ea3e1474af | ||
|
|
67d0b5b946 | ||
|
|
79469c571c | ||
|
|
2821affb49 | ||
|
|
3582fde3a1 | ||
|
|
3af22e8252 | ||
|
|
40d9577d7c | ||
|
|
ec322816a3 | ||
|
|
b2aab9de7e | ||
|
|
27f6dfa633 | ||
|
|
e84013727a | ||
|
|
faf4618355 | ||
|
|
f287ee8cfb | ||
|
|
333f0776eb | ||
|
|
9aa21c694d | ||
|
|
36a4a1a101 |
@@ -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 |
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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."
|
||||
)),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ public class CVCell: UICollectionViewCell, CVRootComponentHost {
|
||||
private func applyLastLayoutAttributes() {
|
||||
|
||||
guard let layoutAttributes = self.lastLayoutAttributes else {
|
||||
Logger.verbose("Missing layoutAttributes.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -261,7 +261,6 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
|
||||
}
|
||||
|
||||
self.updateLastKnownDistanceFromBottom()
|
||||
self.updateInputToolbarLayout()
|
||||
self.showMessageRequestDialogIfRequired()
|
||||
self.configureScrollDownButtons()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -184,7 +184,6 @@ extension ConversationViewController {
|
||||
}
|
||||
|
||||
navigationItem.rightBarButtonItems = barButtons
|
||||
showGroupCallTooltipIfNecessary()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
BIN
Signal/Symbols.xcassets/person/person-x-light.pdf
Normal file
BIN
Signal/Symbols.xcassets/person/person-x-light.pdf
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "forward-fill.pdf",
|
||||
"filename" : "pin-slash.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
Signal/Symbols.xcassets/pin/pin-slash.imageset/pin-slash.pdf
vendored
Normal file
BIN
Signal/Symbols.xcassets/pin/pin-slash.imageset/pin-slash.pdf
vendored
Normal file
Binary file not shown.
15
Signal/Symbols.xcassets/pin/pin.imageset/Contents.json
vendored
Normal file
15
Signal/Symbols.xcassets/pin/pin.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "pin.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Symbols.xcassets/pin/pin.imageset/pin.pdf
vendored
Normal file
BIN
Signal/Symbols.xcassets/pin/pin.imageset/pin.pdf
vendored
Normal file
Binary file not shown.
15
Signal/Symbols.xcassets/polls/pollStop-light.imageset/Contents.json
vendored
Normal file
15
Signal/Symbols.xcassets/polls/pollStop-light.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "pollStop-light.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
93
Signal/Symbols.xcassets/polls/pollStop-light.imageset/pollStop-light.pdf
vendored
Normal file
93
Signal/Symbols.xcassets/polls/pollStop-light.imageset/pollStop-light.pdf
vendored
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -359,7 +359,7 @@ extension LinkedDevicesViewModel: LinkDeviceViewControllerDelegate {
|
||||
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
|
||||
ExperienceUpgradeManager.clearExperienceUpgrade(
|
||||
.newLinkedDeviceNotification,
|
||||
transaction: SDSDB.shimOnlyBridge(tx)
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user