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:
Morgan Pretty 2022-05-23 17:16:14 +10:00
parent 49dd341b6d
commit c500d4c6ca
87 changed files with 1342 additions and 2246 deletions

View File

@ -58,6 +58,7 @@ abstract_target 'GlobalDependencies' do
pod 'Reachability'
pod 'SAMKeychain'
pod 'SwiftProtobuf', '~> 1.5.0'
pod 'DifferenceKit'
end
target 'SessionUtilitiesKit' do

View File

@ -219,6 +219,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: bd0e75b0b6e37b30d8414efed2a5a98635e1a1a6
PODFILE CHECKSUM: 9715c163fab54d487be0c32357d6d1729aa96a7b
COCOAPODS: 1.11.2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,6 +38,7 @@ extension GarbageCollectionJob {
case expiredControlMessageProcessRecords
case threadTypingIndicators
case orphanedAttachmentFiles
case orphanedProfileAvatars
}
public struct Details: Codable {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSDate (Session)
+ (uint64_t)millisecondTimestamp;
@end
NS_ASSUME_NONNULL_END

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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