Fixed a few bugs, resolved a number of TODOs and deleted more unused code
Fixed a couple of bugs with search term highlighting (updated the logic to make the highlighted content follow similar logic to what terms would have actually matched) Fixed a bug where info messages in search results weren't rendering correctly Shifted some duplicate query code for global search into variables Fixed a small bug where sending attachments could incorrectly result in the mentions UI being visible Fixed a bug where quote content was appearing incorrectly Consolidated the ShareExtension Item and the ConversationCell.ViewModel into one type (with a more-limited query) to remove duplicate code Added back a missing asset (deleted a long time ago)
This commit is contained in:
parent
49dd341b6d
commit
c500d4c6ca
1
Podfile
1
Podfile
|
@ -58,6 +58,7 @@ abstract_target 'GlobalDependencies' do
|
|||
pod 'Reachability'
|
||||
pod 'SAMKeychain'
|
||||
pod 'SwiftProtobuf', '~> 1.5.0'
|
||||
pod 'DifferenceKit'
|
||||
end
|
||||
|
||||
target 'SessionUtilitiesKit' do
|
||||
|
|
|
@ -219,6 +219,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: bd0e75b0b6e37b30d8414efed2a5a98635e1a1a6
|
||||
PODFILE CHECKSUM: 9715c163fab54d487be0c32357d6d1729aa96a7b
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; };
|
||||
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; };
|
||||
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0701F8678AA0066283D /* ConversationViewItem.m */; };
|
||||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; };
|
||||
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; };
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
|
||||
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */; };
|
||||
|
@ -257,13 +256,10 @@
|
|||
C300A5F22554B09800555489 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5F12554B09800555489 /* MessageSender.swift */; };
|
||||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; };
|
||||
C300A60D2554B31900555489 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; };
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */ = {isa = PBXBuildFile; fileRef = C300A6312554B6D100555489 /* NSDate+Timestamp.mm */; };
|
||||
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */ = {isa = PBXBuildFile; fileRef = C300A6302554B68200555489 /* NSDate+Timestamp.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C302093D25DCBF07001F572D /* MentionSelectionView.swift */; };
|
||||
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C59247F214E001123EF /* UIView+Glow.swift */; };
|
||||
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; };
|
||||
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
|
||||
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
|
||||
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */; };
|
||||
C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
||||
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; };
|
||||
|
@ -286,12 +282,9 @@
|
|||
C32C5A88256DBCF9003C73A2 /* MessageReceiver+Handling.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+Handling.swift */; };
|
||||
C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */; };
|
||||
C32C5B51256DC219003C73A2 /* NSNotificationCenter+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */; };
|
||||
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; };
|
||||
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */; };
|
||||
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */; };
|
||||
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */; };
|
||||
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */; };
|
||||
C32C5D19256DD493003C73A2 /* LinkPreviewDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* LinkPreviewDraft.swift */; };
|
||||
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
|
||||
|
@ -341,7 +334,7 @@
|
|||
C331FFE92558FB0000070591 /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B82394911B00BA5194 /* Separator.swift */; };
|
||||
C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D44A247E1D9200DB3608 /* PathStatusView.swift */; };
|
||||
C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F8244809150011121A /* PNOptionView.swift */; };
|
||||
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* ConversationCell.swift */; };
|
||||
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */; };
|
||||
C33FD4E9255A149100E217F9 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
||||
C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -415,8 +408,6 @@
|
|||
C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */; };
|
||||
C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
|
||||
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF223255B6D5D007E1867 /* AttachmentSharing.m */; };
|
||||
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF225255B6D5D007E1867 /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; };
|
||||
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */; };
|
||||
C38EF243255B6D67007E1867 /* UIViewController+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF236255B6D65007E1867 /* UIViewController+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -475,7 +466,6 @@
|
|||
C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF382255B6DD1007E1867 /* AttachmentPrepViewController.swift */; };
|
||||
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF383255B6DD1007E1867 /* ApprovalRailCellView.swift */; };
|
||||
C38EF38D255B6DD2007E1867 /* AttachmentCaptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF384255B6DD2007E1867 /* AttachmentCaptionViewController.swift */; };
|
||||
C38EF39B255B6DDA007E1867 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF397255B6DD9007E1867 /* ThreadViewModel.swift */; };
|
||||
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3A8255B6DE4007E1867 /* ImageEditorTextViewController.swift */; };
|
||||
C38EF3B9255B6DE7007E1867 /* ImageEditorPinchGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3A9255B6DE4007E1867 /* ImageEditorPinchGestureRecognizer.swift */; };
|
||||
C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3AA255B6DE4007E1867 /* ImageEditorItem.swift */; };
|
||||
|
@ -534,7 +524,6 @@
|
|||
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; };
|
||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; };
|
||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */; };
|
||||
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */; };
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; };
|
||||
C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -591,7 +580,7 @@
|
|||
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAEF255A580500E217F9 /* NSData+Image.m */; };
|
||||
C3D9E4FD256778E30040E4F3 /* NSData+Image.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB29255A580A00E217F9 /* NSData+Image.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3D9E50E25677A510040E4F3 /* DataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB54255A580D00E217F9 /* DataSource.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */; };
|
||||
C3D9E52725677DF20040E4F3 /* ThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF1255A580500E217F9 /* ThumbnailService.swift */; };
|
||||
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */; };
|
||||
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
||||
C3DB6695260AC923001EFC55 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */; };
|
||||
|
@ -678,7 +667,7 @@
|
|||
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
|
||||
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; };
|
||||
FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; };
|
||||
FD4B200C283367410034334B /* ConversationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200B283367410034334B /* ConversationCellViewModel.swift */; };
|
||||
FD3E0C84283B5835002A425C /* ConversationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* ConversationCellViewModel.swift */; };
|
||||
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
|
||||
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
|
||||
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
|
||||
|
@ -687,7 +676,7 @@
|
|||
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; };
|
||||
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
|
||||
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
|
||||
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localized.swift */; };
|
||||
FD705A8E278CE29800F16121 /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Utilities.swift */; };
|
||||
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; };
|
||||
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
|
||||
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
|
||||
|
@ -952,7 +941,6 @@
|
|||
34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyDownloader.swift; sourceTree = "<group>"; };
|
||||
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewItem.h; sourceTree = "<group>"; };
|
||||
34D1F0701F8678AA0066283D /* ConversationViewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItem.m; sourceTree = "<group>"; };
|
||||
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = "<group>"; };
|
||||
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = "<group>"; };
|
||||
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = "<group>"; };
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
|
||||
|
@ -1173,7 +1161,7 @@
|
|||
B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = "<group>"; };
|
||||
B8BB82A4238F627000BA5194 /* HomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeVC.swift; sourceTree = "<group>"; };
|
||||
B8BB82A8238F62FB00BA5194 /* Gradients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gradients.swift; sourceTree = "<group>"; };
|
||||
B8BB82AA238F669C00BA5194 /* ConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; };
|
||||
B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullConversationCell.swift; sourceTree = "<group>"; };
|
||||
B8BB82B02390C37000BA5194 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
|
||||
B8BB82B423947F2D00BA5194 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
|
||||
B8BB82B82394911B00BA5194 /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = "<group>"; };
|
||||
|
@ -1192,10 +1180,7 @@
|
|||
B8D0A26825E4A2C200C1835E /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
|
||||
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = "<group>"; };
|
||||
B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = "<group>"; };
|
||||
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Jobs.swift"; sourceTree = "<group>"; };
|
||||
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OpenGroups.swift"; sourceTree = "<group>"; };
|
||||
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Messaging.swift"; sourceTree = "<group>"; };
|
||||
B8EB20E6263F7E4B00773E52 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = "<group>"; };
|
||||
B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1218,14 +1203,11 @@
|
|||
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationTimerUpdate.swift; sourceTree = "<group>"; };
|
||||
C300A5F12554B09800555489 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = "<group>"; };
|
||||
C300A5FB2554B0A000555489 /* MessageReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiver.swift; sourceTree = "<group>"; };
|
||||
C300A6302554B68200555489 /* NSDate+Timestamp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDate+Timestamp.h"; sourceTree = "<group>"; };
|
||||
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSDate+Timestamp.mm"; sourceTree = "<group>"; };
|
||||
C302093D25DCBF07001F572D /* MentionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSelectionView.swift; sourceTree = "<group>"; };
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Glow.swift"; sourceTree = "<group>"; };
|
||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
|
||||
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
|
||||
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
|
||||
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = "<group>"; };
|
||||
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
|
||||
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1270,7 +1252,7 @@
|
|||
C33FDAE0255A580400E217F9 /* ByteParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ByteParser.m; sourceTree = "<group>"; };
|
||||
C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupFragment.h; sourceTree = "<group>"; };
|
||||
C33FDAEF255A580500E217F9 /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = "<group>"; };
|
||||
C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSThumbnailService.swift; sourceTree = "<group>"; };
|
||||
C33FDAF1255A580500E217F9 /* ThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailService.swift; sourceTree = "<group>"; };
|
||||
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = "<group>"; };
|
||||
C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIMETypeUtil.h; sourceTree = "<group>"; };
|
||||
C33FDAFD255A580600E217F9 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||
|
@ -1370,9 +1352,7 @@
|
|||
C37F5402255BA9ED002AEA92 /* Environment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Environment.m; sourceTree = "<group>"; };
|
||||
C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AttachmentSharing.m; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF224255B6D5D007E1867 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignalAttachment.swift; path = "SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AttachmentSharing.h; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.h"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewDelegate.swift; path = SignalUtilitiesKit/Utilities/ShareViewDelegate.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSVideoPlayer.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF236255B6D65007E1867 /* UIViewController+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+OWS.h"; path = "SignalUtilitiesKit/Utilities/UIViewController+OWS.h"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1447,7 +1427,6 @@
|
|||
C38EF382255B6DD1007E1867 /* AttachmentPrepViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AttachmentPrepViewController.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF383255B6DD1007E1867 /* ApprovalRailCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ApprovalRailCellView.swift; path = "SignalUtilitiesKit/Shared Views/ApprovalRailCellView.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF384255B6DD2007E1867 /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AttachmentCaptionViewController.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentCaptionViewController.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF397255B6DD9007E1867 /* ThreadViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThreadViewModel.swift; path = SignalUtilitiesKit/Messaging/ThreadViewModel.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3A8255B6DE4007E1867 /* ImageEditorTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageEditorTextViewController.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3A9255B6DE4007E1867 /* ImageEditorPinchGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageEditorPinchGestureRecognizer.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPinchGestureRecognizer.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3AA255B6DE4007E1867 /* ImageEditorItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageEditorItem.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorItem.swift"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1508,7 +1487,6 @@
|
|||
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
C3BBE07F2554CDD70050F1E3 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
|
||||
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = "<group>"; };
|
||||
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = "<group>"; };
|
||||
|
@ -1662,7 +1640,7 @@
|
|||
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
|
||||
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = "<group>"; };
|
||||
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
|
||||
FD4B200B283367410034334B /* ConversationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCellViewModel.swift; sourceTree = "<group>"; };
|
||||
FD3E0C83283B5835002A425C /* ConversationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCellViewModel.swift; sourceTree = "<group>"; };
|
||||
FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = "<group>"; };
|
||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1671,7 +1649,7 @@
|
|||
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = "<group>"; };
|
||||
FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
|
||||
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
FD705A8D278CE29800F16121 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = "<group>"; };
|
||||
FD705A8D278CE29800F16121 /* String+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1942,11 +1920,9 @@
|
|||
4C586924224FAB83003FD070 /* AVAudioSession+OWS.h */,
|
||||
4C586925224FAB83003FD070 /* AVAudioSession+OWS.m */,
|
||||
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
|
||||
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */,
|
||||
B8544E3223D50E4900299F14 /* SNAppearance.swift */,
|
||||
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */,
|
||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
|
||||
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */,
|
||||
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */,
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
|
||||
B886B4A82398BA1500211ABE /* QRCode.swift */,
|
||||
|
@ -2248,8 +2224,6 @@
|
|||
C33FDAFD255A580600E217F9 /* LRUCache.swift */,
|
||||
C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */,
|
||||
C33FDAB8255A580100E217F9 /* NSArray+Functional.m */,
|
||||
C300A6302554B68200555489 /* NSDate+Timestamp.h */,
|
||||
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */,
|
||||
C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */,
|
||||
C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */,
|
||||
C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */,
|
||||
|
@ -2265,7 +2239,7 @@
|
|||
C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */,
|
||||
C33FDB3F255A580C00E217F9 /* String+SSK.swift */,
|
||||
C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */,
|
||||
FD705A8D278CE29800F16121 /* String+Localized.swift */,
|
||||
FD705A8D278CE29800F16121 /* String+Utilities.swift */,
|
||||
C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */,
|
||||
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */,
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */,
|
||||
|
@ -2301,7 +2275,7 @@
|
|||
34330AA21E79686200DF2FB9 /* OWSProgressView.m */,
|
||||
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
|
||||
C354E75923FE2A7600CE22E3 /* BaseVC.swift */,
|
||||
B8BB82AA238F669C00BA5194 /* ConversationCell.swift */,
|
||||
B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */,
|
||||
4542DF53208D40AC007B4E76 /* LoadingViewController.swift */,
|
||||
340FC888204DAC8C007AEB0F /* OWSQRCodeScanningViewController.h */,
|
||||
340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */,
|
||||
|
@ -2478,9 +2452,6 @@
|
|||
C33FDAFE255A580600E217F9 /* OWSStorage.h */,
|
||||
C33FDAB1255A580000E217F9 /* OWSStorage.m */,
|
||||
C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */,
|
||||
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */,
|
||||
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */,
|
||||
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */,
|
||||
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */,
|
||||
C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */,
|
||||
C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */,
|
||||
|
@ -2741,8 +2712,6 @@
|
|||
children = (
|
||||
C379DCEA2567334F0002D4EB /* Attachment Approval */,
|
||||
C379DCE9256733390002D4EB /* Image Editing */,
|
||||
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */,
|
||||
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */,
|
||||
C38EF358255B6DCC007E1867 /* MediaMessageView.swift */,
|
||||
C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */,
|
||||
C38EF3B5255B6DE6007E1867 /* OWSViewController+ImageEditor.swift */,
|
||||
|
@ -2874,7 +2843,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */,
|
||||
C38EF397255B6DD9007E1867 /* ThreadViewModel.swift */,
|
||||
C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */,
|
||||
);
|
||||
path = Messaging;
|
||||
|
@ -2938,7 +2906,6 @@
|
|||
FD09797327FAB3E200936362 /* ProfileManager.swift */,
|
||||
FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */,
|
||||
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */,
|
||||
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */,
|
||||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
|
||||
FDF0B7542807C4BB004C14C5 /* SSKEnvironment.swift */,
|
||||
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
|
||||
|
@ -3030,6 +2997,7 @@
|
|||
C352A2F325574B3300338F3E /* Jobs */,
|
||||
C3A7215C2558C0AC0043A11F /* File Server */,
|
||||
C3A721332558BDDF0043A11F /* Open Groups */,
|
||||
FD3E0C82283B581F002A425C /* Shared Models */,
|
||||
C3BBE0B32554F0D30050F1E3 /* Utilities */,
|
||||
);
|
||||
path = SessionMessagingKit;
|
||||
|
@ -3156,7 +3124,7 @@
|
|||
C3D9E3B52567685D0040E4F3 /* Attachments */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */,
|
||||
C33FDAF1255A580500E217F9 /* ThumbnailService.swift */,
|
||||
C38EF224255B6D5D007E1867 /* SignalAttachment.swift */,
|
||||
);
|
||||
path = Attachments;
|
||||
|
@ -3471,10 +3439,17 @@
|
|||
path = LegacyDatabase;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD3E0C82283B581F002A425C /* Shared Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD3E0C83283B5835002A425C /* ConversationCellViewModel.swift */,
|
||||
);
|
||||
path = "Shared Models";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD4B200A283367350034334B /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD4B200B283367410034334B /* ConversationCellViewModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3569,7 +3544,6 @@
|
|||
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */,
|
||||
FDA8EB0A280E9103002B68E5 /* AppSetup.h in Headers */,
|
||||
C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */,
|
||||
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */,
|
||||
C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */,
|
||||
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */,
|
||||
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */,
|
||||
|
@ -3618,7 +3592,6 @@
|
|||
C3D9E50E25677A510040E4F3 /* DataSource.h in Headers */,
|
||||
B8856DF8256F1633001CE70E /* NSString+SSK.h in Headers */,
|
||||
C3D9E4FD256778E30040E4F3 /* NSData+Image.h in Headers */,
|
||||
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */,
|
||||
C3D9E4E3256778720040E4F3 /* UIImage+OWS.h in Headers */,
|
||||
B8856E1A256F1700001CE70E /* OWSMath.h in Headers */,
|
||||
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
|
||||
|
@ -4338,7 +4311,6 @@
|
|||
C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */,
|
||||
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */,
|
||||
C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */,
|
||||
C38EF39B255B6DDA007E1867 /* ThreadViewModel.swift in Sources */,
|
||||
FDA8EB09280E90FB002B68E5 /* AppSetup.m in Sources */,
|
||||
C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */,
|
||||
C38EF273255B6D7A007E1867 /* OWSDatabaseMigrationRunner.m in Sources */,
|
||||
|
@ -4403,7 +4375,6 @@
|
|||
C38EF3BF255B6DE7007E1867 /* ImageEditorView.swift in Sources */,
|
||||
C38EF365255B6DCC007E1867 /* OWSTableViewController.m in Sources */,
|
||||
C38EF36B255B6DCC007E1867 /* ScreenLockViewController.m in Sources */,
|
||||
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */,
|
||||
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */,
|
||||
C38EF30E255B6DBF007E1867 /* FullTextSearcher.swift in Sources */,
|
||||
C38EF3FA255B6DF7007E1867 /* DirectionalPanGestureRecognizer.swift in Sources */,
|
||||
|
@ -4565,12 +4536,11 @@
|
|||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
|
||||
C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */,
|
||||
FD705A8E278CE29800F16121 /* String+Localized.swift in Sources */,
|
||||
FD705A8E278CE29800F16121 /* String+Utilities.swift in Sources */,
|
||||
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */,
|
||||
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
|
||||
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */,
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -4597,18 +4567,16 @@
|
|||
C352A31325574F5200338F3E /* MessageReceiveJob.swift in Sources */,
|
||||
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
|
||||
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */,
|
||||
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
|
||||
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */,
|
||||
FD09799727FFA84A00936362 /* RecipientState.swift in Sources */,
|
||||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
|
||||
FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */,
|
||||
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */,
|
||||
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
|
||||
C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */,
|
||||
FDF0B7472804F0CE004C14C5 /* DisappearingMessagesJob.swift in Sources */,
|
||||
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
|
||||
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */,
|
||||
C3D9E52725677DF20040E4F3 /* ThumbnailService.swift in Sources */,
|
||||
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */,
|
||||
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */,
|
||||
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */,
|
||||
|
@ -4626,6 +4594,7 @@
|
|||
FDB4BBC92839BEF000B7C95D /* ProfileManagerError.swift in Sources */,
|
||||
FDF0B7552807C4BB004C14C5 /* SSKEnvironment.swift in Sources */,
|
||||
B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */,
|
||||
FD3E0C84283B5835002A425C /* ConversationCellViewModel.swift in Sources */,
|
||||
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */,
|
||||
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */,
|
||||
|
@ -4668,7 +4637,6 @@
|
|||
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */,
|
||||
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
|
||||
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,
|
||||
C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */,
|
||||
FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */,
|
||||
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */,
|
||||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
||||
|
@ -4696,7 +4664,6 @@
|
|||
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
|
||||
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
|
||||
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */,
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */,
|
||||
C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,
|
||||
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
|
||||
|
@ -4729,7 +4696,6 @@
|
|||
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */,
|
||||
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */,
|
||||
EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */,
|
||||
FD4B200C283367410034334B /* ConversationCellViewModel.swift in Sources */,
|
||||
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
|
||||
B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */,
|
||||
B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */,
|
||||
|
@ -4765,7 +4731,7 @@
|
|||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */,
|
||||
FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */,
|
||||
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
|
||||
FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */,
|
||||
|
@ -4820,7 +4786,6 @@
|
|||
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
|
||||
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
|
||||
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
|
||||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
|
||||
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */,
|
||||
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */,
|
||||
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */,
|
||||
|
@ -4841,7 +4806,6 @@
|
|||
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
|
||||
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */,
|
||||
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
|
||||
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
|
||||
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,
|
||||
|
|
|
@ -91,8 +91,8 @@ extension ConversationVC:
|
|||
|
||||
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
|
||||
sendAttachments(attachments, with: messageText ?? "")
|
||||
resetMentions()
|
||||
self.snInputView.text = ""
|
||||
resetMentions()
|
||||
dismiss(animated: true) { }
|
||||
}
|
||||
|
||||
|
@ -112,8 +112,8 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
scrollToBottom(isAnimated: false)
|
||||
resetMentions()
|
||||
self.snInputView.text = ""
|
||||
resetMentions()
|
||||
}
|
||||
|
||||
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
|
||||
|
@ -464,9 +464,9 @@ extension ConversationVC:
|
|||
DispatchQueue.main.async { [weak self] in
|
||||
self?.snInputView.text = ""
|
||||
self?.snInputView.quoteDraftInfo = nil
|
||||
|
||||
self?.resetMentions()
|
||||
}
|
||||
|
||||
resetMentions()
|
||||
|
||||
if GRDBStorage.shared[.playNotificationSoundInForeground] {
|
||||
let soundID = Preferences.Sound.systemSoundId(for: .messageSent, quiet: true)
|
||||
|
@ -692,11 +692,11 @@ extension ConversationVC:
|
|||
|
||||
|
||||
switch mediaView.attachment.state {
|
||||
case .pending, .downloading, .uploading:
|
||||
case .pendingDownload, .downloading, .uploading:
|
||||
// TODO: Tapped a failed incoming attachment
|
||||
break
|
||||
|
||||
case .failed:
|
||||
case .failedDownload:
|
||||
// TODO: Tapped a failed incoming attachment
|
||||
break
|
||||
|
||||
|
@ -755,6 +755,7 @@ extension ConversationVC:
|
|||
// Otherwise share the file
|
||||
let shareVC = UIActivityViewController(activityItems: [ fileUrl ], applicationActivities: nil)
|
||||
navigationController?.present(shareVC, animated: true, completion: nil)
|
||||
|
||||
case .textOnlyMessage:
|
||||
if let reply = viewItem.quotedReply {
|
||||
// Scroll to the source of the reply
|
||||
|
|
|
@ -436,9 +436,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
|
||||
// Scroll to the last unread message if possible; otherwise scroll to the bottom.
|
||||
// When the unread message count is more than the number of view items of a page,
|
||||
// the screen will scroll to the bottom instead of the first unread message.
|
||||
// unreadIndicatorIndex is calculated during loading of the viewItems, so it's
|
||||
// supposed to be accurate.
|
||||
// the screen will scroll to the bottom instead of the first unread message
|
||||
DispatchQueue.main.async {
|
||||
if let focusedInteractionId: Int64 = self.viewModel.focusedInteractionId {
|
||||
self.scrollToInteraction(with: focusedInteractionId, isAnimated: false, highlighted: true)
|
||||
|
|
|
@ -95,8 +95,8 @@ public extension LinkPreview {
|
|||
|
||||
return .loaded
|
||||
|
||||
case .pending, .downloading, .uploading: return .loading
|
||||
case .failed: return .invalid
|
||||
case .pendingDownload, .downloading, .uploading: return .loading
|
||||
case .failedDownload: return .invalid
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,11 +78,11 @@ public class MediaView: UIView {
|
|||
private func createContents() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard attachment.state == .uploaded || attachment.state == .downloaded else {
|
||||
guard attachment.state != .pendingDownload && attachment.state != .downloading else {
|
||||
addDownloadProgressIfNecessary()
|
||||
return
|
||||
}
|
||||
guard attachment.state != .failed else {
|
||||
guard attachment.state != .failedDownload else {
|
||||
configure(forError: .failed)
|
||||
return
|
||||
}
|
||||
|
@ -101,9 +101,9 @@ public class MediaView: UIView {
|
|||
configure(forError: .invalid)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addDownloadProgressIfNecessary() {
|
||||
guard attachment.state != .failed else {
|
||||
guard attachment.state != .failedDownload else {
|
||||
configure(forError: .failed)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ final class DownloadAttachmentModal: Modal {
|
|||
// Start downloading any pending attachments for this contact (UI will automatically be
|
||||
// updated due to the database observation)
|
||||
try Attachment
|
||||
.stateInfo(authorId: profileId, state: .pending)
|
||||
.stateInfo(authorId: profileId, state: .pendingDownload)
|
||||
.fetchAll(db)
|
||||
.forEach { attachmentDownloadInfo in
|
||||
JobRunner.add(
|
||||
|
|
|
@ -58,7 +58,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
result.separatorStyle = .none
|
||||
result.keyboardDismissMode = .onDrag
|
||||
result.register(view: EmptySearchResultCell.self)
|
||||
result.register(view: ConversationCell.self)
|
||||
result.register(view: ConversationCell.Full.self)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
|
||||
return result
|
||||
|
@ -312,12 +312,12 @@ extension GlobalSearchViewController {
|
|||
return cell
|
||||
|
||||
case .contactsAndGroups:
|
||||
let cell: ConversationCell = tableView.dequeue(type: ConversationCell.self, for: indexPath)
|
||||
let cell: ConversationCell.Full = tableView.dequeue(type: ConversationCell.Full.self, for: indexPath)
|
||||
cell.updateForContactAndGroupSearchResult(with: section.elements[indexPath.row], searchText: self.termForCurrentSearchResultSet)
|
||||
return cell
|
||||
|
||||
case .messages:
|
||||
let cell: ConversationCell = tableView.dequeue(type: ConversationCell.self, for: indexPath)
|
||||
let cell: ConversationCell.Full = tableView.dequeue(type: ConversationCell.Full.self, for: indexPath)
|
||||
cell.updateForMessageSearchResult(with: section.elements[indexPath.row], searchText: self.termForCurrentSearchResultSet)
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.register(view: MessageRequestsCell.self)
|
||||
result.register(view: ConversationCell.self)
|
||||
result.register(view: ConversationCell.Full.self)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
|
@ -245,7 +245,12 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
// Reload the table content (animate changes after the first load)
|
||||
tableView.reload(
|
||||
using: StagedChangeset(source: viewModel.viewData, target: updatedViewData),
|
||||
with: .automatic,
|
||||
deleteSectionsAnimation: .none,
|
||||
insertSectionsAnimation: .none,
|
||||
reloadSectionsAnimation: .none,
|
||||
deleteRowsAnimation: .bottom,
|
||||
insertRowsAnimation: .top,
|
||||
reloadRowsAnimation: .none,
|
||||
interrupt: {
|
||||
print("Interrupt change check: \($0.changeCount)")
|
||||
return $0.changeCount > 100
|
||||
|
@ -350,7 +355,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
return cell
|
||||
|
||||
case .threads:
|
||||
let cell: ConversationCell = tableView.dequeue(type: ConversationCell.self, for: indexPath)
|
||||
let cell: ConversationCell.Full = tableView.dequeue(type: ConversationCell.Full.self, for: indexPath)
|
||||
cell.update(with: section.elements[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.backgroundColor = .clear
|
||||
result.separatorStyle = .none
|
||||
result.register(view: ConversationCell.self)
|
||||
result.register(view: ConversationCell.Full.self)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
|
@ -189,7 +189,12 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
// Reload the table content (animate changes after the first load)
|
||||
tableView.reload(
|
||||
using: StagedChangeset(source: viewModel.viewData, target: updatedViewData),
|
||||
with: .automatic,
|
||||
deleteSectionsAnimation: .none,
|
||||
insertSectionsAnimation: .none,
|
||||
reloadSectionsAnimation: .none,
|
||||
deleteRowsAnimation: .bottom,
|
||||
insertRowsAnimation: .top,
|
||||
reloadRowsAnimation: .none,
|
||||
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
||||
) { [weak self] updatedData in
|
||||
self?.viewModel.updateData(updatedData)
|
||||
|
@ -211,7 +216,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: ConversationCell = tableView.dequeue(type: ConversationCell.self, for: indexPath)
|
||||
let cell: ConversationCell.Full = tableView.dequeue(type: ConversationCell.Full.self, for: indexPath)
|
||||
cell.update(with: viewModel.viewData[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ class MessageRequestsCell: UITableViewCell {
|
|||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
result.layer.cornerRadius = (ConversationCell.unreadCountViewSize / 2)
|
||||
result.layer.cornerRadius = (ConversationCell.Full.unreadCountViewSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -115,8 +115,8 @@ class MessageRequestsCell: UITableViewCell {
|
|||
|
||||
unreadCountView.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: (Values.smallSpacing / 2)),
|
||||
unreadCountView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.Full.unreadCountViewSize),
|
||||
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.Full.unreadCountViewSize),
|
||||
|
||||
unreadCountLabel.topAnchor.constraint(equalTo: unreadCountView.topAnchor),
|
||||
unreadCountLabel.leftAnchor.constraint(equalTo: unreadCountView.leftAnchor),
|
||||
|
|
|
@ -504,7 +504,19 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
return
|
||||
}
|
||||
|
||||
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: originalFilePath)) { activityType in
|
||||
let shareVC = UIActivityViewController(activityItems: [ URL(fileURLWithPath: originalFilePath) ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
shareVC.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
|
||||
if let activityError = activityError {
|
||||
SNLog("Failed to share with activityError: \(activityError)")
|
||||
} else if completed {
|
||||
SNLog("Did share with activityType: \(activityType.debugDescription)")
|
||||
}
|
||||
guard
|
||||
let activityType = activityType,
|
||||
activityType == .saveToCameraRoll,
|
||||
|
@ -513,7 +525,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
else { return }
|
||||
|
||||
GRDBStorage.shared.write { db in
|
||||
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: self.viewModel.threadId) else {
|
||||
return
|
||||
}
|
||||
|
@ -530,6 +541,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
)
|
||||
}
|
||||
}
|
||||
self.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc public func didPressDelete(_ sender: Any) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "x-24.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "x-24@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "x-24@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 875 B |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 573 B |
|
@ -35,7 +35,6 @@
|
|||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalCoreKit/OWSLogs.h>
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Gruppe erstellt.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ ist der Gruppe beigetreten.";
|
||||
"GROUP_MEMBER_JOINED" = "%@ ist der Gruppe beigetreten.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ hat die Gruppe verlassen.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ wurde aus der Gruppe entfernt. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ wurde aus der Gruppe entfernt. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ wurden aus der Gruppe entfernt. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ wurden aus der Gruppe entfernt. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Gruppenname lautet jetzt »%@«. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Group created";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ joined the group. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ joined the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ left the group. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ left the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ was removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ were removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Title is now '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " Fue eliminado del grupo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ fue eliminado del grupo. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ fue eliminado del grupo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "El grupo se llama ahora «%@».";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ از گروه خارج شد.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ از گروه حذف شد. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ از گروه حذف شد. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ از گروه حذف شدند. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ از گروه حذف شدند. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "عنوان ، هماکنون '%@' است.";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Ryhmä luotu";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ liittyi ryhmään. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ liittyi ryhmään. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ poistui ryhmästä. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ poistui ryhmästä. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ poistettiin ryhmästä. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ poistettiin ryhmästä. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ poistettiin ryhmästä. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ poistettiin ryhmästä. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Ryhmän nimi on nyt ”%@”. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ a quitté le groupe.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ a été retiré du groupe. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ a été retiré du groupe. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ ont été retirés du groupe. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ ont été retirés du groupe. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Le titre est maintenant « %@ ».";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Group created";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ joined the group. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ joined the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ left the group. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ left the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ was removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ were removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Title is now '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Kreirana Grupa";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ se pridružio grupi. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ se pridružio grupi. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ je napustio grupu. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ je napustio grupu. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ je uklonjen iz grupe. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ je uklonjen iz grupe. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ su uklonjeni iz grupe. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ su uklonjeni iz grupe. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Naslov je sada %@. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ keluar dari group.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ telah dihapus dari grup. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ telah dihapus dari grup. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ telah dihapus dari grup. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ telah dihapus dari grup. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Topik baru saat ini '%@'.";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ ha lasciato il gruppo.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ è stato rimosso dal gruppo. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ è stato rimosso dal gruppo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ sono stati rimossi dal gruppo. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ sono stati rimossi dal gruppo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Il nuovo titolo è '%@'";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@がグループを離れました";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ はグループから削除されました。 ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ はグループから削除されました。 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ はグループから削除されました。 ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ はグループから削除されました。 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "タイトルが「%@」に変更されました";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Groep aangemaakt";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ is toegevoegd aan de groep. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ is toegevoegd aan de groep. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ heeft de groep verlaten. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ heeft de groep verlaten. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ is verwijderd uit de groep. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ is verwijderd uit de groep. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ zijn uit de groep verwijderd. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ zijn uit de groep verwijderd. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Titel is nu '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ opuścił(a) grupę.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ został usunięty z grupy. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ został usunięty z grupy. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ zostali usunięci z grupy. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ zostali usunięci z grupy. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Nowy tytuł to '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ saiu do grupo.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ foi removido do grupo. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ foi removido do grupo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ foram removidos do grupo. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ foram removidos do grupo. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "O título agora é '%@'.";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -209,9 +209,9 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = "%@ покинул группу.";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ был удален из группы. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ был удален из группы. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ были удалены из группы. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ были удалены из группы. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Название изменено на «%@».";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Group created";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ joined the group. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ joined the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ left the group. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ left the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ was removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ were removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Title is now '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Skupina vytvorená";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ sa pripojil/a ku skupine. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ sa pripojil/a ku skupine. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ opustil/a skupinu. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ opustil/a skupinu. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ bol/a odstránený/á zo skupiny. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ bol/a odstránený/á zo skupiny. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ boli odstránení zo skupiny. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ boli odstránení zo skupiny. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Názov je teraz '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Grupp skapad";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ gick med i gruppen. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ gick med i gruppen. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ lämnade gruppen. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ lämnade gruppen. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ togs bort från gruppen. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ togs bort från gruppen. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ togs bort från gruppen. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ togs bort från gruppen. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Titeln är nu '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "สร้างกลุ่มแล้ว";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ ได้เข้าร่วมกลุ่ม";
|
||||
"GROUP_MEMBER_JOINED" = "%@ ได้เข้าร่วมกลุ่ม";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ ออกจากกลุ่ม ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ ออกจากกลุ่ม ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ ถูกลบออกจากกลุ่ม ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ ถูกลบออกจากกลุ่ม ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ ถูกลบออกจากกลุ่ม ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ ถูกลบออกจากกลุ่ม ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "ชื่อเรื่องเปลี่ยนเป็น %@ แล้ว ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "Group created";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ joined the group. ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ joined the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ left the group. ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ left the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ was removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ were removed from the group. ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "Title is now '%@'. ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -205,13 +205,13 @@
|
|||
/* No comment provided by engineer. */
|
||||
"GROUP_CREATED" = "已創立群組";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_JOINED" = " %@ 已加入群組。 ";
|
||||
"GROUP_MEMBER_JOINED" = "%@ 已加入群組。 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_LEFT" = " %@ 已離開群組。 ";
|
||||
"GROUP_MEMBER_LEFT" = "%@ 已離開群組。 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBER_REMOVED" = " %@ 被踢出群組。 ";
|
||||
"GROUP_MEMBER_REMOVED" = "%@ 被踢出群組。 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_MEMBERS_REMOVED" = " %@ 已被群組踢出 ";
|
||||
"GROUP_MEMBERS_REMOVED" = "%@ 已被群組踢出 ";
|
||||
/* No comment provided by engineer. */
|
||||
"GROUP_TITLE_CHANGED" = "標題已更改為 ‘%@‘ ";
|
||||
/* No comment provided by engineer. */
|
||||
|
|
|
@ -64,7 +64,16 @@ final class ShareLogsModal : Modal {
|
|||
if let latestLogFilePath = logFilePaths.first {
|
||||
let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath)
|
||||
self.dismiss(animated: true, completion: {
|
||||
AttachmentSharing.showShareUI(for: latestLogFileURL)
|
||||
if let vc = CurrentAppContext().frontmostViewController() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = vc.view
|
||||
shareVC.popoverPresentationController?.sourceRect = vc.view.bounds
|
||||
}
|
||||
vc.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,553 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
public final class ConversationCell: UITableViewCell {
|
||||
// MARK: - UI
|
||||
|
||||
private let accentLineView: UIView = UIView()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var unreadCountView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
let size = ConversationCell.unreadCountViewSize
|
||||
result.set(.width, greaterThanOrEqualTo: size)
|
||||
result.set(.height, to: size)
|
||||
result.layer.masksToBounds = true
|
||||
result.layer.cornerRadius = (size / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var unreadCountLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var hasMentionView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.backgroundColor = Colors.accent
|
||||
let size = ConversationCell.unreadCountViewSize
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
result.layer.masksToBounds = true
|
||||
result.layer.cornerRadius = (size / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var hasMentionLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.text = "@"
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var isPinnedIcon: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(named: "Pin")?.withRenderingMode(.alwaysTemplate))
|
||||
result.contentMode = .scaleAspectFit
|
||||
let size = ConversationCell.unreadCountViewSize
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
result.tintColor = Colors.pinIcon
|
||||
result.layer.masksToBounds = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var timestampLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.alpha = Values.lowOpacity
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var snippetLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var typingIndicatorView = TypingIndicatorView()
|
||||
|
||||
private lazy var statusIndicatorView: UIImageView = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = (ConversationCell.statusIndicatorSize / 2)
|
||||
result.layer.masksToBounds = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var topLabelStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var bottomLabelStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
public static let unreadCountViewSize: CGFloat = 20
|
||||
private static let statusIndicatorSize: CGFloat = 14
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let cellHeight: CGFloat = 68
|
||||
|
||||
// Background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
|
||||
// Highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
|
||||
// Accent line view
|
||||
accentLineView.set(.width, to: Values.accentLineThickness)
|
||||
accentLineView.set(.height, to: cellHeight)
|
||||
|
||||
// Profile picture view
|
||||
let profilePictureViewSize = Values.mediumProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
|
||||
// Unread count view
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
||||
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
||||
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
||||
|
||||
// Has mention view
|
||||
hasMentionView.addSubview(hasMentionLabel)
|
||||
hasMentionLabel.pin(to: hasMentionView)
|
||||
|
||||
// Label stack view
|
||||
let topLabelSpacer = UIView.hStretchingSpacer()
|
||||
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
|
||||
topLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let snippetLabelContainer = UIView()
|
||||
snippetLabelContainer.addSubview(snippetLabel)
|
||||
snippetLabelContainer.addSubview(typingIndicatorView)
|
||||
|
||||
let bottomLabelSpacer = UIView.hStretchingSpacer()
|
||||
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
|
||||
bottomLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
|
||||
labelContainerView.axis = .vertical
|
||||
labelContainerView.alignment = .leading
|
||||
labelContainerView.spacing = 6
|
||||
labelContainerView.isUserInteractionEnabled = false
|
||||
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
// Constraints
|
||||
accentLineView.pin(.top, to: .top, of: contentView)
|
||||
accentLineView.pin(.bottom, to: .bottom, of: contentView)
|
||||
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
||||
|
||||
// HACK: The six lines below are part of a workaround for a weird layout bug
|
||||
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
topLabelStackView.set(.height, to: 20)
|
||||
topLabelSpacer.set(.height, to: 20)
|
||||
|
||||
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
bottomLabelStackView.set(.height, to: 18)
|
||||
bottomLabelSpacer.set(.height, to: 18)
|
||||
|
||||
statusIndicatorView.set(.width, to: ConversationCell.statusIndicatorSize)
|
||||
statusIndicatorView.set(.height, to: ConversationCell.statusIndicatorSize)
|
||||
|
||||
snippetLabel.pin(to: snippetLabelContainer)
|
||||
|
||||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
|
||||
// HACK: The two lines below are part of a workaround for a weird layout bug
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
|
||||
stackView.set(.height, to: cellHeight)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
// MARK: --Search Results
|
||||
|
||||
public func updateForMessageSearchResult(with cellViewModel: ViewModel, searchText: String) {
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
||||
)
|
||||
|
||||
isPinnedIcon.isHidden = true
|
||||
unreadCountView.isHidden = true
|
||||
hasMentionView.isHidden = true
|
||||
displayNameLabel.attributedText = NSMutableAttributedString(
|
||||
string: cellViewModel.displayName,
|
||||
attributes: [ .foregroundColor: Colors.text]
|
||||
)
|
||||
timestampLabel.isHidden = false
|
||||
timestampLabel.text = DateUtil.formatDate(forDisplay: cellViewModel.lastInteractionDate)
|
||||
bottomLabelStackView.isHidden = false
|
||||
snippetLabel.attributedText = getHighlightedSnippet(
|
||||
content: (cellViewModel.interactionBody ?? ""),
|
||||
authorName: (cellViewModel.authorId != cellViewModel.currentUserPublicKey ?
|
||||
cellViewModel.authorName(for: .contact) :
|
||||
nil
|
||||
),
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.smallFontSize
|
||||
)
|
||||
}
|
||||
|
||||
public func updateForContactAndGroupSearchResult(with cellViewModel: ViewModel, searchText: String) {
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
||||
)
|
||||
|
||||
isPinnedIcon.isHidden = true
|
||||
unreadCountView.isHidden = true
|
||||
hasMentionView.isHidden = true
|
||||
timestampLabel.isHidden = true
|
||||
displayNameLabel.attributedText = getHighlightedSnippet(
|
||||
content: cellViewModel.displayName,
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.mediumFontSize
|
||||
)
|
||||
|
||||
switch cellViewModel.threadVariant {
|
||||
case .contact, .openGroup: bottomLabelStackView.isHidden = true
|
||||
|
||||
case .closedGroup:
|
||||
bottomLabelStackView.isHidden = (cellViewModel.threadMemberNames ?? "").isEmpty
|
||||
snippetLabel.attributedText = getHighlightedSnippet(
|
||||
content: (cellViewModel.threadMemberNames ?? ""),
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.smallFontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: --Standard
|
||||
|
||||
public func update(with cellViewModel: ViewModel) {
|
||||
let unreadCount: UInt = (cellViewModel.threadUnreadCount ?? 0)
|
||||
backgroundColor = (cellViewModel.threadIsPinned ? Colors.cellPinned : Colors.cellBackground)
|
||||
|
||||
if cellViewModel.threadIsBlocked == true {
|
||||
accentLineView.backgroundColor = Colors.destructive
|
||||
accentLineView.alpha = 1
|
||||
}
|
||||
else {
|
||||
accentLineView.backgroundColor = Colors.accent
|
||||
accentLineView.alpha = (unreadCount > 0 ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12
|
||||
}
|
||||
|
||||
isPinnedIcon.isHidden = !cellViewModel.threadIsPinned
|
||||
unreadCountView.isHidden = (unreadCount <= 0)
|
||||
unreadCountLabel.text = (unreadCount < 10000 ? "\(unreadCount)" : "9999+")
|
||||
unreadCountLabel.font = .boldSystemFont(
|
||||
ofSize: (unreadCount < 10000 ? Values.verySmallFontSize : 8)
|
||||
)
|
||||
hasMentionView.isHidden = !(
|
||||
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) &&
|
||||
(cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup)
|
||||
)
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (
|
||||
cellViewModel.threadVariant == .openGroup &&
|
||||
cellViewModel.openGroupProfilePictureData == nil
|
||||
)
|
||||
)
|
||||
displayNameLabel.text = cellViewModel.displayName
|
||||
timestampLabel.text = DateUtil.formatDate(forDisplay: cellViewModel.lastInteractionDate)
|
||||
|
||||
if cellViewModel.threadContactIsTyping == true {
|
||||
snippetLabel.text = ""
|
||||
typingIndicatorView.isHidden = false
|
||||
typingIndicatorView.startAnimation()
|
||||
}
|
||||
else {
|
||||
snippetLabel.attributedText = getSnippet(cellViewModel: cellViewModel)
|
||||
typingIndicatorView.isHidden = true
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
|
||||
statusIndicatorView.backgroundColor = nil
|
||||
|
||||
switch (cellViewModel.interactionVariant, cellViewModel.interactionState) {
|
||||
case (.standardOutgoing, .sending):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "CircleDotDotDot").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.text
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
case (.standardOutgoing, .sent):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.text
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
case (.standardOutgoing, .failed):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "message_status_failed").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.destructive
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
default:
|
||||
statusIndicatorView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Snippet generation
|
||||
|
||||
private func getSnippet(cellViewModel: ViewModel) -> NSMutableAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
|
||||
if Date().timeIntervalSince1970 < (cellViewModel.threadMutedUntilTimestamp ?? 0) {
|
||||
result.append(NSAttributedString(
|
||||
string: "\u{e067} ",
|
||||
attributes: [
|
||||
.font: UIFont.ows_elegantIconsFont(10),
|
||||
.foregroundColor :Colors.unimportant
|
||||
]
|
||||
))
|
||||
}
|
||||
else if cellViewModel.threadOnlyNotifyForMentions == true {
|
||||
let imageAttachment = NSTextAttachment()
|
||||
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: Colors.unimportant)
|
||||
imageAttachment.bounds = CGRect(x: 0, y: -2, width: Values.smallFontSize, height: Values.smallFontSize)
|
||||
|
||||
let imageString = NSAttributedString(attachment: imageAttachment)
|
||||
result.append(imageString)
|
||||
result.append(NSAttributedString(
|
||||
string: " ",
|
||||
attributes: [
|
||||
.font: UIFont.ows_elegantIconsFont(10),
|
||||
.foregroundColor: Colors.unimportant
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
let font: UIFont = ((cellViewModel.threadUnreadCount ?? 0) > 0 ?
|
||||
.boldSystemFont(ofSize: Values.smallFontSize) :
|
||||
.systemFont(ofSize: Values.smallFontSize)
|
||||
)
|
||||
|
||||
if cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup {
|
||||
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
||||
|
||||
result.append(NSAttributedString(
|
||||
string: "\(authorName): ",
|
||||
attributes: [
|
||||
.font: font,
|
||||
.foregroundColor: Colors.text
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
result.append(NSAttributedString(
|
||||
string: MentionUtilities.highlightMentions(
|
||||
in: Interaction.previewText(
|
||||
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
|
||||
body: cellViewModel.interactionBody,
|
||||
authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant),
|
||||
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
|
||||
attachmentCount: cellViewModel.interactionAttachmentCount,
|
||||
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
|
||||
),
|
||||
threadVariant: cellViewModel.threadVariant
|
||||
),
|
||||
attributes: [
|
||||
.font: font,
|
||||
.foregroundColor: Colors.text
|
||||
]
|
||||
))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func getHighlightedSnippet(
|
||||
content: String,
|
||||
authorName: String? = nil,
|
||||
searchText: String,
|
||||
fontSize: CGFloat
|
||||
) -> NSAttributedString {
|
||||
guard !content.isEmpty, content != "NOTE_TO_SELF".localized() else {
|
||||
return NSMutableAttributedString(
|
||||
string: (authorName != nil && authorName?.isEmpty != true ?
|
||||
"\(authorName ?? ""): \(content)" :
|
||||
content
|
||||
),
|
||||
attributes: [ .foregroundColor: Colors.text ]
|
||||
)
|
||||
}
|
||||
|
||||
// Replace mentions in the content
|
||||
//
|
||||
// Note: The 'threadVariant' is used for profile context but in the search results
|
||||
// we don't want to include the truncated id as part of the name so we exclude it
|
||||
let mentionReplacedContent: String = MentionUtilities.highlightMentions(
|
||||
in: content,
|
||||
threadVariant: .contact
|
||||
)
|
||||
let result: NSMutableAttributedString = NSMutableAttributedString(
|
||||
string: mentionReplacedContent,
|
||||
attributes: [
|
||||
.foregroundColor: Colors.text
|
||||
.withAlphaComponent(Values.lowOpacity)
|
||||
]
|
||||
)
|
||||
|
||||
// Bold each part of the searh term which matched
|
||||
let normalizedSnippet: String = mentionReplacedContent.lowercased()
|
||||
var firstMatchRange: Range<String.Index>?
|
||||
|
||||
ConversationCell.ViewModel.searchTermParts(searchText)
|
||||
.map { part -> String in
|
||||
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
|
||||
|
||||
return String(part[part.index(after: part.startIndex)..<part.endIndex])
|
||||
}
|
||||
.forEach { part in
|
||||
guard
|
||||
normalizedSnippet.contains(part.lowercased()),
|
||||
let range: Range<String.Index> = normalizedSnippet.range(of: part.lowercased())
|
||||
else { return }
|
||||
|
||||
// Store the range of the first match so we can focus it in the content displayed
|
||||
if firstMatchRange == nil {
|
||||
firstMatchRange = range
|
||||
}
|
||||
|
||||
let legacyRange: NSRange = NSRange(range, in: normalizedSnippet)
|
||||
result.addAttribute(.foregroundColor, value: Colors.text, range: legacyRange)
|
||||
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: legacyRange)
|
||||
}
|
||||
|
||||
// We then want to truncate the content so the first metching term is visible
|
||||
let startOfSnippet: String.Index = (
|
||||
firstMatchRange.map {
|
||||
max(
|
||||
mentionReplacedContent.startIndex,
|
||||
mentionReplacedContent
|
||||
.index(
|
||||
$0.lowerBound,
|
||||
offsetBy: -10,
|
||||
limitedBy: mentionReplacedContent.startIndex
|
||||
)
|
||||
.defaulting(to: mentionReplacedContent.startIndex)
|
||||
)
|
||||
} ??
|
||||
mentionReplacedContent.startIndex
|
||||
)
|
||||
|
||||
// This method determines if the content is probably too long and returns the truncated or untruncated
|
||||
// content accordingly
|
||||
func truncatingIfNeeded(approxWidth: CGFloat, content: NSAttributedString) -> NSAttributedString {
|
||||
let approxFullWidth: CGFloat = (approxWidth + profilePictureView.size + (Values.mediumSpacing * 3))
|
||||
|
||||
guard ((bounds.width - approxFullWidth) < 0) else { return content }
|
||||
|
||||
return content.attributedSubstring(
|
||||
from: NSRange(startOfSnippet..<normalizedSnippet.endIndex, in: normalizedSnippet)
|
||||
)
|
||||
}
|
||||
|
||||
// Now that we have generated the focused snippet add the author name as a prefix (if provided)
|
||||
return authorName
|
||||
.map { authorName -> NSAttributedString? in
|
||||
guard !authorName.isEmpty else { return nil }
|
||||
|
||||
let authorPrefix: NSAttributedString = NSAttributedString(
|
||||
string: "\(authorName): ...",
|
||||
attributes: [ .foregroundColor: Colors.text ]
|
||||
)
|
||||
|
||||
return authorPrefix
|
||||
.appending(
|
||||
truncatingIfNeeded(
|
||||
approxWidth: (authorPrefix.size().width + result.size().width),
|
||||
content: result
|
||||
)
|
||||
)
|
||||
}
|
||||
.defaulting(
|
||||
to: truncatingIfNeeded(
|
||||
approxWidth: result.size().width,
|
||||
content: result
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,570 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
public extension ConversationCell {
|
||||
public final class Full: UITableViewCell {
|
||||
// MARK: - UI
|
||||
|
||||
private let accentLineView: UIView = UIView()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var unreadCountView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
let size = ConversationCell.Full.unreadCountViewSize
|
||||
result.set(.width, greaterThanOrEqualTo: size)
|
||||
result.set(.height, to: size)
|
||||
result.layer.masksToBounds = true
|
||||
result.layer.cornerRadius = (size / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var unreadCountLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var hasMentionView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.backgroundColor = Colors.accent
|
||||
let size = ConversationCell.Full.unreadCountViewSize
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
result.layer.masksToBounds = true
|
||||
result.layer.cornerRadius = (size / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var hasMentionLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.text = "@"
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var isPinnedIcon: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(named: "Pin")?.withRenderingMode(.alwaysTemplate))
|
||||
result.contentMode = .scaleAspectFit
|
||||
let size = ConversationCell.Full.unreadCountViewSize
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
result.tintColor = Colors.pinIcon
|
||||
result.layer.masksToBounds = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var timestampLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.alpha = Values.lowOpacity
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var snippetLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var typingIndicatorView = TypingIndicatorView()
|
||||
|
||||
private lazy var statusIndicatorView: UIImageView = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = (ConversationCell.Full.statusIndicatorSize / 2)
|
||||
result.layer.masksToBounds = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var topLabelStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var bottomLabelStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.alignment = .center
|
||||
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
public static let unreadCountViewSize: CGFloat = 20
|
||||
private static let statusIndicatorSize: CGFloat = 14
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let cellHeight: CGFloat = 68
|
||||
|
||||
// Background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
|
||||
// Highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
|
||||
// Accent line view
|
||||
accentLineView.set(.width, to: Values.accentLineThickness)
|
||||
accentLineView.set(.height, to: cellHeight)
|
||||
|
||||
// Profile picture view
|
||||
let profilePictureViewSize = Values.mediumProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
|
||||
// Unread count view
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
||||
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
||||
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
||||
|
||||
// Has mention view
|
||||
hasMentionView.addSubview(hasMentionLabel)
|
||||
hasMentionLabel.pin(to: hasMentionView)
|
||||
|
||||
// Label stack view
|
||||
let topLabelSpacer = UIView.hStretchingSpacer()
|
||||
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
|
||||
topLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let snippetLabelContainer = UIView()
|
||||
snippetLabelContainer.addSubview(snippetLabel)
|
||||
snippetLabelContainer.addSubview(typingIndicatorView)
|
||||
|
||||
let bottomLabelSpacer = UIView.hStretchingSpacer()
|
||||
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
|
||||
bottomLabelStackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
|
||||
labelContainerView.axis = .vertical
|
||||
labelContainerView.alignment = .leading
|
||||
labelContainerView.spacing = 6
|
||||
labelContainerView.isUserInteractionEnabled = false
|
||||
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
// Constraints
|
||||
accentLineView.pin(.top, to: .top, of: contentView)
|
||||
accentLineView.pin(.bottom, to: .bottom, of: contentView)
|
||||
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
||||
|
||||
// HACK: The six lines below are part of a workaround for a weird layout bug
|
||||
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
topLabelStackView.set(.height, to: 20)
|
||||
topLabelSpacer.set(.height, to: 20)
|
||||
|
||||
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
bottomLabelStackView.set(.height, to: 18)
|
||||
bottomLabelSpacer.set(.height, to: 18)
|
||||
|
||||
statusIndicatorView.set(.width, to: ConversationCell.Full.statusIndicatorSize)
|
||||
statusIndicatorView.set(.height, to: ConversationCell.Full.statusIndicatorSize)
|
||||
|
||||
snippetLabel.pin(to: snippetLabelContainer)
|
||||
|
||||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
|
||||
// HACK: The two lines below are part of a workaround for a weird layout bug
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
|
||||
stackView.set(.height, to: cellHeight)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
// MARK: --Search Results
|
||||
|
||||
public func updateForMessageSearchResult(with cellViewModel: ViewModel, searchText: String) {
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
||||
)
|
||||
|
||||
isPinnedIcon.isHidden = true
|
||||
unreadCountView.isHidden = true
|
||||
hasMentionView.isHidden = true
|
||||
displayNameLabel.attributedText = NSMutableAttributedString(
|
||||
string: cellViewModel.displayName,
|
||||
attributes: [ .foregroundColor: Colors.text]
|
||||
)
|
||||
timestampLabel.isHidden = false
|
||||
timestampLabel.text = DateUtil.formatDate(forDisplay: cellViewModel.lastInteractionDate)
|
||||
bottomLabelStackView.isHidden = false
|
||||
snippetLabel.attributedText = getHighlightedSnippet(
|
||||
content: Interaction.previewText(
|
||||
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
|
||||
body: cellViewModel.interactionBody,
|
||||
authorDisplayName: cellViewModel.authorName(for: .contact),
|
||||
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
|
||||
attachmentCount: cellViewModel.interactionAttachmentCount,
|
||||
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
|
||||
),
|
||||
authorName: (cellViewModel.authorId != cellViewModel.currentUserPublicKey ?
|
||||
cellViewModel.authorName(for: .contact) :
|
||||
nil
|
||||
),
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.smallFontSize
|
||||
)
|
||||
}
|
||||
|
||||
public func updateForContactAndGroupSearchResult(with cellViewModel: ViewModel, searchText: String) {
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
||||
)
|
||||
|
||||
isPinnedIcon.isHidden = true
|
||||
unreadCountView.isHidden = true
|
||||
hasMentionView.isHidden = true
|
||||
timestampLabel.isHidden = true
|
||||
displayNameLabel.attributedText = getHighlightedSnippet(
|
||||
content: cellViewModel.displayName,
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.mediumFontSize
|
||||
)
|
||||
|
||||
switch cellViewModel.threadVariant {
|
||||
case .contact, .openGroup: bottomLabelStackView.isHidden = true
|
||||
|
||||
case .closedGroup:
|
||||
bottomLabelStackView.isHidden = (cellViewModel.threadMemberNames ?? "").isEmpty
|
||||
snippetLabel.attributedText = getHighlightedSnippet(
|
||||
content: (cellViewModel.threadMemberNames ?? ""),
|
||||
searchText: searchText.lowercased(),
|
||||
fontSize: Values.smallFontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: --Standard
|
||||
|
||||
public func update(with cellViewModel: ViewModel) {
|
||||
let unreadCount: UInt = (cellViewModel.threadUnreadCount ?? 0)
|
||||
backgroundColor = (cellViewModel.threadIsPinned ? Colors.cellPinned : Colors.cellBackground)
|
||||
|
||||
if cellViewModel.threadIsBlocked == true {
|
||||
accentLineView.backgroundColor = Colors.destructive
|
||||
accentLineView.alpha = 1
|
||||
}
|
||||
else {
|
||||
accentLineView.backgroundColor = Colors.accent
|
||||
accentLineView.alpha = (unreadCount > 0 ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12
|
||||
}
|
||||
|
||||
isPinnedIcon.isHidden = !cellViewModel.threadIsPinned
|
||||
unreadCountView.isHidden = (unreadCount <= 0)
|
||||
unreadCountLabel.text = (unreadCount < 10000 ? "\(unreadCount)" : "9999+")
|
||||
unreadCountLabel.font = .boldSystemFont(
|
||||
ofSize: (unreadCount < 10000 ? Values.verySmallFontSize : 8)
|
||||
)
|
||||
hasMentionView.isHidden = !(
|
||||
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) &&
|
||||
(cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup)
|
||||
)
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (
|
||||
cellViewModel.threadVariant == .openGroup &&
|
||||
cellViewModel.openGroupProfilePictureData == nil
|
||||
)
|
||||
)
|
||||
displayNameLabel.text = cellViewModel.displayName
|
||||
timestampLabel.text = DateUtil.formatDate(forDisplay: cellViewModel.lastInteractionDate)
|
||||
|
||||
if cellViewModel.threadContactIsTyping == true {
|
||||
snippetLabel.text = ""
|
||||
typingIndicatorView.isHidden = false
|
||||
typingIndicatorView.startAnimation()
|
||||
}
|
||||
else {
|
||||
snippetLabel.attributedText = getSnippet(cellViewModel: cellViewModel)
|
||||
typingIndicatorView.isHidden = true
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
|
||||
statusIndicatorView.backgroundColor = nil
|
||||
|
||||
switch (cellViewModel.interactionVariant, cellViewModel.interactionState) {
|
||||
case (.standardOutgoing, .sending):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "CircleDotDotDot").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.text
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
case (.standardOutgoing, .sent):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.text
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
case (.standardOutgoing, .failed):
|
||||
statusIndicatorView.image = #imageLiteral(resourceName: "message_status_failed").withRenderingMode(.alwaysTemplate)
|
||||
statusIndicatorView.tintColor = Colors.destructive
|
||||
statusIndicatorView.isHidden = false
|
||||
|
||||
default:
|
||||
statusIndicatorView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Snippet generation
|
||||
|
||||
private func getSnippet(cellViewModel: ViewModel) -> NSMutableAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
|
||||
if Date().timeIntervalSince1970 < (cellViewModel.threadMutedUntilTimestamp ?? 0) {
|
||||
result.append(NSAttributedString(
|
||||
string: "\u{e067} ",
|
||||
attributes: [
|
||||
.font: UIFont.ows_elegantIconsFont(10),
|
||||
.foregroundColor :Colors.unimportant
|
||||
]
|
||||
))
|
||||
}
|
||||
else if cellViewModel.threadOnlyNotifyForMentions == true {
|
||||
let imageAttachment = NSTextAttachment()
|
||||
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: Colors.unimportant)
|
||||
imageAttachment.bounds = CGRect(x: 0, y: -2, width: Values.smallFontSize, height: Values.smallFontSize)
|
||||
|
||||
let imageString = NSAttributedString(attachment: imageAttachment)
|
||||
result.append(imageString)
|
||||
result.append(NSAttributedString(
|
||||
string: " ",
|
||||
attributes: [
|
||||
.font: UIFont.ows_elegantIconsFont(10),
|
||||
.foregroundColor: Colors.unimportant
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
let font: UIFont = ((cellViewModel.threadUnreadCount ?? 0) > 0 ?
|
||||
.boldSystemFont(ofSize: Values.smallFontSize) :
|
||||
.systemFont(ofSize: Values.smallFontSize)
|
||||
)
|
||||
|
||||
if cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup {
|
||||
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
||||
|
||||
result.append(NSAttributedString(
|
||||
string: "\(authorName): ",
|
||||
attributes: [
|
||||
.font: font,
|
||||
.foregroundColor: Colors.text
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
result.append(NSAttributedString(
|
||||
string: MentionUtilities.highlightMentions(
|
||||
in: Interaction.previewText(
|
||||
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
|
||||
body: cellViewModel.interactionBody,
|
||||
authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant),
|
||||
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
|
||||
attachmentCount: cellViewModel.interactionAttachmentCount,
|
||||
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
|
||||
),
|
||||
threadVariant: cellViewModel.threadVariant
|
||||
),
|
||||
attributes: [
|
||||
.font: font,
|
||||
.foregroundColor: Colors.text
|
||||
]
|
||||
))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func getHighlightedSnippet(
|
||||
content: String,
|
||||
authorName: String? = nil,
|
||||
searchText: String,
|
||||
fontSize: CGFloat
|
||||
) -> NSAttributedString {
|
||||
guard !content.isEmpty, content != "NOTE_TO_SELF".localized() else {
|
||||
return NSMutableAttributedString(
|
||||
string: (authorName != nil && authorName?.isEmpty != true ?
|
||||
"\(authorName ?? ""): \(content)" :
|
||||
content
|
||||
),
|
||||
attributes: [ .foregroundColor: Colors.text ]
|
||||
)
|
||||
}
|
||||
|
||||
// Replace mentions in the content
|
||||
//
|
||||
// Note: The 'threadVariant' is used for profile context but in the search results
|
||||
// we don't want to include the truncated id as part of the name so we exclude it
|
||||
let mentionReplacedContent: String = MentionUtilities.highlightMentions(
|
||||
in: content,
|
||||
threadVariant: .contact
|
||||
)
|
||||
let result: NSMutableAttributedString = NSMutableAttributedString(
|
||||
string: mentionReplacedContent,
|
||||
attributes: [
|
||||
.foregroundColor: Colors.text
|
||||
.withAlphaComponent(Values.lowOpacity)
|
||||
]
|
||||
)
|
||||
|
||||
// Bold each part of the searh term which matched
|
||||
let normalizedSnippet: String = mentionReplacedContent.lowercased()
|
||||
var firstMatchRange: Range<String.Index>?
|
||||
|
||||
ConversationCell.ViewModel.searchTermParts(searchText)
|
||||
.map { part -> String in
|
||||
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
|
||||
|
||||
return String(part[part.index(after: part.startIndex)..<part.endIndex])
|
||||
}
|
||||
.forEach { part in
|
||||
// Highlight all ranges of the text (Note: The search logic only finds results that start
|
||||
// with the term so we use the regex below to ensure we only highlight those cases)
|
||||
normalizedSnippet
|
||||
.ranges(
|
||||
of: (CurrentAppContext().isRTL ?
|
||||
"\(part.lowercased())(^|[ ])" :
|
||||
"(^|[ ])\(part.lowercased())"
|
||||
),
|
||||
options: [.regularExpression]
|
||||
)
|
||||
.forEach { range in
|
||||
// Store the range of the first match so we can focus it in the content displayed
|
||||
if firstMatchRange == nil {
|
||||
firstMatchRange = range
|
||||
}
|
||||
|
||||
let legacyRange: NSRange = NSRange(range, in: normalizedSnippet)
|
||||
result.addAttribute(.foregroundColor, value: Colors.text, range: legacyRange)
|
||||
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: legacyRange)
|
||||
}
|
||||
}
|
||||
|
||||
// We then want to truncate the content so the first matching term is visible
|
||||
let startOfSnippet: String.Index = (
|
||||
firstMatchRange.map {
|
||||
max(
|
||||
mentionReplacedContent.startIndex,
|
||||
mentionReplacedContent
|
||||
.index(
|
||||
$0.lowerBound,
|
||||
offsetBy: -10,
|
||||
limitedBy: mentionReplacedContent.startIndex
|
||||
)
|
||||
.defaulting(to: mentionReplacedContent.startIndex)
|
||||
)
|
||||
} ??
|
||||
mentionReplacedContent.startIndex
|
||||
)
|
||||
|
||||
// This method determines if the content is probably too long and returns the truncated or untruncated
|
||||
// content accordingly
|
||||
func truncatingIfNeeded(approxWidth: CGFloat, content: NSAttributedString) -> NSAttributedString {
|
||||
let approxFullWidth: CGFloat = (approxWidth + profilePictureView.size + (Values.mediumSpacing * 3))
|
||||
|
||||
guard ((bounds.width - approxFullWidth) < 0) else { return content }
|
||||
|
||||
return content.attributedSubstring(
|
||||
from: NSRange(startOfSnippet..<normalizedSnippet.endIndex, in: normalizedSnippet)
|
||||
)
|
||||
}
|
||||
|
||||
// Now that we have generated the focused snippet add the author name as a prefix (if provided)
|
||||
return authorName
|
||||
.map { authorName -> NSAttributedString? in
|
||||
guard !authorName.isEmpty else { return nil }
|
||||
|
||||
let authorPrefix: NSAttributedString = NSAttributedString(
|
||||
string: "\(authorName): ...",
|
||||
attributes: [ .foregroundColor: Colors.text ]
|
||||
)
|
||||
|
||||
return authorPrefix
|
||||
.appending(
|
||||
truncatingIfNeeded(
|
||||
approxWidth: (authorPrefix.size().width + result.size().width),
|
||||
content: result
|
||||
)
|
||||
)
|
||||
}
|
||||
.defaulting(
|
||||
to: truncatingIfNeeded(
|
||||
approxWidth: result.size().width,
|
||||
content: result
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionMessagingKit
|
||||
|
@ -38,8 +39,23 @@ public final class BackgroundPoller : NSObject {
|
|||
}
|
||||
|
||||
private static func pollForClosedGroupMessages() -> [Promise<Void>] {
|
||||
let publicKeys = Storage.shared.getUserClosedGroupPublicKeys()
|
||||
return publicKeys.map { getMessages(for: $0) }
|
||||
// Fetch all closed groups (excluding any don't contain the current user as a
|
||||
// GroupMemeber as the user is no longer a member of those)
|
||||
return GRDBStorage.shared
|
||||
.read { db in
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.joining(
|
||||
required: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
|
||||
)
|
||||
.asRequest(of: String.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
.defaulting(to: [])
|
||||
.map { groupPublicKey in
|
||||
getMessages(for: groupPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getMessages(for publicKey: String) -> Promise<Void> {
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
enum ContactUtilities {
|
||||
|
||||
static func getAllContacts() -> [String] {
|
||||
// Collect all contacts
|
||||
var result: [String] = []
|
||||
Storage.read { transaction in
|
||||
// FIXME: If a user deletes a contact thread they will no longer appear in this list (ie. won't be an option for closed group conversations)
|
||||
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard
|
||||
let thread: TSContactThread = object as? TSContactThread,
|
||||
thread.shouldBeVisible,
|
||||
Storage.shared.getContact(
|
||||
with: thread.contactSessionID(),
|
||||
using: transaction
|
||||
)?.didApproveMe == true
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
result.append(thread.contactSessionID())
|
||||
}
|
||||
}
|
||||
func getDisplayName(for publicKey: String) -> String {
|
||||
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
}
|
||||
|
||||
// Remove the current user
|
||||
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) {
|
||||
result.remove(at: index)
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc public enum MessageReceiptStatus: Int {
|
||||
case uploading
|
||||
case sending
|
||||
case sent
|
||||
case delivered
|
||||
case read
|
||||
case failed
|
||||
case skipped
|
||||
}
|
||||
|
||||
@objc
|
||||
public class MessageRecipientStatusUtils: NSObject {
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"do not instantiate this class.")
|
||||
private override init() {
|
||||
}
|
||||
|
||||
// This method is per-recipient.
|
||||
@objc
|
||||
public class func recipientStatus(outgoingMessage: TSOutgoingMessage,
|
||||
recipientState: TSOutgoingMessageRecipientState) -> MessageReceiptStatus {
|
||||
let (messageReceiptStatus, _, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage,
|
||||
recipientState: recipientState)
|
||||
return messageReceiptStatus
|
||||
}
|
||||
|
||||
// This method is per-recipient.
|
||||
@objc
|
||||
public class func shortStatusMessage(outgoingMessage: TSOutgoingMessage,
|
||||
recipientState: TSOutgoingMessageRecipientState) -> String {
|
||||
let (_, shortStatusMessage, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage,
|
||||
recipientState: recipientState)
|
||||
return shortStatusMessage
|
||||
}
|
||||
|
||||
// This method is per-recipient.
|
||||
@objc
|
||||
public class func longStatusMessage(outgoingMessage: TSOutgoingMessage,
|
||||
recipientState: TSOutgoingMessageRecipientState) -> String {
|
||||
let (_, _, longStatusMessage) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage,
|
||||
recipientState: recipientState)
|
||||
return longStatusMessage
|
||||
}
|
||||
|
||||
// This method is per-recipient.
|
||||
class func recipientStatusAndStatusMessage(outgoingMessage: TSOutgoingMessage,
|
||||
recipientState: TSOutgoingMessageRecipientState) -> (status: MessageReceiptStatus, shortStatusMessage: String, longStatusMessage: String) {
|
||||
|
||||
switch recipientState.state {
|
||||
case .failed:
|
||||
let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment: "status message for failed messages")
|
||||
let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "status message for failed messages")
|
||||
return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
|
||||
case .sending:
|
||||
if outgoingMessage.hasAttachments() {
|
||||
assert(outgoingMessage.messageState == .sending)
|
||||
|
||||
let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING",
|
||||
comment: "status message while attachment is uploading")
|
||||
return (status:.uploading, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
|
||||
} else {
|
||||
assert(outgoingMessage.messageState == .sending)
|
||||
|
||||
let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING",
|
||||
comment: "message status while message is sending.")
|
||||
return (status:.sending, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
|
||||
}
|
||||
case .sent:
|
||||
if let readTimestamp = recipientState.readTimestamp {
|
||||
let timestampString = DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value)
|
||||
let shortStatusMessage = timestampString
|
||||
let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment: "status message for read messages").rtlSafeAppend(" ")
|
||||
.rtlSafeAppend(timestampString)
|
||||
return (status:.read, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
|
||||
}
|
||||
if let deliveryTimestamp = recipientState.deliveryTimestamp {
|
||||
let timestampString = DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value)
|
||||
let shortStatusMessage = timestampString
|
||||
let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED",
|
||||
comment: "message status for message delivered to their recipient.").rtlSafeAppend(" ")
|
||||
.rtlSafeAppend(timestampString)
|
||||
return (status:.delivered, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
|
||||
}
|
||||
let statusMessage =
|
||||
NSLocalizedString("MESSAGE_STATUS_SENT",
|
||||
comment: "status message for sent messages")
|
||||
return (status:.sent, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
|
||||
case .skipped:
|
||||
let statusMessage = NSLocalizedString("MESSAGE_STATUS_RECIPIENT_SKIPPED",
|
||||
comment: "message status if message delivery to a recipient is skipped. We skip delivering group messages to users who have left the group or unregistered their Signal account.")
|
||||
return (status:.skipped, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// This method is per-message.
|
||||
internal class func receiptStatusAndMessage(outgoingMessage: TSOutgoingMessage) -> (status: MessageReceiptStatus, message: String) {
|
||||
|
||||
switch outgoingMessage.messageState {
|
||||
case .failed:
|
||||
// Use the "long" version of this message here.
|
||||
return (.failed, NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "status message for failed messages"))
|
||||
case .sending:
|
||||
if outgoingMessage.hasAttachments() {
|
||||
return (.uploading, NSLocalizedString("MESSAGE_STATUS_UPLOADING",
|
||||
comment: "status message while attachment is uploading"))
|
||||
} else {
|
||||
return (.sending, NSLocalizedString("MESSAGE_STATUS_SENDING",
|
||||
comment: "message status while message is sending."))
|
||||
}
|
||||
case .sent:
|
||||
if outgoingMessage.readRecipientIds().count > 0 {
|
||||
return (.read, NSLocalizedString("MESSAGE_STATUS_READ", comment: "status message for read messages"))
|
||||
}
|
||||
if outgoingMessage.wasDeliveredToAnyRecipient {
|
||||
return (.delivered, NSLocalizedString("MESSAGE_STATUS_DELIVERED",
|
||||
comment: "message status for message delivered to their recipient."))
|
||||
}
|
||||
return (.sent, NSLocalizedString("MESSAGE_STATUS_SENT",
|
||||
comment: "status message for sent messages"))
|
||||
default:
|
||||
owsFailDebug("Message has unexpected status: \(outgoingMessage.messageState).")
|
||||
return (.sent, NSLocalizedString("MESSAGE_STATUS_SENT",
|
||||
comment: "status message for sent messages"))
|
||||
}
|
||||
}
|
||||
|
||||
// This method is per-message.
|
||||
@objc
|
||||
public class func receiptMessage(outgoingMessage: TSOutgoingMessage) -> String {
|
||||
let (_, message ) = receiptStatusAndMessage(outgoingMessage: outgoingMessage)
|
||||
return message
|
||||
}
|
||||
|
||||
// This method is per-message.
|
||||
@objc
|
||||
public class func recipientStatus(outgoingMessage: TSOutgoingMessage) -> MessageReceiptStatus {
|
||||
let (status, _ ) = receiptStatusAndMessage(outgoingMessage: outgoingMessage)
|
||||
return status
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func description(forMessageReceiptStatus value: MessageReceiptStatus) -> String {
|
||||
switch(value) {
|
||||
case .read:
|
||||
return "read"
|
||||
case .uploading:
|
||||
return "uploading"
|
||||
case .delivered:
|
||||
return "delivered"
|
||||
case .sent:
|
||||
return "sent"
|
||||
case .sending:
|
||||
return "sending"
|
||||
case .failed:
|
||||
return "failed"
|
||||
case .skipped:
|
||||
return "skipped"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ public enum SMKLegacy {
|
|||
// Preferences
|
||||
|
||||
internal static let preferencesCollection = "SignalPreferences"
|
||||
internal static let additionalPreferencesCollection = "SSKPreferences"
|
||||
internal static let preferencesKeyScreenSecurityDisabled = "Screen Security Key"
|
||||
internal static let preferencesKeyLastRecordedPushToken = "LastRecordedPushToken"
|
||||
internal static let preferencesKeyLastRecordedVoipToken = "LastRecordedVoipToken"
|
||||
|
|
|
@ -774,7 +774,6 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
case .sending: return .sending
|
||||
case .skipped: return .skipped
|
||||
case .sent: return .sent
|
||||
@unknown default: throw GRDBStorageError.migrationFailed
|
||||
}
|
||||
}(),
|
||||
readTimestampMs: legacyState.readTimestamp,
|
||||
|
@ -1227,6 +1226,20 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
return legacyInteractionIdentifierToIdFallbackMap[fallbackIdentifier]
|
||||
}()
|
||||
|
||||
// Don't botther adding any 'MessageSend' jobs VisibleMessages which don't have associated
|
||||
// interactions
|
||||
switch legacyJob.message {
|
||||
case is SMKLegacy._VisibleMessage:
|
||||
guard interactionId != nil else {
|
||||
SNLog("[Migration Warning] Unable to find associated interaction to messageSend job, ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
let job: Job? = try Job(
|
||||
failureCount: legacyJob.failureCount,
|
||||
variant: .messageSend,
|
||||
|
@ -1243,7 +1256,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
)
|
||||
)?.inserted(db)
|
||||
|
||||
if let oldId: String = legacyJob.id, let newId: Int64 = job?.id {
|
||||
if let oldId: String = legacyJob.id {
|
||||
messageSendJobLegacyMap[oldId] = job
|
||||
}
|
||||
}
|
||||
|
@ -1339,6 +1352,10 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
legacyPreferences[key] = object
|
||||
}
|
||||
|
||||
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.additionalPreferencesCollection) { key, object, _ in
|
||||
legacyPreferences[key] = object
|
||||
}
|
||||
|
||||
// Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value
|
||||
// for the notification sound so catch it and default
|
||||
let globalNotificationSoundValue: Int32 = transaction.int(
|
||||
|
@ -1447,12 +1464,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
switch legacyAttachment {
|
||||
case let stream as SMKLegacy._AttachmentStream: // Outgoing or already downloaded
|
||||
switch interactionVariant {
|
||||
case .standardOutgoing: return (stream.isUploaded ? .uploaded : .pending)
|
||||
case .standardOutgoing: return (stream.isUploaded ? .uploaded : .uploading)
|
||||
default: return .downloaded
|
||||
}
|
||||
|
||||
// All other cases can just be set to 'pending'
|
||||
default: return .pending
|
||||
// All other cases can just be set to 'pendingDownload'
|
||||
default: return .pendingDownload
|
||||
}
|
||||
}()
|
||||
let size: CGSize = {
|
||||
|
|
|
@ -49,12 +49,12 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
|
|||
}
|
||||
|
||||
public enum State: Int, Codable, DatabaseValueConvertible {
|
||||
case pending
|
||||
case failedDownload
|
||||
case pendingDownload
|
||||
case downloading
|
||||
case downloaded
|
||||
case uploading
|
||||
case uploaded
|
||||
case failed
|
||||
}
|
||||
|
||||
/// A unique identifier for the attachment
|
||||
|
@ -131,7 +131,7 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
|
|||
id: String = UUID().uuidString,
|
||||
serverId: String? = nil,
|
||||
variant: Variant,
|
||||
state: State = .pending,
|
||||
state: State = .pendingDownload,
|
||||
contentType: String,
|
||||
byteCount: UInt,
|
||||
creationTimestamp: TimeInterval? = nil,
|
||||
|
@ -198,13 +198,13 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR
|
|||
self.id = id
|
||||
self.serverId = nil
|
||||
self.variant = variant
|
||||
self.state = .pending
|
||||
self.state = .uploading
|
||||
self.contentType = contentType
|
||||
self.byteCount = dataSource.dataLength()
|
||||
self.creationTimestamp = nil
|
||||
self.sourceFilename = sourceFilename
|
||||
self.downloadUrl = nil
|
||||
self.localRelativeFilePath = URL(fileURLWithPath: originalFilePath).lastPathComponent
|
||||
self.localRelativeFilePath = Attachment.localRelativeFilePath(from: originalFilePath)
|
||||
self.width = imageSize.map { UInt(floor($0.width)) }
|
||||
self.height = imageSize.map { UInt(floor($0.height)) }
|
||||
self.duration = duration
|
||||
|
@ -351,8 +351,8 @@ extension Attachment {
|
|||
)
|
||||
|
||||
// Assume the data is already correct for "uploading" attachments (and don't override it)
|
||||
case (.uploading, .failed), (.uploaded, .failed): return (self.isValid, self.duration)
|
||||
case (_, .failed): return (false, nil)
|
||||
case (.uploading, .failedDownload), (.uploaded, .failedDownload): return (self.isValid, self.duration)
|
||||
case (_, .failedDownload): return (false, nil)
|
||||
|
||||
default: return (self.isValid, self.duration)
|
||||
}
|
||||
|
@ -407,7 +407,7 @@ extension Attachment {
|
|||
|
||||
return .voiceMessage
|
||||
}()
|
||||
self.state = .pending
|
||||
self.state = .pendingDownload
|
||||
self.contentType = (proto.contentType ?? inferContentType(from: proto.fileName))
|
||||
self.byteCount = UInt(proto.size)
|
||||
self.creationTimestamp = nil
|
||||
|
@ -620,6 +620,13 @@ extension Attachment {
|
|||
)
|
||||
}
|
||||
|
||||
public static func localRelativeFilePath(from originalFilePath: String?) -> String? {
|
||||
guard let originalFilePath: String = originalFilePath else { return nil }
|
||||
|
||||
return originalFilePath
|
||||
.substring(from: (Attachment.attachmentsFolder.count + 1)) // Leading forward slash
|
||||
}
|
||||
|
||||
internal static func imageSize(contentType: String, originalFilePath: String) -> CGSize? {
|
||||
let isVideo: Bool = MIMETypeUtil.isVideo(contentType)
|
||||
let isImage: Bool = MIMETypeUtil.isImage(contentType)
|
||||
|
@ -824,7 +831,7 @@ extension Attachment {
|
|||
return
|
||||
}
|
||||
|
||||
OWSThumbnailService.shared.ensureThumbnail(
|
||||
ThumbnailService.shared.ensureThumbnail(
|
||||
for: self,
|
||||
dimensions: dimensions,
|
||||
success: { loadedThumbnail in success(loadedThumbnail.image, loadedThumbnail.dataSourceBlock) },
|
||||
|
@ -913,8 +920,7 @@ extension Attachment {
|
|||
contentType: OWSMimeTypeImageJpeg,
|
||||
byteCount: UInt(thumbnailData.count),
|
||||
sourceFilename: thumbnailName,
|
||||
localRelativeFilePath: thumbnailPath
|
||||
.substring(from: (Attachment.attachmentsFolder.count + 1)), // Leading forward slash
|
||||
localRelativeFilePath: Attachment.localRelativeFilePath(from: thumbnailPath),
|
||||
width: UInt(thumbnailSize.width),
|
||||
height: UInt(thumbnailSize.height),
|
||||
isValid: true
|
||||
|
@ -940,9 +946,11 @@ extension Attachment {
|
|||
success: (() -> Void)?,
|
||||
failure: ((Error) -> Void)?
|
||||
) {
|
||||
// This can occur if an AttachmnetUploadJob was explicitly created for a message
|
||||
// dependant on the attachment being uploaded (in this case the attachment has
|
||||
// already been uploaded so just succeed)
|
||||
guard state != .uploaded else {
|
||||
SNLog("Attempted to upload an already uploaded/downloaded attachment.")
|
||||
failure?(AttachmentError.invalidStartState)
|
||||
success?()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -123,7 +123,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
|
||||
/// When the interaction was created in milliseconds since epoch
|
||||
///
|
||||
/// **Note:** This value will be `0` if it hasn't been set yet
|
||||
/// **Notes:**
|
||||
/// - This value will be `0` if it hasn't been set yet
|
||||
/// - The code sorts messages using this value
|
||||
/// - This value will ber overwritten by the `serverTimestamp` for open group messages
|
||||
public let timestampMs: Int64
|
||||
|
||||
/// When the interaction was received in milliseconds since epoch
|
||||
|
@ -181,7 +184,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
/// **LinkPreview:** The thumbnails associated to the `LinkPreview`
|
||||
/// **Other:** The files directly attached to the interaction
|
||||
public var attachments: QueryInterfaceRequest<Attachment> {
|
||||
request(for: Interaction.attachments)
|
||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||
|
||||
return request(for: Interaction.attachments)
|
||||
.order(interactionAttachment[.albumIndex])
|
||||
}
|
||||
|
||||
public var quote: QueryInterfaceRequest<Quote> {
|
||||
|
@ -404,63 +410,6 @@ public extension Interaction {
|
|||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Interaction {
|
||||
static func lastInteractionTimestamp(timestampMsKey: String) -> CommonTableExpression<Void> {
|
||||
return CommonTableExpression(
|
||||
named: "lastInteraction",
|
||||
request: Interaction
|
||||
.select(
|
||||
Interaction.Columns.threadId,
|
||||
|
||||
// 'max()' to get the latest
|
||||
max(Interaction.Columns.timestampMs).forKey(timestampMsKey)
|
||||
)
|
||||
.joining(required: Interaction.thread)
|
||||
.group(Interaction.Columns.threadId) // One interaction per thread
|
||||
)
|
||||
}
|
||||
|
||||
static func lastInteraction(
|
||||
lastInteractionKey: String,
|
||||
timestampMsKey: String,
|
||||
threadVariantKey: String,
|
||||
isOpenGroupInvitationKey: String,
|
||||
recipientStatesKey: String
|
||||
) -> CommonTableExpression<Void> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
return CommonTableExpression(
|
||||
named: lastInteractionKey,
|
||||
request: Interaction
|
||||
.select(
|
||||
Interaction.Columns.id,
|
||||
Interaction.Columns.threadId,
|
||||
Interaction.Columns.variant,
|
||||
|
||||
// 'max()' to get the latest
|
||||
max(Interaction.Columns.timestampMs).forKey(timestampMsKey),
|
||||
|
||||
thread[.variant].forKey(threadVariantKey),
|
||||
Interaction.Columns.body,
|
||||
Interaction.Columns.authorId,
|
||||
(linkPreview[.url] != nil).forKey(isOpenGroupInvitationKey)
|
||||
)
|
||||
.joining(required: Interaction.thread.aliased(thread))
|
||||
.joining(
|
||||
optional: Interaction.linkPreview
|
||||
.filter(literal: Interaction.linkPreviewFilterLiteral)
|
||||
.filter(LinkPreview.Columns.variant == LinkPreview.Variant.openGroupInvitation)
|
||||
)
|
||||
.including(all: Interaction.attachments)
|
||||
.including(
|
||||
all: Interaction.recipientStates
|
||||
.select(RecipientState.Columns.state)
|
||||
.forKey(recipientStatesKey)
|
||||
)
|
||||
.group(Interaction.Columns.threadId) // One interaction per thread
|
||||
)
|
||||
}
|
||||
|
||||
/// This will update the `wasRead` state the the interaction
|
||||
///
|
||||
/// - Parameters
|
||||
|
@ -624,19 +573,11 @@ public extension Interaction {
|
|||
func previewText(_ db: Database) -> String {
|
||||
switch variant {
|
||||
case .standardIncoming, .standardOutgoing:
|
||||
struct AttachmentDescriptionInfo: Decodable, FetchableRecord {
|
||||
let id: String
|
||||
let variant: Attachment.Variant
|
||||
let contentType: String
|
||||
let sourceFilename: String?
|
||||
}
|
||||
|
||||
|
||||
return Interaction.previewText(
|
||||
variant: self.variant,
|
||||
body: self.body,
|
||||
attachmentDescriptionInfo: try? attachments
|
||||
.select(.variant, .contentType, .sourceFilename)
|
||||
.select(.id, .variant, .contentType, .sourceFilename)
|
||||
.asRequest(of: Attachment.DescriptionInfo.self)
|
||||
.fetchOne(db),
|
||||
attachmentCount: try? attachments.fetchCount(db),
|
||||
|
|
|
@ -17,10 +17,10 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
|
|||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
|
||||
case name = "displayName"
|
||||
case name
|
||||
case nickname
|
||||
|
||||
case profilePictureUrl = "profilePictureURL"
|
||||
case profilePictureUrl
|
||||
case profilePictureFileName
|
||||
case profileEncryptionKey
|
||||
}
|
||||
|
@ -66,9 +66,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
|
|||
public var description: String {
|
||||
"""
|
||||
Profile(
|
||||
displayName: \(name),
|
||||
name: \(name),
|
||||
profileKey: \(profileEncryptionKey?.keyData.description ?? "null"),
|
||||
profilePictureURL: \(profilePictureUrl ?? "null")
|
||||
profilePictureUrl: \(profilePictureUrl ?? "null")
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
|
|
@ -102,26 +102,26 @@ public struct Quote: Codable, Equatable, FetchableRecord, PersistableRecord, Tab
|
|||
public extension Quote {
|
||||
init?(_ db: Database, proto: SNProtoDataMessage, interactionId: Int64, thread: SessionThread) throws {
|
||||
guard
|
||||
let quote = proto.quote,
|
||||
quote.id != 0,
|
||||
!quote.author.isEmpty
|
||||
let quoteProto = proto.quote,
|
||||
quoteProto.id != 0,
|
||||
!quoteProto.author.isEmpty
|
||||
else { return nil }
|
||||
|
||||
self.interactionId = interactionId
|
||||
self.timestampMs = Int64(quote.id)
|
||||
self.authorId = quote.author
|
||||
self.timestampMs = Int64(quoteProto.id)
|
||||
self.authorId = quoteProto.author
|
||||
|
||||
// Prefer to generate the text snippet locally if available.
|
||||
let quotedInteraction: Interaction? = try? thread
|
||||
.interactions
|
||||
.filter(Interaction.Columns.authorId == quote.author)
|
||||
.filter(Interaction.Columns.timestampMs == Double(quote.id))
|
||||
.filter(Interaction.Columns.authorId == quoteProto.author)
|
||||
.filter(Interaction.Columns.timestampMs == Double(quoteProto.id))
|
||||
.fetchOne(db)
|
||||
|
||||
if let quotedInteraction: Interaction = quotedInteraction, quotedInteraction.body?.isEmpty == false {
|
||||
self.body = quotedInteraction.body
|
||||
}
|
||||
else if let body: String = proto.body, !body.isEmpty {
|
||||
else if let body: String = quoteProto.text, !body.isEmpty {
|
||||
self.body = body
|
||||
}
|
||||
else {
|
||||
|
@ -129,7 +129,7 @@ public extension Quote {
|
|||
}
|
||||
|
||||
// We only use the first attachment
|
||||
if let attachment = quote.attachments.first(where: { $0.thumbnail != nil })?.thumbnail {
|
||||
if let attachment = quoteProto.attachments.first(where: { $0.thumbnail != nil })?.thumbnail {
|
||||
self.attachmentId = try quotedInteraction
|
||||
.map { quotedInteraction -> Attachment? in
|
||||
// If the quotedInteraction has an attachment then try clone it
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
|
||||
extension Storage {
|
||||
|
||||
private static func getClosedGroupEncryptionKeyPairCollection(for groupPublicKey: String) -> String {
|
||||
return "SNClosedGroupEncryptionKeyPairCollection-\(groupPublicKey)"
|
||||
}
|
||||
|
||||
private static let closedGroupPublicKeyCollection = "SNClosedGroupPublicKeyCollection"
|
||||
private static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
|
||||
private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
|
||||
|
||||
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [Box.KeyPair] {
|
||||
var result: [ECKeyPair] = []
|
||||
Storage.read { transaction in
|
||||
result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
|
||||
}
|
||||
return result
|
||||
.map { keyPair -> Box.KeyPair in
|
||||
Box.KeyPair(
|
||||
publicKey: keyPair.publicKey.bytes,
|
||||
secretKey: keyPair.privateKey.bytes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] {
|
||||
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
|
||||
var timestampsAndKeyPairs: [(timestamp: Double, keyPair: ECKeyPair)] = []
|
||||
transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in
|
||||
guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return }
|
||||
timestampsAndKeyPairs.append((timestamp, keyPair))
|
||||
}
|
||||
return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair }
|
||||
}
|
||||
|
||||
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> Box.KeyPair? {
|
||||
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last
|
||||
}
|
||||
|
||||
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> ECKeyPair? {
|
||||
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last
|
||||
}
|
||||
|
||||
public func addClosedGroupEncryptionKeyPair(_ keyPair: Box.KeyPair, for groupPublicKey: String, using transaction: Any) {
|
||||
let ecKeyPair: ECKeyPair = try! ECKeyPair(
|
||||
publicKeyData: Data(keyPair.publicKey),
|
||||
privateKeyData: Data(keyPair.secretKey)
|
||||
)
|
||||
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
|
||||
let timestamp = String(Date().timeIntervalSince1970)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(ecKeyPair, forKey: timestamp, inCollection: collection)
|
||||
}
|
||||
|
||||
public func removeAllClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: Any) {
|
||||
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: collection)
|
||||
}
|
||||
|
||||
public func getUserClosedGroupPublicKeys() -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
Storage.read { transaction in
|
||||
result = self.getUserClosedGroupPublicKeys(using: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set<String> {
|
||||
return Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection))
|
||||
}
|
||||
|
||||
public func addClosedGroupPublicKey(_ groupPublicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(groupPublicKey, forKey: groupPublicKey, inCollection: Storage.closedGroupPublicKeyCollection)
|
||||
}
|
||||
|
||||
public func removeClosedGroupPublicKey(_ groupPublicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: groupPublicKey, inCollection: Storage.closedGroupPublicKeyCollection)
|
||||
}
|
||||
|
||||
public func getClosedGroupFormationTimestamp(for groupPublicKey: String) -> UInt64? {
|
||||
var result: UInt64?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: groupPublicKey, inCollection: Storage.closedGroupFormationTimestampCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setClosedGroupFormationTimestamp(to timestamp: UInt64, for groupPublicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(timestamp, forKey: groupPublicKey, inCollection: Storage.closedGroupFormationTimestampCollection)
|
||||
}
|
||||
|
||||
public func getZombieMembers(for groupPublicKey: String) -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
Storage.read { transaction in
|
||||
if let zombies = transaction.object(forKey: groupPublicKey, inCollection: Storage.closedGroupZombieMembersCollection) as? Set<String> {
|
||||
result = zombies
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setZombieMembers(for groupPublicKey: String, to zombies: Set<String>, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(zombies, forKey: groupPublicKey, inCollection: Storage.closedGroupZombieMembersCollection)
|
||||
}
|
||||
|
||||
public func isClosedGroup(_ publicKey: String) -> Bool {
|
||||
getUserClosedGroupPublicKeys().contains(publicKey)
|
||||
}
|
||||
|
||||
public func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool {
|
||||
getUserClosedGroupPublicKeys(using: transaction).contains(publicKey)
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
public func persist(_ job: Job, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(job, forKey: job.id!, inCollection: type(of: job).collection)
|
||||
}
|
||||
|
||||
public func markJobAsSucceeded(_ job: Job, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: job.id!, inCollection: type(of: job).collection)
|
||||
}
|
||||
|
||||
public func markJobAsFailed(_ job: Job, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: job.id!, inCollection: type(of: job).collection)
|
||||
}
|
||||
|
||||
public func getAllPendingJobs(of type: Job.Type) -> [Job] {
|
||||
var result: [Job] = []
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateRows(inCollection: type.collection) { key, object, _, x in
|
||||
guard let job = object as? Job else { return }
|
||||
result.append(job)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func cancelAllPendingJobs(of type: Job.Type, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.removeAllObjects(inCollection: type.collection)
|
||||
}
|
||||
|
||||
@objc(cancelPendingMessageSendJobIfNeededForMessage:using:)
|
||||
public func cancelPendingMessageSendJobIfNeeded(for tsMessageTimestamp: UInt64, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
var attachmentUploadJobKeys: [String] = []
|
||||
transaction.enumerateRows(inCollection: AttachmentUploadJob.collection) { key, object, _, _ in
|
||||
guard let job = object as? AttachmentUploadJob, job.message.sentTimestamp == tsMessageTimestamp else { return }
|
||||
attachmentUploadJobKeys.append(key)
|
||||
}
|
||||
var messageSendJobKeys: [String] = []
|
||||
transaction.enumerateRows(inCollection: MessageSendJob.collection) { key, object, _, _ in
|
||||
guard let job = object as? MessageSendJob, job.message.sentTimestamp == tsMessageTimestamp else { return }
|
||||
messageSendJobKeys.append(key)
|
||||
}
|
||||
transaction.removeObjects(forKeys: attachmentUploadJobKeys, inCollection: AttachmentUploadJob.collection)
|
||||
transaction.removeObjects(forKeys: messageSendJobKeys, inCollection: MessageSendJob.collection)
|
||||
}
|
||||
|
||||
@objc public func cancelPendingMessageSendJobs(for threadID: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
var attachmentUploadJobKeys: [String] = []
|
||||
transaction.enumerateRows(inCollection: AttachmentUploadJob.collection) { key, object, _, _ in
|
||||
guard let job = object as? AttachmentUploadJob, job.threadID == threadID else { return }
|
||||
attachmentUploadJobKeys.append(key)
|
||||
}
|
||||
var messageSendJobKeys: [String] = []
|
||||
transaction.enumerateRows(inCollection: MessageSendJob.collection) { key, object, _, _ in
|
||||
guard let job = object as? MessageSendJob, job.message.threadID == threadID else { return }
|
||||
messageSendJobKeys.append(key)
|
||||
}
|
||||
transaction.removeObjects(forKeys: attachmentUploadJobKeys, inCollection: AttachmentUploadJob.collection)
|
||||
transaction.removeObjects(forKeys: messageSendJobKeys, inCollection: MessageSendJob.collection)
|
||||
}
|
||||
|
||||
public func getAttachmentUploadJob(for attachmentID: String) -> AttachmentUploadJob? {
|
||||
var result: [AttachmentUploadJob] = []
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateRows(inCollection: AttachmentUploadJob.collection) { _, object, _, _ in
|
||||
guard let job = object as? AttachmentUploadJob, job.attachmentID == attachmentID else { return }
|
||||
result.append(job)
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
assert(result.isEmpty || result.count == 1)
|
||||
#endif
|
||||
return result.first
|
||||
}
|
||||
|
||||
public func getAttachmentDownloadJobs(for threadID: String) -> [AttachmentDownloadJob] {
|
||||
var result: [AttachmentDownloadJob] = []
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateRows(inCollection: AttachmentDownloadJob.collection) { _, object, _, _ in
|
||||
guard let job = object as? AttachmentDownloadJob, job.threadID == threadID else { return }
|
||||
result.append(job)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func resumeAttachmentDownloadJobsIfNeeded(for threadID: String) {
|
||||
let jobs = getAttachmentDownloadJobs(for: threadID)
|
||||
jobs.forEach { job in
|
||||
job.delegate = JobQueue.shared
|
||||
job.isDeferred = false
|
||||
job.execute()
|
||||
}
|
||||
}
|
||||
|
||||
public func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? {
|
||||
var result: MessageSendJob?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: messageSendJobID, inCollection: MessageSendJob.collection) as? MessageSendJob
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func resumeMessageSendJobIfNeeded(_ messageSendJobID: String) {
|
||||
guard let job = getMessageSendJob(for: messageSendJobID) else { return }
|
||||
job.delegate = JobQueue.shared
|
||||
job.execute()
|
||||
}
|
||||
|
||||
public func isJobCanceled(_ job: Job) -> Bool {
|
||||
var result = true
|
||||
Storage.read { transaction in
|
||||
result = !transaction.hasObject(forKey: job.id!, inCollection: type(of: job).collection)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
extension Storage {
|
||||
|
||||
/// Returns the ID of the thread.
|
||||
public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
var threadOrNil: TSThread?
|
||||
if let openGroupID = openGroupID {
|
||||
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID),
|
||||
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) {
|
||||
threadOrNil = thread
|
||||
}
|
||||
} else if let groupPublicKey = groupPublicKey {
|
||||
guard Storage.shared.isClosedGroup(groupPublicKey) else { return nil }
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
threadOrNil = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction)
|
||||
} else {
|
||||
threadOrNil = TSContactThread.getOrCreateThread(withContactSessionID: publicKey, transaction: transaction)
|
||||
}
|
||||
return threadOrNil?.uniqueId
|
||||
}
|
||||
|
||||
/// Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||
public func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
guard let threadID = getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: groupPublicKey, openGroupID: openGroupID, using: transaction),
|
||||
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return nil }
|
||||
let tsMessage: TSMessage
|
||||
if message.sender == getUserHexEncodedPublicKey() {
|
||||
if TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil { return nil }
|
||||
let tsOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
var recipients: [String] = []
|
||||
if let syncTarget = message.syncTarget {
|
||||
recipients.append(syncTarget)
|
||||
} else if let thread = thread as? TSGroupThread {
|
||||
if thread.isClosedGroup { recipients = thread.groupModel.groupMemberIds }
|
||||
else { recipients.append(LKGroupUtilities.getDecodedGroupID(thread.groupModel.groupId)) }
|
||||
}
|
||||
recipients.forEach { recipient in
|
||||
tsOutgoingMessage.update(withSentRecipient: recipient, wasSentByUD: true, transaction: transaction)
|
||||
}
|
||||
tsMessage = tsOutgoingMessage
|
||||
} else {
|
||||
if TSIncomingMessage.find(withAuthorId: message.sender!, timestamp: message.sentTimestamp!, transaction: transaction) != nil { return nil }
|
||||
tsMessage = TSIncomingMessage.from(message, quotedMessage: quotedMessage, linkPreview: linkPreview, associatedWith: thread)
|
||||
}
|
||||
tsMessage.save(with: transaction)
|
||||
tsMessage.attachments(with: transaction).forEach { attachment in
|
||||
attachment.albumMessageId = tsMessage.uniqueId!
|
||||
attachment.save(with: transaction)
|
||||
}
|
||||
return tsMessage.uniqueId!
|
||||
}
|
||||
|
||||
/// Returns the IDs of the saved attachments.
|
||||
public func persist(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] {
|
||||
return attachments.map { attachment in
|
||||
let tsAttachment = TSAttachmentPointer.from(attachment)
|
||||
tsAttachment.save(with: transaction as! YapDatabaseReadWriteTransaction)
|
||||
return tsAttachment.uniqueId!
|
||||
}
|
||||
}
|
||||
|
||||
/// Also touches the associated message.
|
||||
public func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsMessageID: String, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
// Workaround for some YapDatabase funkiness where pointer at this point can actually be a TSAttachmentStream
|
||||
guard pointer.responds(to: #selector(setter: TSAttachmentPointer.state)) else { return }
|
||||
pointer.state = state
|
||||
pointer.save(with: transaction)
|
||||
guard let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) else { return }
|
||||
MessageInvalidator.invalidate(tsMessage, with: transaction)
|
||||
}
|
||||
|
||||
/// Also touches the associated message.
|
||||
public func persist(_ stream: TSAttachmentStream, associatedWith tsMessageID: String, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
stream.save(with: transaction)
|
||||
guard let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) else { return }
|
||||
MessageInvalidator.invalidate(tsMessage, with: transaction)
|
||||
}
|
||||
|
||||
private static let receivedMessageTimestampsCollection = "ReceivedMessageTimestampsCollection"
|
||||
|
||||
public func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] {
|
||||
var result: [UInt64] = []
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
transaction.enumerateRows(inCollection: Storage.receivedMessageTimestampsCollection) { _, object, _, _ in
|
||||
guard let timestamps = object as? [UInt64] else { return }
|
||||
result = timestamps
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func removeReceivedMessageTimestamps(_ timestamps: Set<UInt64>, using transaction: Any) {
|
||||
var receivedMessageTimestamps = getReceivedMessageTimestamps(using: transaction)
|
||||
timestamps.forEach { timestamp in
|
||||
guard let index = receivedMessageTimestamps.firstIndex(of: timestamp) else { return }
|
||||
receivedMessageTimestamps.remove(at: index)
|
||||
}
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection)
|
||||
}
|
||||
|
||||
public func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) {
|
||||
var receivedMessageTimestamps = getReceivedMessageTimestamps(using: transaction)
|
||||
// TODO: Do we need to sort the timestamps here?
|
||||
if receivedMessageTimestamps.count > 1000 { receivedMessageTimestamps.remove(at: 0) } // Limit the size of the collection to 1000
|
||||
receivedMessageTimestamps.append(timestamp)
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,8 +101,10 @@ public enum AttachmentDownloadJob: JobExecutor {
|
|||
.with(
|
||||
state: .downloaded,
|
||||
creationTimestamp: Date().timeIntervalSince1970,
|
||||
localRelativeFilePath: attachment.originalFilePath?
|
||||
.substring(from: (Attachment.attachmentsFolder.count + 1)) // Leading forward slash
|
||||
localRelativeFilePath: (
|
||||
attachment.localRelativeFilePath ??
|
||||
Attachment.localRelativeFilePath(from: attachment.originalFilePath)
|
||||
)
|
||||
)
|
||||
.saved(db)
|
||||
}
|
||||
|
@ -121,7 +123,7 @@ public enum AttachmentDownloadJob: JobExecutor {
|
|||
/// `isValid` and `duration` values based on the downloaded data and the state
|
||||
GRDBStorage.shared.write { db in
|
||||
_ = try attachment
|
||||
.with(state: .failed)
|
||||
.with(state: .failedDownload)
|
||||
.saved(db)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ public enum FailedAttachmentDownloadsJob: JobExecutor {
|
|||
GRDBStorage.shared.write { db in
|
||||
let changeCount: Int = try Attachment
|
||||
.filter(Attachment.Columns.state == Attachment.State.downloading)
|
||||
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failed))
|
||||
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedDownload))
|
||||
|
||||
Logger.debug("Marked \(changeCount) attachments as failed")
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ extension GarbageCollectionJob {
|
|||
case expiredControlMessageProcessRecords
|
||||
case threadTypingIndicators
|
||||
case orphanedAttachmentFiles
|
||||
case orphanedProfileAvatars
|
||||
}
|
||||
|
||||
public struct Details: Codable {
|
||||
|
|
|
@ -47,7 +47,7 @@ public enum MessageSendJob: JobExecutor {
|
|||
|
||||
// If there were failed attachments then this job should fail (can't send a
|
||||
// message which has associated attachments if the attachments fail to upload)
|
||||
guard !allAttachmentStateInfo.contains(where: { $0.state == .failed }) else {
|
||||
guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else {
|
||||
return (true, false)
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ public enum MessageSendJob: JobExecutor {
|
|||
// but not on the message recipients device - both LinkPreview and Quote can
|
||||
// have this case)
|
||||
try allAttachmentStateInfo
|
||||
.filter { $0.state == .pending || $0.state == .downloaded }
|
||||
.filter { $0.state == .uploading || $0.state == .downloaded }
|
||||
.compactMap { stateInfo in
|
||||
JobRunner
|
||||
.insert(
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
public enum OWSThumbnailError: Error {
|
||||
case failure(description: String)
|
||||
case assertionFailure(description: String)
|
||||
case externalError(description: String, underlyingError:Error)
|
||||
}
|
||||
|
||||
@objc public class OWSLoadedThumbnail: NSObject {
|
||||
public typealias DataSourceBlock = () throws -> Data
|
||||
|
||||
@objc
|
||||
public let image: UIImage
|
||||
let dataSourceBlock: DataSourceBlock
|
||||
|
||||
@objc
|
||||
public init(image: UIImage, filePath: String) {
|
||||
self.image = image
|
||||
self.dataSourceBlock = {
|
||||
return try Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(image: UIImage, data: Data) {
|
||||
self.image = image
|
||||
self.dataSourceBlock = {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func data() throws -> Data {
|
||||
return try dataSourceBlock()
|
||||
}
|
||||
}
|
||||
|
||||
private struct OWSThumbnailRequest {
|
||||
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
|
||||
public typealias FailureBlock = (Error) -> Void
|
||||
|
||||
let attachment: Attachment
|
||||
let dimensions: UInt
|
||||
let success: SuccessBlock
|
||||
let failure: FailureBlock
|
||||
}
|
||||
|
||||
@objc public class OWSThumbnailService: NSObject {
|
||||
|
||||
// MARK: - Singleton class
|
||||
|
||||
@objc(shared)
|
||||
public static let shared = OWSThumbnailService()
|
||||
|
||||
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
|
||||
public typealias FailureBlock = (Error) -> Void
|
||||
|
||||
private let serialQueue = DispatchQueue(label: "OWSThumbnailService")
|
||||
|
||||
// This property should only be accessed on the serialQueue.
|
||||
//
|
||||
// We want to process requests in _reverse_ order in which they
|
||||
// arrive so that we prioritize the most recent view state.
|
||||
private var thumbnailRequestStack = [OWSThumbnailRequest]()
|
||||
|
||||
private func canThumbnailAttachment(attachment: Attachment) -> Bool {
|
||||
return attachment.isImage || attachment.isAnimated || attachment.isVideo
|
||||
}
|
||||
|
||||
// success and failure will be called async _off_ the main thread.
|
||||
@objc
|
||||
public func ensureThumbnail(forAttachment attachment: TSAttachmentStream,
|
||||
thumbnailDimensionPoints: UInt,
|
||||
success: @escaping SuccessBlock,
|
||||
failure: @escaping FailureBlock) {
|
||||
serialQueue.async {
|
||||
let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure)
|
||||
self.thumbnailRequestStack.append(thumbnailRequest)
|
||||
|
||||
public func ensureThumbnail(
|
||||
for attachment: Attachment,
|
||||
dimensions: UInt,
|
||||
success: @escaping SuccessBlock,
|
||||
failure: @escaping FailureBlock
|
||||
) {
|
||||
serialQueue.async {
|
||||
self.thumbnailRequestStack.append(
|
||||
OWSThumbnailRequest(
|
||||
attachment: attachment,
|
||||
dimensions: dimensions,
|
||||
success: success,
|
||||
failure: failure
|
||||
)
|
||||
)
|
||||
|
||||
self.processNextRequestSync()
|
||||
}
|
||||
}
|
||||
|
||||
private func processNextRequestAsync() {
|
||||
serialQueue.async {
|
||||
self.processNextRequestSync()
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called on the serialQueue.
|
||||
private func processNextRequestSync() {
|
||||
guard let thumbnailRequest = thumbnailRequestStack.popLast() else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
|
||||
DispatchQueue.global().async {
|
||||
thumbnailRequest.success(loadedThumbnail)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.global().async {
|
||||
thumbnailRequest.failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called on the serialQueue.
|
||||
//
|
||||
// It should be safe to assume that an attachment will never end up with two thumbnails of
|
||||
// the same size since:
|
||||
//
|
||||
// * Thumbnails are only added by this method.
|
||||
// * This method checks for an existing thumbnail using the same connection.
|
||||
// * This method is performed on the serial queue.
|
||||
private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail {
|
||||
let attachment = thumbnailRequest.attachment
|
||||
guard canThumbnailAttachment(attachment: attachment) else {
|
||||
throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.")
|
||||
}
|
||||
let thumbnailPath = attachment.thumbnailPath(for: thumbnailRequest.dimensions)
|
||||
if FileManager.default.fileExists(atPath: thumbnailPath) {
|
||||
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
|
||||
throw OWSThumbnailError.failure(description: "Could not load thumbnail.")
|
||||
}
|
||||
return OWSLoadedThumbnail(image: image, filePath: thumbnailPath)
|
||||
}
|
||||
|
||||
let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
|
||||
guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
|
||||
throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
|
||||
}
|
||||
guard let originalFilePath = attachment.originalFilePath else {
|
||||
throw OWSThumbnailError.failure(description: "Missing original file path.")
|
||||
}
|
||||
let maxDimension = CGFloat(thumbnailRequest.dimensions)
|
||||
let thumbnailImage: UIImage
|
||||
if attachment.isImage || attachment.isAnimated {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
} else if attachment.isVideo {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
} else {
|
||||
throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.")
|
||||
}
|
||||
guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
|
||||
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||||
}
|
||||
do {
|
||||
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic)
|
||||
} catch let error as NSError {
|
||||
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
|
||||
}
|
||||
OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
|
||||
return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public class ThumbnailService {
|
||||
// MARK: - Singleton class
|
||||
|
||||
public static let shared: ThumbnailService = ThumbnailService()
|
||||
|
||||
public typealias SuccessBlock = (LoadedThumbnail) -> Void
|
||||
public typealias FailureBlock = (Error) -> Void
|
||||
|
||||
private let serialQueue = DispatchQueue(label: "ThumbnailService")
|
||||
|
||||
// This property should only be accessed on the serialQueue.
|
||||
//
|
||||
// We want to process requests in _reverse_ order in which they
|
||||
// arrive so that we prioritize the most recent view state.
|
||||
private var requestStack = [Request]()
|
||||
|
||||
private func canThumbnailAttachment(attachment: Attachment) -> Bool {
|
||||
return attachment.isImage || attachment.isAnimated || attachment.isVideo
|
||||
}
|
||||
|
||||
public func ensureThumbnail(
|
||||
for attachment: Attachment,
|
||||
dimensions: UInt,
|
||||
success: @escaping SuccessBlock,
|
||||
failure: @escaping FailureBlock
|
||||
) {
|
||||
serialQueue.async {
|
||||
self.requestStack.append(
|
||||
Request(
|
||||
attachment: attachment,
|
||||
dimensions: dimensions,
|
||||
success: success,
|
||||
failure: failure
|
||||
)
|
||||
)
|
||||
|
||||
self.processNextRequestSync()
|
||||
}
|
||||
}
|
||||
|
||||
private func processNextRequestAsync() {
|
||||
serialQueue.async {
|
||||
self.processNextRequestSync()
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called on the serialQueue.
|
||||
private func processNextRequestSync() {
|
||||
guard let thumbnailRequest = requestStack.popLast() else { return }
|
||||
|
||||
do {
|
||||
let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
|
||||
DispatchQueue.global().async {
|
||||
thumbnailRequest.success(loadedThumbnail)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
DispatchQueue.global().async {
|
||||
thumbnailRequest.failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called on the serialQueue.
|
||||
//
|
||||
// It should be safe to assume that an attachment will never end up with two thumbnails of
|
||||
// the same size since:
|
||||
//
|
||||
// * Thumbnails are only added by this method.
|
||||
// * This method checks for an existing thumbnail using the same connection.
|
||||
// * This method is performed on the serial queue.
|
||||
private func process(thumbnailRequest: Request) throws -> LoadedThumbnail {
|
||||
let attachment = thumbnailRequest.attachment
|
||||
|
||||
guard canThumbnailAttachment(attachment: attachment) else {
|
||||
throw ThumbnailError.failure(description: "Cannot thumbnail attachment.")
|
||||
}
|
||||
|
||||
let thumbnailPath = attachment.thumbnailPath(for: thumbnailRequest.dimensions)
|
||||
|
||||
if FileManager.default.fileExists(atPath: thumbnailPath) {
|
||||
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
|
||||
throw ThumbnailError.failure(description: "Could not load thumbnail.")
|
||||
}
|
||||
return LoadedThumbnail(image: image, filePath: thumbnailPath)
|
||||
}
|
||||
|
||||
let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
|
||||
|
||||
guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
|
||||
throw ThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
|
||||
}
|
||||
guard let originalFilePath = attachment.originalFilePath else {
|
||||
throw ThumbnailError.failure(description: "Missing original file path.")
|
||||
}
|
||||
|
||||
let maxDimension = CGFloat(thumbnailRequest.dimensions)
|
||||
let thumbnailImage: UIImage
|
||||
|
||||
if attachment.isImage || attachment.isAnimated {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
}
|
||||
else if attachment.isVideo {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
}
|
||||
else {
|
||||
throw ThumbnailError.assertionFailure(description: "Invalid attachment type.")
|
||||
}
|
||||
|
||||
guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
|
||||
throw ThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||||
}
|
||||
|
||||
do {
|
||||
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic)
|
||||
}
|
||||
catch let error as NSError {
|
||||
throw ThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
|
||||
}
|
||||
|
||||
OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
|
||||
|
||||
return LoadedThumbnail(image: thumbnailImage, data: thumbnailData)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ThumbnailService {
|
||||
enum ThumbnailError: Error {
|
||||
case failure(description: String)
|
||||
case assertionFailure(description: String)
|
||||
case externalError(description: String, underlyingError: Error)
|
||||
}
|
||||
|
||||
struct LoadedThumbnail {
|
||||
public typealias DataSourceBlock = () throws -> Data
|
||||
|
||||
public let image: UIImage
|
||||
public let dataSourceBlock: DataSourceBlock
|
||||
|
||||
public init(image: UIImage, filePath: String) {
|
||||
self.image = image
|
||||
self.dataSourceBlock = {
|
||||
return try Data(contentsOf: URL(fileURLWithPath: filePath))
|
||||
}
|
||||
}
|
||||
|
||||
public init(image: UIImage, data: Data) {
|
||||
self.image = image
|
||||
self.dataSourceBlock = {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
public func data() throws -> Data {
|
||||
return try dataSourceBlock()
|
||||
}
|
||||
}
|
||||
|
||||
private struct Request {
|
||||
public typealias SuccessBlock = (LoadedThumbnail) -> Void
|
||||
public typealias FailureBlock = (Error) -> Void
|
||||
|
||||
let attachment: Attachment
|
||||
let dimensions: UInt
|
||||
let success: SuccessBlock
|
||||
let failure: FailureBlock
|
||||
}
|
||||
}
|
|
@ -392,7 +392,7 @@ extension MessageReceiver {
|
|||
let messageSentTimestamp: TimeInterval = (TimeInterval(message.sentTimestamp ?? 0) / 1000)
|
||||
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
|
||||
|
||||
// Update profile if needed (want to do this regarless of whether the message exists or
|
||||
// Update profile if needed (want to do this regardless of whether the message exists or
|
||||
// not to ensure the profile info gets sync between a users devices at every chance)
|
||||
if let profile = message.profile {
|
||||
var contactProfileKey: OWSAES256Key? = nil
|
||||
|
|
|
@ -116,7 +116,7 @@ extension MessageSender {
|
|||
}()
|
||||
let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId)
|
||||
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
|
||||
.stateInfo(interactionId: interactionId, state: .pending)
|
||||
.stateInfo(interactionId: interactionId, state: .uploading)
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import SessionSnodeKit
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
|
||||
@objc(LKPushNotificationAPI)
|
||||
public final class PushNotificationAPI : NSObject {
|
||||
|
@ -44,10 +47,20 @@ public final class PushNotificationAPI : NSObject {
|
|||
promise.catch2 { error in
|
||||
SNLog("Couldn't unregister from push notifications.")
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
Storage.shared.getUserClosedGroupPublicKeys().forEach { closedGroupPublicKey in
|
||||
performOperation(.unsubscribe, for: closedGroupPublicKey, publicKey: getUserHexEncodedPublicKey())
|
||||
|
||||
// Unsubscribe from all closed groups (including ones the user is no longer a member of, just in case)
|
||||
GRDBStorage.shared.read { db in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.asRequest(of: String.self)
|
||||
.fetchAll(db)
|
||||
.forEach { closedGroupPublicKey in
|
||||
performOperation(.unsubscribe, for: closedGroupPublicKey, publicKey: userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
|
@ -86,10 +99,22 @@ public final class PushNotificationAPI : NSObject {
|
|||
promise.catch2 { error in
|
||||
SNLog("Couldn't register device token.")
|
||||
}
|
||||
|
||||
// Subscribe to all closed groups
|
||||
Storage.shared.getUserClosedGroupPublicKeys().forEach { closedGroupPublicKey in
|
||||
performOperation(.subscribe, for: closedGroupPublicKey, publicKey: publicKey)
|
||||
GRDBStorage.shared.read { db in
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.joining(
|
||||
required: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
|
||||
)
|
||||
.asRequest(of: String.self)
|
||||
.fetchAll(db)
|
||||
.forEach { closedGroupPublicKey in
|
||||
performOperation(.subscribe, for: closedGroupPublicKey, publicKey: publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionMessagingKit
|
||||
|
||||
fileprivate typealias ViewModel = ConversationCell.ViewModel
|
||||
|
||||
public enum ConversationCell {}
|
||||
|
||||
// MARK: - ViewModel
|
||||
|
||||
extension ConversationCell {
|
||||
/// This type is used to populate the `ConversationCell` in the `HomeVC`, `MessageRequestsViewController` and the
|
||||
/// `GlobalSearchViewController`, it has a number of query methods which can be used to retrieve the relevant data for each
|
||||
|
@ -53,7 +56,7 @@ extension ConversationCell {
|
|||
public static let closedGroupProfileBackFallbackString: String = CodingKeys.closedGroupProfileBackFallback.stringValue
|
||||
public static let interactionAttachmentDescriptionInfoString: String = CodingKeys.interactionAttachmentDescriptionInfo.stringValue
|
||||
|
||||
public var differenceIdentifier: ViewModel { self }
|
||||
public var differenceIdentifier: ViewModel { self } // TODO: Confirm this does what we want (ie. update on any data change)
|
||||
|
||||
public let threadId: String
|
||||
public let threadVariant: SessionThread.Variant
|
||||
|
@ -243,6 +246,7 @@ public extension ConversationCell.ViewModel {
|
|||
/// Explicitly set default values for the fields ignored for search results
|
||||
let numColumnsBeforeProfiles: Int = 11
|
||||
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 10
|
||||
// TODO: Some testing around the subqueries in the joins to see if they impact performance ('Simulator1' device takes ~125ms to complete this query)
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||
|
@ -391,6 +395,7 @@ public extension ConversationCell.ViewModel {
|
|||
GROUP BY \(thread[.id])
|
||||
ORDER BY \(ordering)
|
||||
"""
|
||||
|
||||
return request.adapted { db in
|
||||
let adapters = try splittingRowAdapters(columnCounts: [
|
||||
numColumnsBeforeProfiles,
|
||||
|
@ -523,7 +528,7 @@ public extension ConversationCell.ViewModel {
|
|||
/// parse and might throw
|
||||
///
|
||||
/// Explicitly set default values for the fields ignored for search results
|
||||
let numColumnsBeforeProfiles: Int = 11
|
||||
let numColumnsBeforeProfiles: Int = 5
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||
|
@ -680,14 +685,9 @@ public extension ConversationCell.ViewModel {
|
|||
|
||||
"""
|
||||
|
||||
// Contact thread nickname searching (ignoring note to self - handled separately)
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
// MARK: --Contact Threads
|
||||
let contactQueryCommonJoinFilterGroup: SQL = """
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND
|
||||
\(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false
|
||||
|
@ -706,6 +706,16 @@ public extension ConversationCell.ViewModel {
|
|||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
|
||||
// Contact thread nickname searching (ignoring note to self - handled separately)
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND
|
||||
\(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
"""
|
||||
sqlQuery += contactQueryCommonJoinFilterGroup
|
||||
|
||||
// Contact thread name searching (ignoring note to self - handled separately)
|
||||
sqlQuery += """
|
||||
|
||||
|
@ -714,26 +724,61 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND
|
||||
\(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false
|
||||
LEFT JOIN \(ClosedGroup.self) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
"""
|
||||
sqlQuery += contactQueryCommonJoinFilterGroup
|
||||
|
||||
// MARK: --Closed Group Threads
|
||||
let closedGroupQueryCommonJoinFilterGroup: SQL = """
|
||||
JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(thread[.id])
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
'' AS \(ViewModel.threadMemberNamesKey)
|
||||
GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
) AS \(groupMemberInfoLiteral) ON false
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId])
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MAX(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey)
|
||||
)
|
||||
|
||||
WHERE
|
||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)"))
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
|
||||
WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
|
||||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
|
||||
|
@ -745,58 +790,12 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(thread[.id])
|
||||
)
|
||||
JOIN \(closedGroupFullTextSearch) ON (
|
||||
\(closedGroupFullTextSearch).rowid = \(closedGroupLiteral).rowid AND
|
||||
\(closedGroupFullTextSearch).\(closedGroupNameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId])
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MAX(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey)
|
||||
)
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
|
||||
WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
|
||||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
sqlQuery += closedGroupQueryCommonJoinFilterGroup
|
||||
|
||||
// Closed group member nickname searching
|
||||
sqlQuery += """
|
||||
|
@ -806,59 +805,13 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(thread[.id])
|
||||
)
|
||||
JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId])
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND
|
||||
\(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId])
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MAX(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey)
|
||||
)
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
|
||||
WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
|
||||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
sqlQuery += closedGroupQueryCommonJoinFilterGroup
|
||||
|
||||
// Closed group member name searching
|
||||
sqlQuery += """
|
||||
|
@ -868,60 +821,15 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(thread[.id])
|
||||
)
|
||||
JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId])
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND
|
||||
\(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)"))
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId])
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MAX(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(groupMember[.profileId]) != \(userPublicKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey)
|
||||
)
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
|
||||
WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
|
||||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
sqlQuery += closedGroupQueryCommonJoinFilterGroup
|
||||
|
||||
// MARK: --Open Group Threads
|
||||
// Open group thread name searching
|
||||
sqlQuery += """
|
||||
|
||||
|
@ -953,17 +861,9 @@ public extension ConversationCell.ViewModel {
|
|||
GROUP BY \(thread[.id])
|
||||
"""
|
||||
|
||||
// Note to self thread searching for 'Note to Self' (need to join an FTS table to
|
||||
// ensure there is a 'rank' column)
|
||||
sqlQuery += """
|
||||
|
||||
UNION ALL
|
||||
|
||||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
// MARK: --Note to Self Thread
|
||||
let noteToSelfQueryCommonJoins: SQL = """
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
LEFT JOIN \(profileFullTextSearch) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false
|
||||
|
@ -975,6 +875,22 @@ public extension ConversationCell.ViewModel {
|
|||
'' AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
) AS \(groupMemberInfoLiteral) ON false
|
||||
"""
|
||||
|
||||
// Note to self thread searching for 'Note to Self' (need to join an FTS table to
|
||||
// ensure there is a 'rank' column)
|
||||
sqlQuery += """
|
||||
|
||||
UNION ALL
|
||||
|
||||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
|
||||
LEFT JOIN \(profileFullTextSearch) ON false
|
||||
"""
|
||||
sqlQuery += noteToSelfQueryCommonJoins
|
||||
sqlQuery += """
|
||||
|
||||
WHERE
|
||||
\(SQL("\(thread[.id]) = \(userPublicKey)")) AND
|
||||
|
@ -989,22 +905,14 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND
|
||||
\(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false
|
||||
LEFT JOIN \(ClosedGroup.self) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
'' AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
) AS \(groupMemberInfoLiteral) ON false
|
||||
"""
|
||||
sqlQuery += noteToSelfQueryCommonJoins
|
||||
sqlQuery += """
|
||||
|
||||
WHERE \(SQL("\(thread[.id]) = \(userPublicKey)"))
|
||||
"""
|
||||
|
@ -1017,22 +925,14 @@ public extension ConversationCell.ViewModel {
|
|||
"""
|
||||
sqlQuery += selectQuery
|
||||
sqlQuery += """
|
||||
JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
|
||||
JOIN \(profileFullTextSearch) ON (
|
||||
\(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND
|
||||
\(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false
|
||||
LEFT JOIN \(ClosedGroup.self) ON false
|
||||
LEFT JOIN \(OpenGroup.self) ON false
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
'' AS \(ViewModel.threadMemberNamesKey)
|
||||
FROM \(GroupMember.self)
|
||||
) AS \(groupMemberInfoLiteral) ON false
|
||||
"""
|
||||
sqlQuery += noteToSelfQueryCommonJoins
|
||||
sqlQuery += """
|
||||
|
||||
WHERE \(SQL("\(thread[.id]) = \(userPublicKey)"))
|
||||
"""
|
||||
|
@ -1090,3 +990,119 @@ public extension ConversationCell.ViewModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Share Extension
|
||||
|
||||
public extension ConversationCell.ViewModel {
|
||||
static func shareQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<ConversationCell.ViewModel>> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
|
||||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
|
||||
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
|
||||
/// the `ViewModel.closedGroupProfileFrontKey` entry below otherwise the query will fail to
|
||||
/// parse and might throw
|
||||
///
|
||||
/// Explicitly set default values for the fields ignored for search results
|
||||
let numColumnsBeforeProfiles: Int = 6
|
||||
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
|
||||
\(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey),
|
||||
|
||||
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey),
|
||||
\(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey),
|
||||
\(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
\(ViewModel.closedGroupProfileFrontKey).*,
|
||||
\(ViewModel.closedGroupProfileBackKey).*,
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).*,
|
||||
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
|
||||
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
|
||||
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT *, MAX(\(interaction[.timestampMs]))
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON (
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MAX(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
|
||||
)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON (
|
||||
\(closedGroup[.threadId]) IS NOT NULL AND
|
||||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)"))
|
||||
)
|
||||
|
||||
WHERE (
|
||||
\(thread[.shouldBeVisible]) = true AND (
|
||||
-- Is not a message request
|
||||
\(SQL("\(thread[.variant]) != \(SessionThread.Variant.contact)")) OR
|
||||
\(SQL("\(thread[.id]) = \(userPublicKey)")) OR
|
||||
\(contact[.isApproved]) = true
|
||||
) AND (
|
||||
-- Only show the 'Note to Self' thread if it has an interaction
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) OR
|
||||
\(interaction[.id]) IS NOT NULL
|
||||
)
|
||||
)
|
||||
|
||||
GROUP BY \(thread[.id])
|
||||
ORDER BY IFNULL(\(interaction[.timestampMs]), \(thread[.creationDateTimestamp])) DESC
|
||||
"""
|
||||
|
||||
return request.adapted { db in
|
||||
let adapters = try splittingRowAdapters(columnCounts: [
|
||||
numColumnsBeforeProfiles,
|
||||
Profile.numberOfSelectedColumns(db),
|
||||
Profile.numberOfSelectedColumns(db),
|
||||
Profile.numberOfSelectedColumns(db),
|
||||
Profile.numberOfSelectedColumns(db)
|
||||
])
|
||||
|
||||
return ScopeAdapter([
|
||||
ViewModel.contactProfileString: adapters[1],
|
||||
ViewModel.closedGroupProfileFrontString: adapters[2],
|
||||
ViewModel.closedGroupProfileBackString: adapters[3],
|
||||
ViewModel.closedGroupProfileBackFallbackString: adapters[4]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ public struct ProfileManager {
|
|||
public static func profileAvatarFilepath(filename: String) -> String {
|
||||
guard !filename.isEmpty else { return "" }
|
||||
|
||||
return URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath())
|
||||
return URL(fileURLWithPath: sharedDataProfileAvatarsDirPath)
|
||||
.appendingPathComponent(filename)
|
||||
.path
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
enum ProofOfWork {
|
||||
|
||||
/// A modified version of [Bitmessage's Proof of Work Implementation](https://bitmessage.org/wiki/Proof_of_work).
|
||||
static func calculate(ttl: UInt64, publicKey: String, data: String) -> (timestamp: UInt64, base64EncodedNonce: String)? {
|
||||
let nonceSize = MemoryLayout<UInt64>.size
|
||||
// Get millisecond timestamp
|
||||
let timestamp = NSDate.millisecondTimestamp()
|
||||
// Construct payload
|
||||
let payloadAsString = String(timestamp) + String(ttl) + publicKey + data
|
||||
let payload = payloadAsString.bytes
|
||||
// Calculate target
|
||||
let numerator = UInt64.max
|
||||
let difficulty = UInt64(1)
|
||||
let totalSize = UInt64(payload.count + nonceSize)
|
||||
let ttlInSeconds = ttl / 1000
|
||||
let denominator = difficulty * (totalSize + (ttlInSeconds * totalSize) / UInt64(UInt16.max))
|
||||
let target = numerator / denominator
|
||||
// Calculate proof of work
|
||||
var value = UInt64.max
|
||||
let payloadHash = payload.sha512()
|
||||
var nonce = UInt64(0)
|
||||
while value > target {
|
||||
nonce = nonce &+ 1
|
||||
let hash = (nonce.bigEndianBytes + payloadHash).sha512()
|
||||
guard let newValue = UInt64(fromBigEndianBytes: [UInt8](hash[0..<nonceSize])) else { return nil }
|
||||
value = newValue
|
||||
}
|
||||
// Encode as base 64
|
||||
let base64EncodedNonce = nonce.bigEndianBytes.toBase64()
|
||||
// Return
|
||||
return (timestamp, base64EncodedNonce)
|
||||
}
|
||||
}
|
|
@ -293,12 +293,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
|
|||
|
||||
private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? {
|
||||
if utiType == (kUTTypeURL as String) {
|
||||
// Share URLs as oversize text messages whose text content is the URL.
|
||||
//
|
||||
// NOTE: SharingThreadPickerViewController will try to unpack them
|
||||
// and send them as normal text messages if possible.
|
||||
let urlString = url.absoluteString
|
||||
return DataSourceValue.dataSource(withOversizeText: urlString)
|
||||
// Share URLs as text messages whose text content is the URL
|
||||
return DataSourceValue.dataSource(withText: url.absoluteString)
|
||||
}
|
||||
else if UTTypeConformsTo(utiType as CFString, kUTTypeText) {
|
||||
// Share text as oversize text messages.
|
||||
|
|
|
@ -87,16 +87,16 @@ final class SimplifiedConversationCell: UITableViewCell {
|
|||
|
||||
// MARK: - Updating
|
||||
|
||||
public func update(with item: ThreadPickerViewModel.Item, currentUserProfile: Profile) {
|
||||
accentLineView.alpha = (item.isBlocked ? 1 : 0)
|
||||
public func update(with cellViewModel: ConversationCell.ViewModel) {
|
||||
accentLineView.alpha = (cellViewModel.threadIsBlocked == true ? 1 : 0)
|
||||
profilePictureView.update(
|
||||
publicKey: item.id,
|
||||
profile: item.profile(currentUserProfile: currentUserProfile),
|
||||
additionalProfile: item.additionalProfile,
|
||||
threadVariant: item.variant,
|
||||
openGroupProfilePicture: item.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (item.variant == .openGroup && item.openGroupProfilePictureData == nil)
|
||||
publicKey: cellViewModel.threadId,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: cellViewModel.additionalProfile,
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
||||
)
|
||||
displayNameLabel.text = item.displayName(currentUserProfile: currentUserProfile)
|
||||
displayNameLabel.text = cellViewModel.displayName
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
)
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedViewData: ThreadPickerViewModel.ViewData) {
|
||||
private func handleUpdates(_ updatedViewData: [ConversationCell.ViewModel]) {
|
||||
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
||||
// in from a frame of CGRect.zero)
|
||||
guard hasLoadedInitialData else {
|
||||
|
@ -163,31 +163,23 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
// Reload the table content (animate changes after the first load)
|
||||
tableView.reload(
|
||||
using: StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items),
|
||||
using: StagedChangeset(source: viewModel.viewData, target: updatedViewData),
|
||||
with: .automatic,
|
||||
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
||||
) { [weak self] updatedData in
|
||||
self?.viewModel.updateData(
|
||||
ThreadPickerViewModel.ViewData(
|
||||
currentUserProfile: updatedViewData.currentUserProfile,
|
||||
items: updatedData
|
||||
)
|
||||
)
|
||||
self?.viewModel.updateData(updatedData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.viewModel.viewData.items.count
|
||||
return self.viewModel.viewData.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath)
|
||||
cell.update(
|
||||
with: self.viewModel.viewData.items[indexPath.row],
|
||||
currentUserProfile: self.viewModel.viewData.currentUserProfile
|
||||
)
|
||||
cell.update(with: self.viewModel.viewData[indexPath.row])
|
||||
|
||||
return cell
|
||||
}
|
||||
|
@ -200,7 +192,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
guard let attachments: [SignalAttachment] = ShareVC.attachmentPrepPromise?.value else { return }
|
||||
|
||||
let approvalVC: OWSNavigationController = AttachmentApprovalViewController.wrappedInNavController(
|
||||
threadId: self.viewModel.viewData.items[indexPath.row].id,
|
||||
threadId: self.viewModel.viewData[indexPath.row].threadId,
|
||||
attachments: attachments,
|
||||
approvalDelegate: self
|
||||
)
|
||||
|
|
|
@ -7,165 +7,8 @@ import SignalUtilitiesKit
|
|||
import SessionMessagingKit
|
||||
|
||||
public class ThreadPickerViewModel {
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
viewData = ViewData(
|
||||
currentUserProfile: Profile.fetchOrCreateCurrentUser(),
|
||||
items: []
|
||||
)
|
||||
}
|
||||
|
||||
public struct Item: FetchableRecord, Decodable, Equatable, Differentiable {
|
||||
public struct GroupMemberInfo: FetchableRecord, Decodable, Equatable {
|
||||
public let profile: Profile
|
||||
}
|
||||
|
||||
fileprivate static let closedGroupNameKey = CodingKeys.closedGroupName.stringValue
|
||||
fileprivate static let openGroupNameKey = CodingKeys.openGroupName.stringValue
|
||||
fileprivate static let openGroupProfilePictureDataKey = CodingKeys.openGroupProfilePictureData.stringValue
|
||||
fileprivate static let contactProfileKey = CodingKeys.contactProfile.stringValue
|
||||
fileprivate static let closedGroupAvatarProfilesKey = CodingKeys.closedGroupAvatarProfiles.stringValue
|
||||
fileprivate static let contactIsBlockedKey = CodingKeys.contactIsBlocked.stringValue
|
||||
fileprivate static let isNoteToSelfKey = CodingKeys.isNoteToSelf.stringValue
|
||||
|
||||
public var differenceIdentifier: String { id }
|
||||
|
||||
public let id: String
|
||||
public let variant: SessionThread.Variant
|
||||
|
||||
public let closedGroupName: String?
|
||||
public let openGroupName: String?
|
||||
public let openGroupProfilePictureData: Data?
|
||||
private let contactProfile: Profile?
|
||||
private let closedGroupAvatarProfiles: [GroupMemberInfo]?
|
||||
|
||||
/// A flag indicating whether the contact is blocked (will be null for non-contact threads)
|
||||
private let contactIsBlocked: Bool?
|
||||
public let isNoteToSelf: Bool
|
||||
|
||||
public func displayName(currentUserProfile: Profile) -> String {
|
||||
return SessionThread.displayName(
|
||||
threadId: id,
|
||||
variant: variant,
|
||||
closedGroupName: closedGroupName,
|
||||
openGroupName: openGroupName,
|
||||
isNoteToSelf: isNoteToSelf,
|
||||
profile: contactProfile
|
||||
)
|
||||
}
|
||||
|
||||
public func profile(currentUserProfile: Profile) -> Profile? {
|
||||
switch variant {
|
||||
case .contact: return contactProfile
|
||||
case .openGroup: return nil
|
||||
case .closedGroup:
|
||||
// If there is only a single user in the group then we want to use the current user
|
||||
// profile at the back
|
||||
if closedGroupAvatarProfiles?.count == 1 {
|
||||
return currentUserProfile
|
||||
}
|
||||
|
||||
return closedGroupAvatarProfiles?.first?.profile
|
||||
}
|
||||
}
|
||||
|
||||
public var additionalProfile: Profile? {
|
||||
switch variant {
|
||||
case .closedGroup: return closedGroupAvatarProfiles?.last?.profile
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// A flag indicating whether the thread is blocked (only contact threads can be blocked)
|
||||
public var isBlocked: Bool {
|
||||
return (contactIsBlocked == true)
|
||||
}
|
||||
|
||||
// MARK: - Query
|
||||
|
||||
public static func query(userPublicKey: String) -> QueryInterfaceRequest<Item> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let lastInteraction: TableAlias = TableAlias()
|
||||
|
||||
let lastInteractionTimestampExpression: CommonTableExpression = Interaction.lastInteractionTimestamp(
|
||||
timestampMsKey: Interaction.Columns.timestampMs.stringValue
|
||||
)
|
||||
// FIXME: Exclude unwritable opengroups
|
||||
return SessionThread
|
||||
.select(
|
||||
thread[.id],
|
||||
thread[.variant],
|
||||
thread[.creationDateTimestamp],
|
||||
|
||||
closedGroup[.name].forKey(Item.closedGroupNameKey),
|
||||
openGroup[.name].forKey(Item.openGroupNameKey),
|
||||
openGroup[.imageData].forKey(Item.openGroupProfilePictureDataKey),
|
||||
|
||||
contact[.isBlocked].forKey(Item.contactIsBlockedKey),
|
||||
SessionThread.isNoteToSelf(userPublicKey: userPublicKey).forKey(Item.isNoteToSelfKey)
|
||||
)
|
||||
.filter(SessionThread.Columns.shouldBeVisible == true)
|
||||
.filter(SessionThread.isNotMessageRequest(userPublicKey: userPublicKey))
|
||||
.filter(
|
||||
// Only show the Note to Self if it has an interaction
|
||||
SessionThread.Columns.id != userPublicKey ||
|
||||
lastInteraction[Interaction.Columns.timestampMs] != nil
|
||||
)
|
||||
.aliased(thread)
|
||||
.joining(
|
||||
optional: SessionThread.contact
|
||||
.aliased(contact)
|
||||
.including(
|
||||
optional: Contact.profile
|
||||
.forKey(Item.contactProfileKey)
|
||||
)
|
||||
)
|
||||
.joining(
|
||||
optional: SessionThread.closedGroup
|
||||
.aliased(closedGroup)
|
||||
.including(
|
||||
all: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
|
||||
.filter(GroupMember.Columns.profileId != userPublicKey)
|
||||
.order(GroupMember.Columns.profileId) // Sort to provide a level of stability
|
||||
.limit(2)
|
||||
.including(required: GroupMember.profile)
|
||||
.forKey(Item.closedGroupAvatarProfilesKey)
|
||||
)
|
||||
)
|
||||
.joining(optional: SessionThread.openGroup.aliased(openGroup))
|
||||
.with(lastInteractionTimestampExpression)
|
||||
.including(
|
||||
optional: SessionThread
|
||||
.association(
|
||||
to: lastInteractionTimestampExpression,
|
||||
on: { thread, lastInteraction in
|
||||
thread[SessionThread.Columns.id] == lastInteraction[Interaction.Columns.threadId]
|
||||
}
|
||||
)
|
||||
.aliased(lastInteraction)
|
||||
)
|
||||
.order(
|
||||
(
|
||||
lastInteraction[Interaction.Columns.timestampMs] ??
|
||||
(thread[.creationDateTimestamp] * 1000)
|
||||
).desc
|
||||
)
|
||||
.asRequest(of: Item.self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ViewData: Equatable {
|
||||
let currentUserProfile: Profile
|
||||
let items: [Item]
|
||||
}
|
||||
|
||||
/// This value is the current state of the view
|
||||
public private(set) var viewData: ViewData
|
||||
public private(set) var viewData: [ConversationCell.ViewModel] = []
|
||||
|
||||
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
|
||||
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
|
||||
|
@ -173,20 +16,18 @@ public class ThreadPickerViewModel {
|
|||
/// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static
|
||||
/// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries
|
||||
public lazy var observableViewData = ValueObservation
|
||||
.trackingConstantRegion { db -> ViewData in
|
||||
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||
return ViewData(
|
||||
currentUserProfile: Profile.fetchOrCreateCurrentUser(db),
|
||||
items: try Item
|
||||
.query(userPublicKey: currentUserProfile.id)
|
||||
.fetchAll(db)
|
||||
)
|
||||
.trackingConstantRegion { db -> [ConversationCell.ViewModel] in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
return try ConversationCell.ViewModel
|
||||
.shareQuery(userPublicKey: userPublicKey)
|
||||
.fetchAll(db)
|
||||
}
|
||||
.removeDuplicates()
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func updateData(_ updatedData: ViewData) {
|
||||
public func updateData(_ updatedData: [ConversationCell.ViewModel]) {
|
||||
self.viewData = updatedData
|
||||
}
|
||||
}
|
||||
|
|
|
@ -432,7 +432,7 @@ public enum OnionRequestAPI {
|
|||
guard let bodyAsData = bodyAsString.data(using: .utf8),
|
||||
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
if let timestamp = body["t"] as? Int64 {
|
||||
let offset = timestamp - Int64(NSDate.millisecondTimestamp())
|
||||
let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.clockOffset = offset
|
||||
}
|
||||
guard 200...299 ~= statusCode else {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSDate (Session)
|
||||
|
||||
+ (uint64_t)millisecondTimestamp;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,16 +0,0 @@
|
|||
#import "NSDate+Timestamp.h"
|
||||
#import <chrono>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation NSDate (Session)
|
||||
|
||||
+ (uint64_t)millisecondTimestamp
|
||||
{
|
||||
return (uint64_t)(std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import SignalCoreKit
|
||||
|
||||
public extension String {
|
||||
func localized() -> String {
|
||||
// If the localized string matches the key provided then the localisation failed
|
||||
let localizedString = NSLocalizedString(self, comment: "")
|
||||
owsAssertDebug(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings")
|
||||
|
||||
return localizedString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import SignalCoreKit
|
||||
|
||||
public extension String {
|
||||
func localized() -> String {
|
||||
// If the localized string matches the key provided then the localisation failed
|
||||
let localizedString = NSLocalizedString(self, comment: "")
|
||||
owsAssertDebug(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings")
|
||||
|
||||
return localizedString
|
||||
}
|
||||
|
||||
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
|
||||
var ranges: [Range<Index>] = []
|
||||
|
||||
while
|
||||
(ranges.last.map({ $0.upperBound < self.endIndex }) ?? true),
|
||||
let range = self.range(
|
||||
of: substring,
|
||||
options: options,
|
||||
range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex,
|
||||
locale: locale
|
||||
)
|
||||
{
|
||||
ranges.append(range)
|
||||
}
|
||||
|
||||
return ranges
|
||||
}
|
||||
}
|
|
@ -41,6 +41,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
+ (nullable DataSource *)dataSourceWithData:(NSData *)data utiType:(NSString *)utiType;
|
||||
|
||||
+ (nullable DataSource *)dataSourceWithText:(NSString *_Nullable)text;
|
||||
|
||||
+ (DataSource *)dataSourceWithSyncMessageData:(NSData *)data;
|
||||
|
||||
+ (DataSource *)emptyDataSource;
|
||||
|
|
|
@ -134,6 +134,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [self dataSourceWithData:data fileExtension:fileExtension];
|
||||
}
|
||||
|
||||
+ (nullable DataSource *)dataSourceWithText:(NSString *_Nullable)text
|
||||
{
|
||||
if (!text) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [text.filterStringForDisplay dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [self dataSourceWithData:data fileExtension:kTextAttachmentFileExtension];
|
||||
}
|
||||
|
||||
+ (DataSource *)dataSourceWithSyncMessageData:(NSData *)data
|
||||
{
|
||||
return [self dataSourceWithData:data fileExtension:kSyncMessageFileExtension];
|
||||
|
|
|
@ -15,7 +15,7 @@ extern NSString *const OWSMimeTypeImageBmp2;
|
|||
extern NSString *const OWSMimeTypeUnknownForTests;
|
||||
|
||||
extern NSString *const kOversizeTextAttachmentUTI;
|
||||
extern NSString *const kOversizeTextAttachmentFileExtension;
|
||||
extern NSString *const kTextAttachmentFileExtension;
|
||||
extern NSString *const kUnknownTestAttachmentUTI;
|
||||
extern NSString *const kSyncMessageFileExtension;
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype";
|
|||
NSString *const OWSMimeTypeApplicationZip = @"application/zip";
|
||||
NSString *const OWSMimeTypeApplicationPdf = @"application/pdf";
|
||||
|
||||
NSString *const kOversizeTextAttachmentFileExtension = @"txt";
|
||||
NSString *const kTextAttachmentFileExtension = @"txt";
|
||||
NSString *const kUnknownTestAttachmentUTI = @"org.whispersystems.unknown";
|
||||
NSString *const kSyncMessageFileExtension = @"bin";
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
|
|||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SessionUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSDate+Timestamp.h>
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
|
|
|
@ -6,7 +6,7 @@ import Foundation
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
protocol AttachmentApprovalInputAccessoryViewDelegate: class {
|
||||
protocol AttachmentApprovalInputAccessoryViewDelegate: AnyObject {
|
||||
func attachmentApprovalInputUpdateMediaRail()
|
||||
func attachmentApprovalInputStartEditingCaptions()
|
||||
func attachmentApprovalInputStopEditingCaptions()
|
||||
|
@ -88,6 +88,14 @@ class AttachmentApprovalInputAccessoryView: UIView {
|
|||
// the layout if you hide the keyboard in the simulator (or if the
|
||||
// user uses an external keyboard).
|
||||
stackView.autoPinEdge(toSuperviewMargin: .bottom)
|
||||
|
||||
let galleryRailBlockingView: UIView = UIView()
|
||||
galleryRailBlockingView.backgroundColor = backgroundView.backgroundColor
|
||||
stackView.addSubview(galleryRailBlockingView)
|
||||
galleryRailBlockingView.pin(.top, to: .bottom, of: attachmentTextToolbar)
|
||||
galleryRailBlockingView.pin(.left, to: .left, of: stackView)
|
||||
galleryRailBlockingView.pin(.right, to: .right, of: stackView)
|
||||
galleryRailBlockingView.pin(.bottom, to: .bottom, of: stackView)
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
|
|
@ -436,7 +436,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
// MARK: - View Helpers
|
||||
|
||||
func remove(attachmentItem: SignalAttachmentItem) {
|
||||
if attachmentItem == currentItem {
|
||||
if attachmentItem.isEqual(to: currentItem) {
|
||||
if let nextItem = attachmentItemCollection.itemAfter(item: attachmentItem) {
|
||||
setCurrentItem(nextItem, direction: .forward, animated: true)
|
||||
}
|
||||
|
@ -449,30 +449,9 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
}
|
||||
}
|
||||
|
||||
guard let cell = galleryRailView.cellViews.first(where: { $0.item === attachmentItem }) else {
|
||||
owsFailDebug("cell was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.2,
|
||||
animations: {
|
||||
// shrink stack view item until it disappears
|
||||
cell.isHidden = true
|
||||
|
||||
// simultaneously fade out
|
||||
cell.alpha = 0
|
||||
},
|
||||
completion: { [weak self] _ in
|
||||
self?.attachmentItemCollection.remove(item: attachmentItem)
|
||||
|
||||
if let strongSelf: AttachmentApprovalViewController = self {
|
||||
self?.approvalDelegate?.attachmentApproval?(strongSelf, didRemoveAttachment: attachmentItem.attachment)
|
||||
}
|
||||
|
||||
self?.updateMediaRail()
|
||||
}
|
||||
)
|
||||
self.attachmentItemCollection.remove(item: attachmentItem)
|
||||
self.approvalDelegate?.attachmentApproval(self, didRemoveAttachment: attachmentItem.attachment)
|
||||
self.updateMediaRail()
|
||||
}
|
||||
|
||||
// MARK: - UIPageViewControllerDelegate
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
typedef void (^AttachmentSharingCompletion)(UIActivityType __nullable activityType);
|
||||
|
||||
@interface AttachmentSharing : NSObject
|
||||
|
||||
+ (void)showShareUIForAttachments:(NSArray<TSAttachmentStream *> *)attachmentStreams
|
||||
completion:(nullable AttachmentSharingCompletion)completion;
|
||||
|
||||
+ (void)showShareUIForAttachment:(TSAttachmentStream *)stream;
|
||||
+ (void)showShareUIForAttachment:(TSAttachmentStream *)stream completion:(nullable AttachmentSharingCompletion)completion;
|
||||
|
||||
+ (void)showShareUIForURL:(NSURL *)url;
|
||||
+ (void)showShareUIForURL:(NSURL *)url completion:(nullable AttachmentSharingCompletion)completion;
|
||||
|
||||
+ (void)showShareUIForURLs:(NSArray<NSURL *> *)urls completion:(nullable AttachmentSharingCompletion)completion;
|
||||
|
||||
+ (void)showShareUIForText:(NSString *)text;
|
||||
+ (void)showShareUIForText:(NSString *)text completion:(nullable AttachmentSharingCompletion)completion;
|
||||
|
||||
#ifdef DEBUG
|
||||
+ (void)showShareUIForUIImage:(UIImage *)image;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,119 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AttachmentSharing.h"
|
||||
#import "UIUtil.h"
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation AttachmentSharing
|
||||
|
||||
+ (void)showShareUIForAttachments:(NSArray<TSAttachmentStream *> *)attachmentStreams
|
||||
completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(attachmentStreams.count > 0);
|
||||
|
||||
NSMutableArray<NSURL *> *urls = [NSMutableArray new];
|
||||
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
|
||||
[urls addObject:attachmentStream.originalMediaURL];
|
||||
}
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:urls completion:completion];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForAttachment:(TSAttachmentStream *)stream
|
||||
{
|
||||
OWSAssertDebug(stream);
|
||||
|
||||
[self showShareUIForAttachment:stream completion:nil];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForAttachment:(TSAttachmentStream *)stream completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(stream);
|
||||
|
||||
[self showShareUIForURL:stream.originalMediaURL completion:completion];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForURL:(NSURL *)url
|
||||
{
|
||||
[self showShareUIForURL:url completion:nil];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForURL:(NSURL *)url completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(url);
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:@[ url ]
|
||||
completion:completion];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForURLs:(NSArray<NSURL *> *)urls completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(urls.count > 0);
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:urls
|
||||
completion:completion];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForText:(NSString *)text
|
||||
{
|
||||
[self showShareUIForText:text completion:nil];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForText:(NSString *)text completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(text);
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:@[ text, ]
|
||||
completion:completion];
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
+ (void)showShareUIForUIImage:(UIImage *)image
|
||||
{
|
||||
OWSAssertDebug(image);
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:@[ image, ]
|
||||
completion:nil];
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (void)showShareUIForActivityItems:(NSArray *)activityItems completion:(nullable AttachmentSharingCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(activityItems);
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
UIActivityViewController *activityViewController =
|
||||
[[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:@[]];
|
||||
|
||||
[activityViewController setCompletionWithItemsHandler:^(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
|
||||
if (activityError) {
|
||||
OWSLogInfo(@"Failed to share with activityError: %@", activityError);
|
||||
} else if (completed) {
|
||||
OWSLogInfo(@"Did share with activityType: %@", activityType);
|
||||
}
|
||||
|
||||
if (completion) {
|
||||
DispatchMainThreadSafe(^{ completion(activityType); });
|
||||
}
|
||||
}];
|
||||
|
||||
UIViewController *fromViewController = CurrentAppContext().frontmostViewController;
|
||||
while (fromViewController.presentedViewController) {
|
||||
fromViewController = fromViewController.presentedViewController;
|
||||
}
|
||||
OWSAssertDebug(fromViewController);
|
||||
[fromViewController presentViewController:activityViewController animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionMessagingKit
|
||||
|
||||
public struct ThreadViewModel: Equatable {
|
||||
public let thread: SessionThread
|
||||
public let name: String
|
||||
public let unreadCount: UInt
|
||||
public let unreadMentionCount: UInt
|
||||
|
||||
public let lastInteraction: Interaction?
|
||||
public let lastInteractionDate: Date
|
||||
public let lastInteractionText: String?
|
||||
public let lastInteractionState: RecipientState.State?
|
||||
|
||||
public init(
|
||||
thread: SessionThread,
|
||||
name: String,
|
||||
unreadCount: UInt,
|
||||
unreadMentionCount: UInt,
|
||||
lastInteraction: Interaction?,
|
||||
lastInteractionDate: Date,
|
||||
lastInteractionText: String?,
|
||||
lastInteractionState: RecipientState.State?
|
||||
) {
|
||||
self.thread = thread
|
||||
self.name = name
|
||||
self.unreadCount = unreadCount
|
||||
self.unreadMentionCount = unreadMentionCount
|
||||
|
||||
self.lastInteraction = lastInteraction
|
||||
self.lastInteractionDate = lastInteractionDate
|
||||
self.lastInteractionText = lastInteractionText
|
||||
self.lastInteractionState = lastInteractionState
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
|
|||
|
||||
#import <SignalUtilitiesKit/AppSetup.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/ByteParser.h>
|
||||
#import <SignalUtilitiesKit/FunctionalUtil.h>
|
||||
#import <SignalUtilitiesKit/NSArray+OWS.h>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol ApprovalRailCellViewDelegate: class {
|
||||
protocol ApprovalRailCellViewDelegate: AnyObject {
|
||||
func approvalRailCellView(_ approvalRailCellView: ApprovalRailCellView, didRemoveItem attachmentItem: SignalAttachmentItem)
|
||||
func canRemoveApprovalRailCellView(_ approvalRailCellView: ApprovalRailCellView) -> Bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue