From 42853a08c9b5cb378de06f3fec5c0d68a133ee2d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 17 Aug 2023 11:28:03 +1000 Subject: [PATCH 1/5] Fixed a couple of minor outgoing quote bugs Added a nicer mechanism for using table aliases Fixed a bug where the quote preview view close button could overlap the content Fixed a bug where an outgoing quote wouldn't show it's thumbnail in some cases Fixed a bug where quoting a link preview wouldn't show the link preview attachment --- Session.xcodeproj/project.pbxproj | 4 +++ .../Content Views/QuoteView.swift | 7 ++--- .../Database/Models/Attachment.swift | 4 +-- .../Database/Models/Interaction.swift | 9 +++--- .../Jobs/Types/GarbageCollectionJob.swift | 2 +- .../Shared Models/MessageViewModel.swift | 30 +++++++++++++------ .../SessionThreadViewModel.swift | 2 +- .../Database/Types/TypedTableAlias.swift | 10 +++++-- .../SQLInterpolation+Utilities.swift | 21 +++++++++++++ 9 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a83905cf0..a8954a3b9 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -635,6 +635,7 @@ FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; }; FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; }; FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; }; + FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; }; FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; }; FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; }; @@ -1755,6 +1756,7 @@ FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = ""; }; FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; + FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = ""; }; FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = ""; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = ""; }; @@ -3693,6 +3695,7 @@ FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, + FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -5666,6 +5669,7 @@ C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, + FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index bda79ae51..ea86a406c 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -156,7 +156,7 @@ final class QuoteView: UIView { if attachment.isVisualMedia { attachment.thumbnail( size: .small, - success: { image, _ in + success: { [imageView] image, _ in guard Thread.isMainThread else { DispatchQueue.main.async { imageView.image = image @@ -234,8 +234,6 @@ final class QuoteView: UIView { } // Label stack view - let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace) - let isCurrentUser: Bool = [ currentUserPublicKey, currentUserBlinded15PublicKey, @@ -288,9 +286,8 @@ final class QuoteView: UIView { cancelButton.set(.height, to: cancelButtonSize) cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside) - addSubview(cancelButton) + mainStackView.addArrangedSubview(cancelButton) cancelButton.center(.vertical, in: self) - cancelButton.pin(.right, to: .right, of: self) } } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index bedc9fc31..9c1733e6c 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -522,7 +522,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) ) @@ -568,7 +568,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) ) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index c50645ca0..ffecf09bd 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -29,13 +29,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu /// Whenever using this `linkPreview` association make sure to filter the result using /// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey) - public static var linkPreviewFilterLiteral: SQL = { - let interaction: TypedTableAlias = TypedTableAlias() - let linkPreview: TypedTableAlias = TypedTableAlias() + public static func linkPreviewFilterLiteral( + interaction: TypedTableAlias = TypedTableAlias(), + linkPreview: TypedTableAlias = TypedTableAlias() + ) -> SQL { let halfResolution: Double = LinkPreview.timstampResolution return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) * 1000 AND (\(linkPreview[.timestamp]) + \(halfResolution)) * 1000)" - }() + } public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey) public typealias Columns = CodingKeys diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 7b5989579..96c28cf99 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -143,7 +143,7 @@ public enum GarbageCollectionJob: JobExecutor { FROM \(LinkPreview.self) LEFT JOIN \(Interaction.self) ON ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) WHERE \(interaction[.id]) IS NULL ) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 2a796cbd1..c4aeef7a6 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -777,10 +777,11 @@ public extension MessageViewModel { let disappearingMessagesConfig: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() let quote: TypedTableAlias = TypedTableAlias() + let quoteInteraction: TypedTableAlias = TypedTableAlias(name: "quoteInteraction") + let quoteLinkPreview: TypedTableAlias = TypedTableAlias(name: "quoteLinkPreview") let linkPreview: TypedTableAlias = TypedTableAlias() let threadProfile: SQL = SQL(stringLiteral: "threadProfile") - let quoteInteraction: SQL = SQL(stringLiteral: "quoteInteraction") let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment") let readReceipt: SQL = SQL(stringLiteral: "readReceipt") let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name) @@ -845,7 +846,7 @@ public extension MessageViewModel { \(quote[.interactionId]), \(quote[.authorId]), \(quote[.timestampMs]), - \(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn), + \(quoteInteraction[.body]), \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), \(ViewModel.quoteAttachmentKey).*, \(ViewModel.linkPreviewKey).*, @@ -873,12 +874,12 @@ public extension MessageViewModel { LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id]) - LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON ( - \(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND ( - \(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR ( + LEFT JOIN \(quoteInteraction) ON ( + \(quoteInteraction[.timestampMs]) = \(quote[.timestampMs]) AND ( + \(quoteInteraction[.authorId]) = \(quote[.authorId]) OR ( -- A users outgoing message is stored in some cases using their standard id -- but the quote will use their blinded id so handle that case - \(quoteInteraction).\(authorIdColumn) = \(userPublicKey) AND + \(quoteInteraction[.authorId]) = \(userPublicKey) AND ( \(quote[.authorId]) = \(blinded15PublicKey ?? "''") OR \(quote[.authorId]) = \(blinded25PublicKey ?? "''") @@ -887,14 +888,25 @@ public extension MessageViewModel { ) ) LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( - \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND + \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction[.id]) AND \(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) + LEFT JOIN \(quoteLinkPreview) ON ( + \(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND + \(Interaction.linkPreviewFilterLiteral( + interaction: quoteInteraction, + linkPreview: quoteLinkPreview + )) + ) + LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON ( + \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) OR + \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteLinkPreview[.attachmentId]) OR + \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quote[.attachmentId]) + ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND - \(Interaction.linkPreviewFilterLiteral) + \(Interaction.linkPreviewFilterLiteral()) ) LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId]) LEFT JOIN \(RecipientState.self) ON ( diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index c9d0c43eb..de2548745 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -712,7 +712,7 @@ public extension SessionThreadViewModel { ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND - \(Interaction.linkPreviewFilterLiteral) AND + \(Interaction.linkPreviewFilterLiteral()) AND \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) ) LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift index 14dd0aacd..fa91388c4 100644 --- a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift @@ -4,9 +4,15 @@ import Foundation import GRDB public class TypedTableAlias where T: TableRecord, T: ColumnExpressible { - public let alias: TableAlias = TableAlias(name: T.databaseTableName) + internal let name: String + internal let tableName: String + public let alias: TableAlias - public init() {} + public init(name: String = T.databaseTableName) { + self.name = name + self.tableName = T.databaseTableName + self.alias = TableAlias(name: name) + } public subscript(_ column: T.Columns) -> SQLExpression { return alias[column.name] diff --git a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift new file mode 100644 index 000000000..d8918ef17 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift @@ -0,0 +1,21 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension SQLInterpolation { + /// Appends the table name of the record type. + /// + /// // SELECT * FROM player + /// let player: TypedTableAlias = TypedTableAlias() + /// let request: SQLRequest = "SELECT * FROM \(player)" + @_disfavoredOverload + mutating func appendInterpolation(_ typedTableAlias: TypedTableAlias) { + let name: String = typedTableAlias.name + let tableName: String = typedTableAlias.tableName + + guard name != tableName else { return appendLiteral(tableName.quotedDatabaseIdentifier) } + + appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(name.quotedDatabaseIdentifier)") + } +} From e6c26e7ff4f4a24c3a3f9a766922859820c2c61d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 17 Aug 2023 16:39:47 +1000 Subject: [PATCH 2/5] Cleaned up the GRDB interface for complex queries --- Session.xcodeproj/project.pbxproj | 4 + .../GlobalSearchViewController.swift | 5 + .../MediaGalleryViewModel.swift | 42 +- .../Settings/BlockedContactsViewModel.swift | 21 +- .../Database/Models/ClosedGroup.swift | 13 + .../Database/Models/Interaction.swift | 20 +- .../Database/Models/OpenGroup.swift | 13 + .../Database/Models/Profile.swift | 15 + .../Database/Models/SessionThread.swift | 2 +- .../Jobs/Types/GarbageCollectionJob.swift | 22 +- .../Shared Models/MentionInfo.swift | 28 +- .../Shared Models/MessageViewModel.swift | 281 +++--- .../SessionThreadViewModel.swift | 936 +++++++++--------- .../Types/PagedDatabaseObserver.swift | 28 +- .../Database/Types/TypedTableAlias.swift | 59 +- .../SQLInterpolation+Utilities.swift | 50 +- .../Utilities/ScopeAdapter+Utilities.swift | 13 + 17 files changed, 877 insertions(+), 675 deletions(-) create mode 100644 SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a8954a3b9..d720d5f32 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -636,6 +636,7 @@ FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; }; FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; }; FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; }; + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; }; FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; }; FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; }; @@ -1757,6 +1758,7 @@ FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = ""; }; FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = ""; }; FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = ""; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = ""; }; @@ -3695,6 +3697,7 @@ FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */, FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */, ); path = Utilities; @@ -5681,6 +5684,7 @@ C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7d4103a85..fdb8ca753 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U ]) } catch { + // Don't log the 'interrupt' error as that's just the user typing too fast + if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { + SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + } + return .failure(error) } } diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 10eff35e3..0ff182537 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -199,16 +199,18 @@ public class MediaGalleryViewModel { } } - public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { - fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) - fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue) - - fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue + public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case interactionVariant + case interactionAuthorId + case interactionTimestampMs + + case rowId + case attachmentAlbumIndex + case attachment + } public var id: String { attachment.id } public var differenceIdentifier: String { attachment.id } @@ -306,7 +308,7 @@ public class MediaGalleryViewModel { let finalFilterSQL: SQL = { guard let customFilters: SQL = customFilters else { return """ - WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) + WHERE \(attachment[.rowId]) IN \(rowIds) """ } @@ -318,14 +320,14 @@ public class MediaGalleryViewModel { }() let request: SQLRequest = """ SELECT - \(interaction[.id]) AS \(Item.interactionIdKey), - \(interaction[.variant]) AS \(Item.interactionVariantKey), - \(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), - \(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), + \(interaction[.id]) AS \(Item.Columns.interactionId), + \(interaction[.variant]) AS \(Item.Columns.interactionVariant), + \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId), + \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs), - \(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), - \(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), - \(Item.attachmentKey).* + \(attachment[.rowId]) AS \(Item.Columns.rowId), + \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex), + \(attachment.allColumns) FROM \(Attachment.self) \(joinSQL) \(finalFilterSQL) @@ -338,8 +340,8 @@ public class MediaGalleryViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - Item.attachmentString: adapters[1] + return ScopeAdapter.with(Item.self, [ + .attachment: adapters[1] ]) } } diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index bf4a46bea..0a612d0b0 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -258,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel = """ SELECT - \(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), - \(DataModel.profileKey).* + \(profile[.rowId]) AS \(DataModel.Columns.rowId), + \(profile.allColumns) FROM \(Profile.self) - WHERE \(profile.alias[Column.rowID]) IN \(rowIds) + WHERE \(profile[.rowId]) IN \(rowIds) ORDER BY \(orderSQL) """ @@ -300,8 +301,8 @@ class BlockedContactsViewModel: SessionTableViewModel SQLRequest { let interaction: TypedTableAlias = TypedTableAlias() - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) - let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) let request: SQLRequest = """ SELECT @@ -720,9 +730,9 @@ public extension Interaction { \(interaction[.timestampMs]) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interaction.alias[Column.rowID]) AND - \(SQL("\(interactionFullTextSearch).\(threadIdLiteral) = \(threadId)")) AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(SQL("\(interactionFullTextSearch[.threadId]) = \(threadId)")) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) ORDER BY \(interaction[.timestampMs].desc) diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index 55da87c1d..d4b27a35c 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -215,6 +215,19 @@ public extension OpenGroup { } } +// MARK: - Search Queries + +public extension OpenGroup { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case name + } + + let name: String + } +} + // MARK: - Convenience public extension OpenGroup { diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index a9a6bd8af..7b8695929 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -298,6 +298,21 @@ public extension Profile { } } +// MARK: - Search Queries + +public extension Profile { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case nickname + case name + } + + let nickname: String? + let name: String + } +} + // MARK: - Convenience public extension Profile { diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 163d38038..cb8f84fc3 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -365,7 +365,7 @@ public extension SessionThread { let contact: TypedTableAlias = TypedTableAlias() return """ - SELECT \(thread.allColumns()) + SELECT \(thread.allColumns) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) WHERE ( diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 96c28cf99..633588a3d 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -82,7 +82,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Interaction.self) WHERE \(Column.rowID) IN ( - SELECT \(interaction.alias[Column.rowID]) + SELECT \(interaction[.rowId]) FROM \(Interaction.self) JOIN \(SessionThread.self) ON ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -90,7 +90,7 @@ public enum GarbageCollectionJob: JobExecutor { ) JOIN ( SELECT - COUNT(\(interaction.alias[Column.rowID])) AS interactionCount, + COUNT(\(interaction[.rowId])) AS interactionCount, \(interaction[.threadId]) FROM \(Interaction.self) GROUP BY \(interaction[.threadId]) @@ -112,7 +112,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Job.self) WHERE \(Column.rowID) IN ( - SELECT \(job.alias[Column.rowID]) + SELECT \(job[.rowId]) FROM \(Job.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId]) LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId]) @@ -139,7 +139,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(LinkPreview.self) WHERE \(Column.rowID) IN ( - SELECT \(linkPreview.alias[Column.rowID]) + SELECT \(linkPreview[.rowId]) FROM \(LinkPreview.self) LEFT JOIN \(Interaction.self) ON ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND @@ -159,7 +159,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(OpenGroup.self) WHERE \(Column.rowID) IN ( - SELECT \(openGroup.alias[Column.rowID]) + SELECT \(openGroup[.rowId]) FROM \(OpenGroup.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) WHERE ( @@ -178,7 +178,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Capability.self) WHERE \(Column.rowID) IN ( - SELECT \(capability.alias[Column.rowID]) + SELECT \(capability[.rowId]) FROM \(Capability.self) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer]) WHERE \(openGroup[.threadId]) IS NULL @@ -195,7 +195,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(BlindedIdLookup.self) WHERE \(Column.rowID) IN ( - SELECT \(blindedIdLookup.alias[Column.rowID]) + SELECT \(blindedIdLookup[.rowId]) FROM \(BlindedIdLookup.self) LEFT JOIN \(SessionThread.self) ON ( \(thread[.id]) = \(blindedIdLookup[.blindedId]) OR @@ -222,7 +222,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Contact.self) WHERE \(Column.rowID) IN ( - SELECT \(contact.alias[Column.rowID]) + SELECT \(contact[.rowId]) FROM \(Contact.self) LEFT JOIN \(BlindedIdLookup.self) ON ( \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND @@ -243,7 +243,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Attachment.self) WHERE \(Column.rowID) IN ( - SELECT \(attachment.alias[Column.rowID]) + SELECT \(attachment[.rowId]) FROM \(Attachment.self) LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id]) @@ -269,7 +269,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Profile.self) WHERE \(Column.rowID) IN ( - SELECT \(profile.alias[Column.rowID]) + SELECT \(profile[.rowId]) FROM \(Profile.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id]) LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id]) @@ -310,7 +310,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(SessionThread.self) WHERE \(Column.rowID) IN ( - SELECT \(thread.alias[Column.rowID]) + SELECT \(thread[.rowId]) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) diff --git a/SessionMessagingKit/Shared Models/MentionInfo.swift b/SessionMessagingKit/Shared Models/MentionInfo.swift index 984ddf63d..f4bb049cb 100644 --- a/SessionMessagingKit/Shared Models/MentionInfo.swift +++ b/SessionMessagingKit/Shared Models/MentionInfo.swift @@ -3,12 +3,14 @@ import GRDB import SessionUtilitiesKit -public struct MentionInfo: FetchableRecord, Decodable { - fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - - fileprivate static let profileString: String = CodingKeys.profile.stringValue +public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case profile + case threadVariant + case openGroupServer + case openGroupRoomToken + } public let profile: Profile public let threadVariant: SessionThread.Variant @@ -79,7 +81,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) @@ -89,7 +91,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) JOIN \(GroupMember.self) ON ( @@ -107,9 +109,9 @@ public extension MentionInfo { SELECT \(Profile.self).*, MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), - \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), - \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), + \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) \(targetJoin) JOIN \(Interaction.self) ON ( @@ -130,8 +132,8 @@ public extension MentionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - MentionInfo.profileString: adapters[0] + return ScopeAdapter.with(MentionInfo.self, [ + .profile: adapters[0] ]) } } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index c4aeef7a6..3eaada1b1 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -11,42 +11,66 @@ fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInt fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo -public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadIsTrustedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsTrusted.stringValue) - public static let threadHasDisappearingMessagesEnabledKey: SQL = SQL(stringLiteral: CodingKeys.threadHasDisappearingMessagesEnabled.stringValue) - public static let threadOpenGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupServer.stringValue) - public static let threadOpenGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupPublicKey.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let stateKey: SQL = SQL(stringLiteral: CodingKeys.state.stringValue) - public static let hasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.hasAtLeastOneReadReceipt.stringValue) - public static let mostRecentFailureTextKey: SQL = SQL(stringLiteral: CodingKeys.mostRecentFailureText.stringValue) - public static let isTypingIndicatorKey: SQL = SQL(stringLiteral: CodingKeys.isTypingIndicator.stringValue) - public static let isSenderOpenGroupModeratorKey: SQL = SQL(stringLiteral: CodingKeys.isSenderOpenGroupModerator.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - public static let quoteKey: SQL = SQL(stringLiteral: CodingKeys.quote.stringValue) - public static let quoteAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.quoteAttachment.stringValue) - public static let linkPreviewKey: SQL = SQL(stringLiteral: CodingKeys.linkPreview.stringValue) - public static let linkPreviewAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.linkPreviewAttachment.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) - public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) - public static let canHaveProfileKey: SQL = SQL(stringLiteral: CodingKeys.canHaveProfile.stringValue) - public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) - public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue) - public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) - public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) - public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) - public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) - - public static let profileString: String = CodingKeys.profile.stringValue - public static let quoteString: String = CodingKeys.quote.stringValue - public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue - public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue - public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue +public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case threadId + case threadVariant + case threadIsTrusted + case threadHasDisappearingMessagesEnabled + case threadOpenGroupServer + case threadOpenGroupPublicKey + case threadContactNameInternal + + // Interaction Info + + case rowId + case id + case openGroupServerMessageId + case variant + case timestampMs + case receivedAtTimestampMs + case authorId + case authorNameInternal + case body + case rawBody + case expiresStartedAtMs + case expiresInSeconds + + case state + case hasAtLeastOneReadReceipt + case mostRecentFailureText + case isSenderOpenGroupModerator + case isTypingIndicator + case profile + case quote + case quoteAttachment + case linkPreview + case linkPreviewAttachment + + case currentUserPublicKey + + // Post-Query Processing Data + + case attachments + case reactionInfo + case cellType + case authorName + case senderName + case canHaveProfile + case shouldShowProfile + case shouldShowDateHeader + case containsOnlyEmoji + case glyphCount + case previousVariant + case positionInCluster + case isOnlyMessageInCluster + case isLast + case isLastOutgoing + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case optimisticMessageId + } public enum CellType: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible { case textOnlyMessage @@ -462,13 +486,13 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, // MARK: - AttachmentInteractionInfo public extension MessageViewModel { - struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - public static let interactionAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachment.stringValue) - - public static let attachmentString: String = CodingKeys.attachment.stringValue - public static let interactionAttachmentString: String = CodingKeys.interactionAttachment.stringValue + struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case attachment + case interactionAttachment + } public let rowId: Int64 public let attachment: Attachment @@ -491,13 +515,13 @@ public extension MessageViewModel { // MARK: - ReactionInfo public extension MessageViewModel { - struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - - public static let reactionString: String = CodingKeys.reaction.stringValue - public static let profileString: String = CodingKeys.profile.stringValue + struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case reaction + case profile + } public let rowId: Int64 public let reaction: Reaction @@ -522,9 +546,12 @@ public extension MessageViewModel { // MARK: - TypingIndicatorInfo public extension MessageViewModel { - struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) + struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + } public let rowId: Int64 public let threadId: String @@ -776,60 +803,48 @@ public extension MessageViewModel { let contact: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let threadProfile: TypedTableAlias = TypedTableAlias(name: "threadProfile") let quote: TypedTableAlias = TypedTableAlias() let quoteInteraction: TypedTableAlias = TypedTableAlias(name: "quoteInteraction") + let quoteInteractionAttachment: TypedTableAlias = TypedTableAlias( + name: "quoteInteractionAttachment" + ) let quoteLinkPreview: TypedTableAlias = TypedTableAlias(name: "quoteLinkPreview") + let quoteAttachment: TypedTableAlias = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue) let linkPreview: TypedTableAlias = TypedTableAlias() - - let threadProfile: SQL = SQL(stringLiteral: "threadProfile") - let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment") - let readReceipt: SQL = SQL(stringLiteral: "readReceipt") - let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name) - let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name) - let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name) - let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name) - let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readTimestampMsColumn: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let timestampMsColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let authorIdColumn: SQL = SQL(stringLiteral: Interaction.Columns.authorId.name) - let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name) - let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) + let linkPreviewAttachment: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment) + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let numColumnsBeforeLinkedRecords: Int = 22 let finalGroupSQL: SQL = (groupSQL ?? "") let request: SQLRequest = """ SELECT - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), -- Default to 'true' for non-contact threads - IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.threadIsTrustedKey), + IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.Columns.threadIsTrusted), -- Default to 'false' when no contact exists - IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), - \(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), - \(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), - IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), + IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.Columns.threadHasDisappearingMessagesEnabled), + \(openGroup[.server]) AS \(ViewModel.Columns.threadOpenGroupServer), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.threadOpenGroupPublicKey), + IFNULL(\(threadProfile[.nickname]), \(threadProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), \(interaction[.id]), \(interaction[.openGroupServerMessageId]), \(interaction[.variant]), \(interaction[.timestampMs]), \(interaction[.receivedAtTimestampMs]), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), \(interaction[.body]), \(interaction[.expiresStartedAtMs]), \(interaction[.expiresInSeconds]), -- Default to 'sending' assuming non-processed interaction when null - IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), - (\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), - \(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), + IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.state), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.hasAtLeastOneReadReceipt), + \(recipientState[.mostRecentFailureText]) AS \(ViewModel.Columns.mostRecentFailureText), EXISTS ( SELECT 1 @@ -840,36 +855,36 @@ public extension MessageViewModel { \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) ) - ) AS \(ViewModel.isSenderOpenGroupModeratorKey), + ) AS \(ViewModel.Columns.isSenderOpenGroupModerator), - \(ViewModel.profileKey).*, + \(profile.allColumns), \(quote[.interactionId]), \(quote[.authorId]), \(quote[.timestampMs]), \(quoteInteraction[.body]), - \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), - \(ViewModel.quoteAttachmentKey).*, - \(ViewModel.linkPreviewKey).*, - \(ViewModel.linkPreviewAttachmentKey).*, + \(quoteInteractionAttachment[.attachmentId]), + \(quoteAttachment.allColumns), + \(linkPreview.allColumns), + \(linkPreviewAttachment.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey), -- All of the below properties are set in post-query processing but to prevent the -- query from crashing when decoding we need to provide default values - \(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), - '' AS \(ViewModel.authorNameKey), - false AS \(ViewModel.canHaveProfileKey), - false AS \(ViewModel.shouldShowProfileKey), - false AS \(ViewModel.shouldShowDateHeaderKey), - \(Position.middle) AS \(ViewModel.positionInClusterKey), - false AS \(ViewModel.isOnlyMessageInClusterKey), - false AS \(ViewModel.isLastKey), - false AS \(ViewModel.isLastOutgoingKey) + \(CellType.textOnlyMessage) AS \(ViewModel.Columns.cellType), + '' AS \(ViewModel.Columns.authorName), + false AS \(ViewModel.Columns.canHaveProfile), + false AS \(ViewModel.Columns.shouldShowProfile), + false AS \(ViewModel.Columns.shouldShowDateHeader), + \(Position.middle) AS \(ViewModel.Columns.positionInCluster), + false AS \(ViewModel.Columns.isOnlyMessageInCluster), + false AS \(ViewModel.Columns.isLast), + false AS \(ViewModel.Columns.isLastOutgoing) FROM \(Interaction.self) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(interaction[.threadId]) + LEFT JOIN \(threadProfile) ON \(threadProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) @@ -887,9 +902,9 @@ public extension MessageViewModel { ) ) ) - LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( - \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction[.id]) AND - \(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 + LEFT JOIN \(quoteInteractionAttachment) ON ( + \(quoteInteractionAttachment[.interactionId]) = \(quoteInteraction[.id]) AND + \(quoteInteractionAttachment[.albumIndex]) = 0 ) LEFT JOIN \(quoteLinkPreview) ON ( \(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND @@ -898,27 +913,27 @@ public extension MessageViewModel { linkPreview: quoteLinkPreview )) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON ( - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) OR - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteLinkPreview[.attachmentId]) OR - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quote[.attachmentId]) + LEFT JOIN \(quoteAttachment) ON ( + \(quoteAttachment[.id]) = \(quoteInteractionAttachment[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quoteLinkPreview[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quote[.attachmentId]) ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(Interaction.linkPreviewFilterLiteral()) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId]) + LEFT JOIN \(linkPreviewAttachment) ON \(linkPreviewAttachment[.id]) = \(linkPreview[.attachmentId]) LEFT JOIN \(RecipientState.self) ON ( -- Ignore 'skipped' states \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(recipientState[.interactionId]) = \(interaction[.id]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( - \(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND - \(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) + LEFT JOIN \(readReceipt) ON ( + \(readReceipt[.readTimestampMs]) IS NOT NULL AND + \(readReceipt[.interactionId]) = \(interaction[.id]) ) - WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) + WHERE \(interaction[.rowId]) IN \(rowIds) \(finalGroupSQL) ORDER BY \(orderSQL) """ @@ -933,12 +948,12 @@ public extension MessageViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.profileString: adapters[1], - ViewModel.quoteString: adapters[2], - ViewModel.quoteAttachmentString: adapters[3], - ViewModel.linkPreviewString: adapters[4], - ViewModel.linkPreviewAttachmentString: adapters[5] + return ScopeAdapter.with(ViewModel.self, [ + .profile: adapters[1], + .quote: adapters[2], + .quoteAttachment: adapters[3], + .linkPreview: adapters[4], + .linkPreviewAttachment: adapters[5] ]) } } @@ -965,9 +980,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(attachment.alias[Column.rowID]) AS \(AttachmentInteractionInfo.rowIdKey), - \(AttachmentInteractionInfo.attachmentKey).*, - \(AttachmentInteractionInfo.interactionAttachmentKey).* + \(attachment[.rowId]) AS \(AttachmentInteractionInfo.Columns.rowId), + \(attachment.allColumns), + \(interactionAttachment.allColumns) FROM \(Attachment.self) JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id]) \(finalFilterSQL) @@ -980,9 +995,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { InteractionAttachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - AttachmentInteractionInfo.attachmentString: adapters[1], - AttachmentInteractionInfo.interactionAttachmentString: adapters[2] + return ScopeAdapter.with(AttachmentInteractionInfo.self, [ + .attachment: adapters[1], + .interactionAttachment: adapters[2] ]) } } @@ -1046,9 +1061,9 @@ public extension MessageViewModel.ReactionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), - \(ReactionInfo.reactionKey).*, - \(ReactionInfo.profileKey).* + \(reaction[.rowId]) AS \(ReactionInfo.Columns.rowId), + \(reaction.allColumns), + \(profile.allColumns) FROM \(Reaction.self) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) \(finalFilterSQL) @@ -1061,9 +1076,9 @@ public extension MessageViewModel.ReactionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ReactionInfo.reactionString: adapters[1], - ReactionInfo.profileString: adapters[2] + return ScopeAdapter.with(ReactionInfo.self, [ + .reaction: adapters[1], + .profile: adapters[2] ]) } } @@ -1129,8 +1144,8 @@ public extension MessageViewModel.TypingIndicatorInfo { }() let request: SQLRequest = """ SELECT - \(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), - \(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) + \(threadTypingIndicator[.rowId]), + \(threadTypingIndicator[.threadId]) FROM \(ThreadTypingIndicator.self) \(finalFilterSQL) """ diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index de2548745..87a09f337 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -14,65 +14,70 @@ fileprivate typealias ViewModel = SessionThreadViewModel /// /// **Note:** When updating the UI make sure to check the actual queries being run as some fields will have incorrect default values /// in order to optimise their queries to only include the required data -public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadCreationDateTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadCreationDateTimestamp.stringValue) - public static let threadMemberNamesKey: SQL = SQL(stringLiteral: CodingKeys.threadMemberNames.stringValue) - public static let threadIsNoteToSelfKey: SQL = SQL(stringLiteral: CodingKeys.threadIsNoteToSelf.stringValue) - public static let threadIsMessageRequestKey: SQL = SQL(stringLiteral: CodingKeys.threadIsMessageRequest.stringValue) - public static let threadRequiresApprovalKey: SQL = SQL(stringLiteral: CodingKeys.threadRequiresApproval.stringValue) - public static let threadShouldBeVisibleKey: SQL = SQL(stringLiteral: CodingKeys.threadShouldBeVisible.stringValue) - public static let threadPinnedPriorityKey: SQL = SQL(stringLiteral: CodingKeys.threadPinnedPriority.stringValue) - public static let threadIsBlockedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsBlocked.stringValue) - public static let threadMutedUntilTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadMutedUntilTimestamp.stringValue) - public static let threadOnlyNotifyForMentionsKey: SQL = SQL(stringLiteral: CodingKeys.threadOnlyNotifyForMentions.stringValue) - public static let threadMessageDraftKey: SQL = SQL(stringLiteral: CodingKeys.threadMessageDraft.stringValue) - public static let threadContactIsTypingKey: SQL = SQL(stringLiteral: CodingKeys.threadContactIsTyping.stringValue) - public static let threadWasMarkedUnreadKey: SQL = SQL(stringLiteral: CodingKeys.threadWasMarkedUnread.stringValue) - public static let threadUnreadCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadCount.stringValue) - public static let threadUnreadMentionCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadMentionCount.stringValue) - public static let disappearingMessagesConfigurationKey: SQL = SQL(stringLiteral: CodingKeys.disappearingMessagesConfiguration.stringValue) - public static let contactProfileKey: SQL = SQL(stringLiteral: CodingKeys.contactProfile.stringValue) - public static let closedGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupName.stringValue) - public static let closedGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupUserCount.stringValue) - public static let currentUserIsClosedGroupMemberKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupMember.stringValue) - public static let currentUserIsClosedGroupAdminKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupAdmin.stringValue) - public static let closedGroupProfileFrontKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileFront.stringValue) - public static let closedGroupProfileBackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBack.stringValue) - public static let closedGroupProfileBackFallbackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBackFallback.stringValue) - public static let openGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.openGroupName.stringValue) - public static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - public static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - public static let openGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPublicKey.stringValue) - public static let openGroupProfilePictureDataKey: SQL = SQL(stringLiteral: CodingKeys.openGroupProfilePictureData.stringValue) - public static let openGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.openGroupUserCount.stringValue) - public static let openGroupPermissionsKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPermissions.stringValue) - public static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - public static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - public static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - public static let interactionBodyKey: SQL = SQL(stringLiteral: CodingKeys.interactionBody.stringValue) - public static let interactionStateKey: SQL = SQL(stringLiteral: CodingKeys.interactionState.stringValue) - public static let interactionHasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.interactionHasAtLeastOneReadReceipt.stringValue) - public static let interactionIsOpenGroupInvitationKey: SQL = SQL(stringLiteral: CodingKeys.interactionIsOpenGroupInvitation.stringValue) - public static let interactionAttachmentDescriptionInfoKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentDescriptionInfo.stringValue) - public static let interactionAttachmentCountKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentCount.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - - public static let threadWasMarkedUnreadString: String = CodingKeys.threadWasMarkedUnread.stringValue - public static let threadUnreadCountString: String = CodingKeys.threadUnreadCount.stringValue - public static let threadUnreadMentionCountString: String = CodingKeys.threadUnreadMentionCount.stringValue - public static let closedGroupUserCountString: String = CodingKeys.closedGroupUserCount.stringValue - public static let openGroupUserCountString: String = CodingKeys.openGroupUserCount.stringValue - public static let disappearingMessagesConfigurationString: String = CodingKeys.disappearingMessagesConfiguration.stringValue - public static let contactProfileString: String = CodingKeys.contactProfile.stringValue - public static let closedGroupProfileFrontString: String = CodingKeys.closedGroupProfileFront.stringValue - public static let closedGroupProfileBackString: String = CodingKeys.closedGroupProfileBack.stringValue - public static let closedGroupProfileBackFallbackString: String = CodingKeys.closedGroupProfileBackFallback.stringValue - public static let interactionAttachmentDescriptionInfoString: String = CodingKeys.interactionAttachmentDescriptionInfo.stringValue +public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + case threadVariant + case threadCreationDateTimestamp + case threadMemberNames + + case threadIsNoteToSelf + case threadIsMessageRequest + case threadRequiresApproval + case threadShouldBeVisible + case threadPinnedPriority + case threadIsBlocked + case threadMutedUntilTimestamp + case threadOnlyNotifyForMentions + case threadMessageDraft + + case threadContactIsTyping + case threadWasMarkedUnread + case threadUnreadCount + case threadUnreadMentionCount + + // Thread display info + + case disappearingMessagesConfiguration + + case contactProfile + case closedGroupProfileFront + case closedGroupProfileBack + case closedGroupProfileBackFallback + case closedGroupName + case closedGroupUserCount + case currentUserIsClosedGroupMember + case currentUserIsClosedGroupAdmin + case openGroupName + case openGroupServer + case openGroupRoomToken + case openGroupPublicKey + case openGroupProfilePictureData + case openGroupUserCount + case openGroupPermissions + + // Interaction display info + + case interactionId + case interactionVariant + case interactionTimestampMs + case interactionBody + case interactionState + case interactionHasAtLeastOneReadReceipt + case interactionIsOpenGroupInvitation + case interactionAttachmentDescriptionInfo + case interactionAttachmentCount + + case authorId + case threadContactNameInternal + case authorNameInternal + case currentUserPublicKey + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case recentReactionEmoji + } public var differenceIdentifier: String { threadId } public var id: String { threadId } @@ -554,6 +559,51 @@ public extension SessionThreadViewModel { } } +// MARK: - AggregateInteraction + +private struct AggregateInteraction: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case threadId + case interactionTimestampMs + case threadUnreadCount + case threadUnreadMentionCount + } + + let interactionId: Int64 + let threadId: String + let interactionTimestampMs: Int64 + let threadUnreadCount: UInt? + let threadUnreadMentionCount: UInt? +} + +// MARK: - ClosedGroupUserCount + +private struct ClosedGroupUserCount: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case closedGroupUserCount + } + + let groupId: String + let closedGroupUserCount: Int +} + +// MARK: - GroupMemberInfo + +private struct GroupMemberInfo: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case threadMemberNames + } + + let groupId: String + let threadMemberNames: String +} + // MARK: - HomeVC & MessageRequestsViewController // MARK: --SessionThreadViewModel @@ -570,65 +620,57 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() let typingIndicator: TypedTableAlias = TypedTableAlias() - let closedGroup: TypedTableAlias = TypedTableAlias() - let groupMember: TypedTableAlias = TypedTableAlias() - let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() let recipientState: TypedTableAlias = TypedTableAlias() + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let linkPreview: TypedTableAlias = TypedTableAlias() + let firstInteractionAttachment: TypedTableAlias = TypedTableAlias(name: "firstInteractionAttachment") let attachment: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") - let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment") - let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) - + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) + let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) + let groupMember: TypedTableAlias = TypedTableAlias() + let openGroup: TypedTableAlias = TypedTableAlias() /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` 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 = 14 let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), - (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey), + (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.Columns.threadContactIsTyping), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), + \(aggregateInteraction[.threadUnreadMentionCount]), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -638,7 +680,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -648,15 +690,15 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - \(interaction[.body]) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + \(interaction[.body]) AS \(ViewModel.Columns.interactionBody), -- Default to 'sending' assuming non-processed interaction when null IFNULL(( @@ -668,22 +710,22 @@ public extension SessionThreadViewModel { \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) ) LIMIT 1 - ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey), + ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.interactionState), - (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey), - (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.interactionHasAtLeastOneReadReceipt), + (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.Columns.interactionIsOpenGroupInvitation), -- These 4 properties will be combined into 'Attachment.DescriptionInfo' \(attachment[.id]), \(attachment[.variant]), \(attachment[.contentType]), \(attachment[.sourceFilename]), - COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), + COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.Columns.interactionAttachmentCount), \(interaction[.authorId]), - IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(contactProfile[.nickname]), \(contactProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) @@ -691,46 +733,46 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey), - SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( - \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND - \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL + LEFT JOIN \(readReceipt) ON ( + \(interaction[.id]) = \(readReceipt[.interactionId]) AND + \(readReceipt[.readTimestampMs]) IS NOT NULL ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(Interaction.linkPreviewFilterLiteral()) AND \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) ) - LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( - \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND - \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 + LEFT JOIN \(firstInteractionAttachment) ON ( + \(firstInteractionAttachment[.interactionId]) = \(interaction[.id]) AND + \(firstInteractionAttachment[.albumIndex]) = 0 ) - LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) + LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachment[.attachmentId]) LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) -- Thread naming & avatar content - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -741,9 +783,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -754,13 +796,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) - WHERE \(thread.alias[Column.rowID]) IN \(rowIds) + WHERE \(thread[.rowId]) IN \(rowIds) \(groupSQL) ORDER BY \(orderSQL) """ @@ -776,12 +818,12 @@ public extension SessionThreadViewModel { Attachment.DescriptionInfo.numberOfSelectedColumns() ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4], - ViewModel.interactionAttachmentDescriptionInfoString: adapters[6] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4], + .interactionAttachmentDescriptionInfo: adapters[6] ]) } } @@ -868,55 +910,52 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfiguration: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let closedGroupUserCount: TypedTableAlias = TypedTableAlias(name: "closedGroupUserCount") /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `disappearingMessageSConfiguration` 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 = 15 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND IFNULL(\(contact[.didApproveMe]), false) = false - ) AS \(ViewModel.threadRequiresApprovalKey), - \(thread[.shouldBeVisible]) AS \(ViewModel.threadShouldBeVisibleKey), + ) AS \(ViewModel.Columns.threadRequiresApproval), + \(thread[.shouldBeVisible]) AS \(ViewModel.Columns.threadShouldBeVisible), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), - \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), + \(thread[.messageDraft]) AS \(ViewModel.Columns.threadMessageDraft), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), - \(ViewModel.disappearingMessagesConfigurationKey).*, + \(disappearingMessagesConfiguration.allColumns), - \(ViewModel.contactProfileKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey), + \(contactProfile.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(closedGroupUserCount[.closedGroupUserCount]), EXISTS ( SELECT 1 @@ -926,49 +965,50 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), - \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.userCount]) AS \(ViewModel.Columns.openGroupUserCount), + \(openGroup[.permissions]) AS \(ViewModel.Columns.openGroupPermissions), - \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey), - \(aggregateInteractionLiteral).\(ViewModel.interactionTimestampMsKey), + \(aggregateInteraction[.interactionId]), + \(aggregateInteraction[.interactionTimestampMs]), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfiguration[.threadId]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE ( \(SQL("\(interaction[.threadId]) = \(threadId)")) AND \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) ) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN ( SELECT \(groupMember[.groupId]), - COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey) + COUNT(\(groupMember[.rowId])) AS \(ClosedGroupUserCount.Columns.closedGroupUserCount) FROM \(GroupMember.self) WHERE ( \(SQL("\(groupMember[.groupId]) = \(threadId)")) AND \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) ) - ) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)")) + ) AS \(closedGroupUserCount) ON \(SQL("\(closedGroupUserCount[.groupId]) = \(threadId)")) WHERE \(SQL("\(thread[.id]) = \(threadId)")) """ @@ -980,9 +1020,9 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.disappearingMessagesConfigurationString: adapters[1], - ViewModel.contactProfileString: adapters[2] + return ScopeAdapter.with(ViewModel.self, [ + .disappearingMessagesConfiguration: adapters[1], + .contactProfile: adapters[2] ]) } } @@ -990,39 +1030,40 @@ public extension SessionThreadViewModel { static func conversationSettingsQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` 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 = 9 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -1032,7 +1073,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -1042,24 +1083,24 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1070,9 +1111,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1083,10 +1124,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE \(SQL("\(thread[.id]) = \(threadId)")) @@ -1101,11 +1142,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1192,13 +1233,15 @@ public extension SessionThreadViewModel { static func messagesQuery(userPublicKey: String, pattern: FTS5Pattern) -> AdaptedFetchRequest> { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() - let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let interactionLiteral: SQL = SQL(stringLiteral: Interaction.databaseTableName) - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -1208,44 +1251,44 @@ public extension SessionThreadViewModel { let numColumnsBeforeProfiles: Int = 6 let request: SQLRequest = """ SELECT - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.Columns.interactionBody), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interactionLiteral).rowid AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(interaction[.threadId]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1256,9 +1299,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1269,10 +1312,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) ORDER BY \(Column.rank), \(interaction[.timestampMs].desc) @@ -1288,11 +1331,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1315,31 +1358,26 @@ public extension SessionThreadViewModel { /// returned results will always be `-1` for those results static func contactsAndGroupsQuery(userPublicKey: String, pattern: FTS5Pattern, searchTerm: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() + let groupMemberProfile: TypedTableAlias = TypedTableAlias(name: "groupMemberProfile") let openGroup: TypedTableAlias = TypedTableAlias() + let groupMemberInfo: TypedTableAlias = TypedTableAlias(name: "groupMemberInfo") let profile: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) + let profileFullTextSearch: TypedTableAlias = TypedTableAlias(name: Profile.fullTextSearchTableName) + let closedGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: ClosedGroup.fullTextSearchTableName) + let openGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: OpenGroup.fullTextSearchTableName) - let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) - let closedGroupNameColumnLiteral: SQL = SQL(stringLiteral: ClosedGroup.Columns.name.name) - let closedGroupLiteral: SQL = SQL(stringLiteral: ClosedGroup.databaseTableName) - let closedGroupFullTextSearch: SQL = SQL(stringLiteral: ClosedGroup.fullTextSearchTableName) - let openGroupNameColumnLiteral: SQL = SQL(stringLiteral: OpenGroup.Columns.name.name) - let openGroupLiteral: SQL = SQL(stringLiteral: OpenGroup.databaseTableName) - let openGroupFullTextSearch: SQL = SQL(stringLiteral: OpenGroup.fullTextSearchTableName) - let groupMemberInfoLiteral: SQL = SQL(stringLiteral: "groupMemberInfo") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let groupMemberProfileLiteral: SQL = SQL(stringLiteral: "groupMemberProfile") let noteToSelfLiteral: SQL = SQL(stringLiteral: "NOTE_TO_SELF".localized().lowercased()) let searchTermLiteral: SQL = SQL(stringLiteral: searchTerm.lowercased()) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// We use `IFNULL(rank, 100)` because the custom `Note to Self` like comparison will get a null /// `rank` value which ends up as the first result, by defaulting to `100` it will always be ranked last compared @@ -1350,24 +1388,24 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) @@ -1375,18 +1413,13 @@ public extension SessionThreadViewModel { // MARK: --Contact Threads let contactQueryCommonJoinFilterGroup: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - 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 + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND @@ -1398,8 +1431,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1413,8 +1446,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1429,14 +1462,14 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT \(groupMember[.groupId]), - GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey) + GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(GroupMemberInfo.Columns.threadMemberNames) 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) = ( + ) AS \(groupMemberInfo) ON \(groupMemberInfo[.groupId]) = \(closedGroup[.threadId]) + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1447,9 +1480,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1460,13 +1493,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + LEFT JOIN \(closedGroupProfileBackFallback) ON ( + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false - LEFT JOIN \(OpenGroup.self) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(openGroup.never) WHERE ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyGroup)")) OR @@ -1484,8 +1517,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(closedGroupFullTextSearch) ON ( - \(closedGroupFullTextSearch).rowid = \(closedGroupLiteral).rowid AND - \(closedGroupFullTextSearch).\(closedGroupNameColumnLiteral) MATCH \(pattern) + \(closedGroupFullTextSearch[.rowId]) = \(closedGroup[.rowId]) AND + \(closedGroupFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1498,10 +1531,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1514,10 +1547,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1533,20 +1566,15 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) JOIN \(openGroupFullTextSearch) ON ( - \(openGroupFullTextSearch).rowid = \(openGroupLiteral).rowid AND - \(openGroupFullTextSearch).\(openGroupNameColumnLiteral) MATCH \(pattern) + \(openGroupFullTextSearch[.rowId]) = \(openGroup[.rowId]) AND + \(openGroupFullTextSearch[.name]) MATCH \(pattern) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) 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 - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -1556,18 +1584,13 @@ public extension SessionThreadViewModel { // MARK: --Note to Self Thread let noteToSelfQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - 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 \(OpenGroup.self) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) """ // Note to self thread searching for 'Note to Self' (need to join an FTS table to @@ -1600,8 +1623,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1620,8 +1643,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1635,41 +1658,36 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - -1 AS \(ViewModel.rowIdKey), - \(contact[.id]) AS \(ViewModel.threadIdKey), - \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.threadVariantKey), - 0 AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + -1 AS \(ViewModel.Columns.rowId), + \(contact[.id]) AS \(ViewModel.Columns.threadId), + \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.Columns.threadVariant), + 0 AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - false AS \(ViewModel.threadIsNoteToSelfKey), - -1 AS \(ViewModel.threadPinnedPriorityKey), + false AS \(ViewModel.Columns.threadIsNoteToSelf), + -1 AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Contact.self) """ let hiddenContactQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(contact[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(contact[.id]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(contact[.id]) - 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 + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(thread[.id]) IS NULL GROUP BY \(contact[.id]) @@ -1685,8 +1703,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1701,8 +1719,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1717,13 +1735,13 @@ public extension SessionThreadViewModel { \(sqlQuery) ) - GROUP BY \(ViewModel.threadIdKey) + GROUP BY \(ViewModel.Columns.threadId) ORDER BY \(Column.rank), - \(ViewModel.threadIsNoteToSelfKey), - \(ViewModel.closedGroupNameKey), - \(ViewModel.openGroupNameKey), - \(ViewModel.threadIdKey) + \(ViewModel.Columns.threadIsNoteToSelf), + \(ViewModel.Columns.closedGroupName), + \(ViewModel.Columns.openGroupName), + \(ViewModel.Columns.threadId) LIMIT \(SQL("\(SessionThreadViewModel.searchResultsLimit)")) """ @@ -1752,11 +1770,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1764,31 +1782,30 @@ public extension SessionThreadViewModel { /// This method returns only the 'Note to Self' thread in the structure of a search result conversation static func noteToSelfOnlyQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw let numColumnsBeforeProfiles: Int = 8 let request: SQLRequest = """ SELECT 100 AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - '' AS \(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + '' AS \(ViewModel.Columns.threadMemberNames), - true AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + true AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, + \(contactProfile.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) WHERE \(SQL("\(thread[.id]) = \(userPublicKey)")) """ @@ -1801,8 +1818,8 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1] ]) } } @@ -1814,67 +1831,70 @@ public extension SessionThreadViewModel { static func shareQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` 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 = 7 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + 0 AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1885,9 +1905,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1898,10 +1918,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE ( @@ -1929,11 +1949,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index ab6ae915f..2c41643c2 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -1351,18 +1351,24 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet // Fetch the inserted/updated rows let additionalFilters: SQL = SQL(rowIds.contains(Column.rowID)) - let updatedItems: [T] = (try? dataQuery(additionalFilters) - .fetchAll(db)) - .defaulting(to: []) - // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link - // preview) then trigger the update callback (if there were deletions) and stop here - guard !updatedItems.isEmpty else { return hasOtherChanges } - - // Process the upserted data (assume at least one value changed) - dataCache.mutate { $0 = $0.upserting(items: updatedItems) } - - return true + do { + let updatedItems: [T] = try dataQuery(additionalFilters) + .fetchAll(db) + + // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link + // preview) then trigger the update callback (if there were deletions) and stop here + guard !updatedItems.isEmpty else { return hasOtherChanges } + + // Process the upserted data (assume at least one value changed) + dataCache.mutate { $0 = $0.upserting(items: updatedItems) } + + return true + } + catch { + SNLog("[PagedDatabaseObserver] Error loading associated data: \(error)") + return hasOtherChanges + } } public func clearCache(_ db: Database) { diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift index fa91388c4..ed42d63e0 100644 --- a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift @@ -3,28 +3,65 @@ import Foundation import GRDB -public class TypedTableAlias where T: TableRecord, T: ColumnExpressible { - internal let name: String - internal let tableName: String - public let alias: TableAlias +public struct TypedTableAlias { + public enum RowIdColumn { + case rowId + } - public init(name: String = T.databaseTableName) { + internal let name: String + internal let tableName: String? + internal let alias: TableAlias + + public var allColumns: SQLSelection { alias[AllColumns().sqlSelection] } + public var never: NeverJoiningTypedTableAlias { NeverJoiningTypedTableAlias(alias: self) } + + // MARK: - Initialization + + public init(name: String, tableName: String? = nil) { + self.name = name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(name: String) where T: TableRecord { self.name = name self.tableName = T.databaseTableName self.alias = TableAlias(name: name) } + public init() where T: TableRecord { + self = TypedTableAlias(name: T.databaseTableName) + } + + public init(_ viewModel: VM.Type, column: VM.Columns, tableName: String?) { + self.name = column.name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(_ viewModel: VM.Type, column: VM.Columns) where T: TableRecord { + self = TypedTableAlias(viewModel, column: column, tableName: T.databaseTableName) + } + + // MARK: - Functions + public subscript(_ column: T.Columns) -> SQLExpression { return alias[column.name] } - /// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will - /// throw when trying to decode - public func allColumns() -> SQLSelection { - return alias[AllColumns().sqlSelection] + public subscript(_ column: RowIdColumn) -> SQLSelection { + return alias[Column.rowID] } } +// MARK: - NeverJoiningTypedTableAlias + +public struct NeverJoiningTypedTableAlias { + internal let alias: TypedTableAlias +} + +// MARK: - Extensions + extension QueryInterfaceRequest { public func aliased(_ typedAlias: TypedTableAlias) -> Self { return aliased(typedAlias.alias) @@ -38,7 +75,5 @@ extension Association { } extension TableAlias { - public func allColumns() -> SQLSelection { - return self[AllColumns().sqlSelection] - } + public var allColumns: SQLSelection { self[AllColumns().sqlSelection] } } diff --git a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift index d8918ef17..01d0c64bb 100644 --- a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift @@ -12,10 +12,58 @@ public extension SQLInterpolation { @_disfavoredOverload mutating func appendInterpolation(_ typedTableAlias: TypedTableAlias) { let name: String = typedTableAlias.name - let tableName: String = typedTableAlias.tableName + guard let tableName: String = typedTableAlias.tableName else { return appendLiteral(name.quotedDatabaseIdentifier) } guard name != tableName else { return appendLiteral(tableName.quotedDatabaseIdentifier) } appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(name.quotedDatabaseIdentifier)") } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN team AS testTeam ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let testTeam: TypedTableAlias = TypedTableAlias(name: "testTeam") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(testTeam.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T: TableRecord { + guard let tableName: String = neverJoiningAlias.alias.tableName else { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + return + } + + appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T.Columns: CaseIterable { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + private func generateSelection(for type: T.Type) -> String where T.Columns: CaseIterable { + return T.Columns.allCases + .map { "NULL AS \($0.name)" } + .joined(separator: ", ") + } + + private func generateSelection(for type: T.Type) -> String { + return "SELECT 1" + } } diff --git a/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift new file mode 100644 index 000000000..500131972 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension ScopeAdapter { + static func with( + _ viewModel: VM.Type, + _ scopes: [VM.Columns: RowAdapter] + ) -> ScopeAdapter { + return ScopeAdapter(scopes.reduce(into: [:]) { result, next in result[next.key.name] = next.value }) + } +} From 8e28726fa78800778581aaae1ff56c76d807136d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 17 Aug 2023 18:06:06 +1000 Subject: [PATCH 3/5] Fixed a bug where unwritable conversations appeared in the share extension --- .../SessionThreadViewModel.swift | 22 ++++++++++++++++++- .../ThreadPickerViewModel.swift | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 87a09f337..841f844e9 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -1846,7 +1846,7 @@ public extension SessionThreadViewModel { /// the `contactProfile` 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 = 7 + let numColumnsBeforeProfiles: Int = 8 let request: SQLRequest = """ SELECT @@ -1856,6 +1856,11 @@ public extension SessionThreadViewModel { \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + ( + \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND + \(SQL("\(thread[.id]) != \(userPublicKey)")) AND + IFNULL(\(contact[.isApproved]), false) = false + ) AS \(ViewModel.Columns.threadIsMessageRequest), IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), @@ -1865,8 +1870,23 @@ public extension SessionThreadViewModel { \(closedGroupProfileBack.allColumns), \(closedGroupProfileBackFallback.allColumns), \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND + \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + ) + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), + \(openGroup[.permissions]) AS \(ViewModel.Columns.openGroupPermissions), + + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) diff --git a/SessionShareExtension/ThreadPickerViewModel.swift b/SessionShareExtension/ThreadPickerViewModel.swift index 2d07a43cd..e2bae7488 100644 --- a/SessionShareExtension/ThreadPickerViewModel.swift +++ b/SessionShareExtension/ThreadPickerViewModel.swift @@ -28,6 +28,7 @@ public class ThreadPickerViewModel { .shareQuery(userPublicKey: userPublicKey) .fetchAll(db) } + .map { threads -> [SessionThreadViewModel] in threads.filter { $0.canWrite } } // Exclude unwritable threads .removeDuplicates() .handleEvents(didFail: { SNLog("[ThreadPickerViewModel] Observation failed with error: \($0)") }) From 252e85fef9e3b68cfde8b18be2e88e178b721ec2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 18 Aug 2023 16:14:25 +1000 Subject: [PATCH 4/5] Fixed a bug where the swipe-to-reply wasn't moving the message status text --- Session/Conversations/Message Cells/VisibleMessageCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 3a016fa51..c264472ea 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -52,7 +52,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView, replyButton, timerView, - messageStatusImageView, + messageStatusContainerView, reactionContainerView ] From dfdf843f66fe40f900fed8957fc8298ec92eb481 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 21 Aug 2023 16:13:17 +1000 Subject: [PATCH 5/5] Fixed a few more issues Fixed an issue where tapping a remote notification wasn't opening the conversation Fixed an issue where the Share Extension could fail due to not having a snode pool loaded Fixed an issue where the Theme may not be applied at the right time in the Share extension --- .../ConversationVC+Interaction.swift | 21 ++- .../SendMediaNavigationController.swift | 35 ++-- .../Open Groups/OpenGroupManager.swift | 4 +- .../NSENotificationPresenter.swift | 21 ++- .../NotificationServiceExtension.swift | 1 + .../ShareNavController.swift | 9 +- SessionShareExtension/ThreadPickerVC.swift | 167 +++++++++++------- SessionSnodeKit/Jobs/GetSnodePoolJob.swift | 2 +- SessionSnodeKit/Networking/SnodeAPI.swift | 13 +- .../AttachmentApprovalViewController.swift | 21 ++- 10 files changed, 197 insertions(+), 97 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2b0def3f6..9d7c469a1 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -154,6 +154,7 @@ extension ConversationVC: _ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, + threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies ) { @@ -180,7 +181,14 @@ extension ConversationVC: // MARK: - AttachmentApprovalViewControllerDelegate - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) { + func attachmentApproval( + _ attachmentApproval: AttachmentApprovalViewController, + didApproveAttachments attachments: [SignalAttachment], + forThreadId threadId: String, + threadVariant: SessionThread.Variant, + messageText: String?, + using dependencies: Dependencies + ) { sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies) resetMentions() @@ -255,11 +263,13 @@ extension ConversationVC: func handleLibraryButtonTapped() { let threadId: String = self.viewModel.threadData.threadId + let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant Permissions.requestLibraryPermissionIfNeeded { [weak self] in DispatchQueue.main.async { let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( - threadId: threadId + threadId: threadId, + threadVariant: threadVariant ) sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.modalPresentationStyle = .fullScreen @@ -277,7 +287,10 @@ extension ConversationVC: SNLog("Proceeding without microphone access. Any recorded video will be silent.") } - let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(threadId: self.viewModel.threadData.threadId) + let sendMediaNavController = SendMediaNavigationController.showingCameraFirst( + threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant + ) sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.modalPresentationStyle = .fullScreen @@ -363,6 +376,7 @@ extension ConversationVC: func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { let navController = AttachmentApprovalViewController.wrappedInNavController( threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, attachments: attachments, approvalDelegate: self ) @@ -647,6 +661,7 @@ extension ConversationVC: let approvalVC = AttachmentApprovalViewController.wrappedInNavController( threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, attachments: [ attachment ], approvalDelegate: self ) diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index 32cf44c16..e706d08be 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -18,12 +18,14 @@ class SendMediaNavigationController: UINavigationController { static let bottomButtonsCenterOffset: CGFloat = -50 private let threadId: String + private let threadVariant: SessionThread.Variant private var disposables: Set = Set() // MARK: - Initialization - init(threadId: String) { + init(threadId: String, threadVariant: SessionThread.Variant) { self.threadId = threadId + self.threadVariant = threadVariant super.init(nibName: nil, bundle: nil) } @@ -74,17 +76,15 @@ class SendMediaNavigationController: UINavigationController { public weak var sendMediaNavDelegate: SendMediaNavDelegate? - @objc - public class func showingCameraFirst(threadId: String) -> SendMediaNavigationController { - let navController = SendMediaNavigationController(threadId: threadId) + public class func showingCameraFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController { + let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant) navController.viewControllers = [navController.captureViewController] return navController } - @objc - public class func showingMediaLibraryFirst(threadId: String) -> SendMediaNavigationController { - let navController = SendMediaNavigationController(threadId: threadId) + public class func showingMediaLibraryFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController { + let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant) navController.viewControllers = [navController.mediaLibraryViewController] return navController @@ -233,6 +233,7 @@ class SendMediaNavigationController: UINavigationController { let approvalViewController = AttachmentApprovalViewController( mode: .sharedNavigation, threadId: self.threadId, + threadVariant: self.threadVariant, attachments: self.attachments ) approvalViewController.approvalDelegate = self @@ -431,8 +432,22 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat attachmentDraftCollection.remove(attachment: attachment) } - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) { - sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText, using: dependencies) + func attachmentApproval( + _ attachmentApproval: AttachmentApprovalViewController, + didApproveAttachments attachments: [SignalAttachment], + forThreadId threadId: String, + threadVariant: SessionThread.Variant, + messageText: String?, + using dependencies: Dependencies + ) { + sendMediaNavDelegate?.sendMediaNav( + self, + didApproveAttachments: attachments, + forThreadId: threadId, + threadVariant: threadVariant, + messageText: messageText, + using: dependencies + ) } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { @@ -765,7 +780,7 @@ private class DoneButton: UIView { protocol SendMediaNavDelegate: AnyObject { func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?) - func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) + func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies) func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String? func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?) diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 1af88d959..b45dfcf2c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -786,14 +786,14 @@ public final class OpenGroupManager { } } - if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo { + if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo, let proto: SNProtoContent = processedMessage?.proto { try MessageReceiver.handle( db, threadId: (lookup.sessionId ?? lookup.blindedId), threadVariant: .contact, // Technically not open group messages message: messageInfo.message, serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, - associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), + associatedWithProto: proto, using: dependencies ) } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index f091f4eb6..2a0d12b80 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -45,8 +45,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { .replacingMentions(for: thread.id)) .defaulting(to: "APN_Message".localized()) - var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] - userInfo[NotificationServiceExtension.threadIdKey] = thread.id + let userInfo: [String: Any] = [ + NotificationServiceExtension.isFromRemoteKey: true, + NotificationServiceExtension.threadIdKey: thread.id, + NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue + ] let notificationContent = UNMutableNotificationContent() notificationContent.userInfo = userInfo @@ -145,8 +148,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { // Only notify missed calls guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return } - var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] - userInfo[NotificationServiceExtension.threadIdKey] = thread.id + let userInfo: [String: Any] = [ + NotificationServiceExtension.isFromRemoteKey: true, + NotificationServiceExtension.threadIdKey: thread.id, + NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue + ] let notificationContent = UNMutableNotificationContent() notificationContent.userInfo = userInfo @@ -206,8 +212,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { default: notificationBody = NotificationStrings.incomingMessageBody } - var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] - userInfo[NotificationServiceExtension.threadIdKey] = thread.id + let userInfo: [String: Any] = [ + NotificationServiceExtension.isFromRemoteKey: true, + NotificationServiceExtension.threadIdKey: thread.id, + NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue + ] let notificationContent = UNMutableNotificationContent() notificationContent.userInfo = userInfo diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 22c6f4948..a80b424f5 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -18,6 +18,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension public static let isFromRemoteKey = "remote" public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId" + public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw" public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter" // MARK: Did receive a remote push notification request diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 22c0ce612..1789ab470 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -36,10 +36,6 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { SetCurrentAppContext(appContext) } - // Need to manually trigger these since we don't have a "mainWindow" here and the current theme - // might have been changed since the share extension was last opened - ThemeManager.applySavedTheme() - Logger.info("") _ = AppVersion.sharedInstance() @@ -66,6 +62,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { case .failure: SNLog("[SessionShareExtension] Failed to complete migrations") case .success: DispatchQueue.main.async { + // Need to manually trigger these since we don't have a "mainWindow" here + // and the current theme might have been changed since the share extension + // was last opened + ThemeManager.applySavedTheme() + // performUpdateCheck must be invoked after Environment has been initialized because // upgrade process may depend on Environment. self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync) diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index afc92180a..ced7e707c 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -185,6 +185,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController( threadId: strongSelf.viewModel.viewData[indexPath.row].threadId, + threadVariant: strongSelf.viewModel.viewData[indexPath.row].threadVariant, attachments: attachments, approvalDelegate: strongSelf ) @@ -197,6 +198,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView _ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, + threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies = Dependencies() ) { @@ -221,78 +223,111 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in Storage.resumeDatabaseAccess() - dependencies.storage - .writePublisher { db -> MessageSender.PreparedSendData in - guard - let threadVariant: SessionThread.Variant = try SessionThread - .filter(id: threadId) - .select(.variant) - .asRequest(of: SessionThread.Variant.self) - .fetchOne(db) - else { throw MessageSenderError.noThread } - - // Create the interaction - let interaction: Interaction = try Interaction( - threadId: threadId, - authorId: getUserHexEncodedPublicKey(db), - variant: .standardOutgoing, - body: body, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), - expiresInSeconds: try? DisappearingMessagesConfiguration - .select(.durationSeconds) - .filter(id: threadId) - .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) - .asRequest(of: TimeInterval.self) - .fetchOne(db), - linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) - ).inserted(db) - - guard let interactionId: Int64 = interaction.id else { - throw StorageError.failedToSave + /// When we prepare the message we set the timestamp to be the `SnodeAPI.currentOffsetTimestampMs()` + /// but won't actually have a value because the share extension won't have talked to a service node yet which can cause + /// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate + Just(()) + .setFailureType(to: Error.self) + .flatMap { _ in + // We may not have sufficient snodes, so rather than failing we try to load/fetch + // them if needed + guard !SnodeAPI.hasCachedSnodesIncludingExpired() else { + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() } - // If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing - // one then add it now - if - isSharingUrl, - let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft, - (try? interaction.linkPreview.isEmpty(db)) == true - { - try LinkPreview( - url: linkPreviewDraft.urlString, - title: linkPreviewDraft.title, - attachmentId: LinkPreview - .generateAttachmentIfPossible( - imageData: linkPreviewDraft.jpegImageData, - mimeType: OWSMimeTypeImageJpeg - )? - .inserted(db) - .id - ).insert(db) - } - - // Prepare any attachments - try Attachment.process( - db, - data: Attachment.prepare(attachments: finalAttachments), - for: interactionId - ) - - // Prepare the message send data - return try MessageSender - .preparedSendData( - db, - interaction: interaction, - threadId: threadId, - threadVariant: threadVariant, - using: dependencies - ) + return SnodeAPI.getSnodePool() + .map { _ in () } + .eraseToAnyPublisher() } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .flatMap { _ in + SnodeAPI + .getSwarm( + for: { + switch threadVariant { + case .contact, .legacyGroup, .group: return threadId + case .community: return getUserHexEncodedPublicKey(using: dependencies) + } + }(), + using: dependencies + ) + .tryFlatMapWithRandomSnode { SnodeAPI.getNetworkTime(from: $0, using: dependencies) } + .map { _ in () } + .eraseToAnyPublisher() + } + .flatMap { _ in + dependencies.storage.writePublisher { db -> MessageSender.PreparedSendData in + guard + let threadVariant: SessionThread.Variant = try SessionThread + .filter(id: threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db) + else { throw MessageSenderError.noThread } + + // Create the interaction + let interaction: Interaction = try Interaction( + threadId: threadId, + authorId: getUserHexEncodedPublicKey(db), + variant: .standardOutgoing, + body: body, + timestampMs: SnodeAPI.currentOffsetTimestampMs(), + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db), + linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) + ).inserted(db) + + guard let interactionId: Int64 = interaction.id else { + throw StorageError.failedToSave + } + + // If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing + // one then add it now + if + isSharingUrl, + let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft, + (try? interaction.linkPreview.isEmpty(db)) == true + { + try LinkPreview( + url: linkPreviewDraft.urlString, + title: linkPreviewDraft.title, + attachmentId: LinkPreview + .generateAttachmentIfPossible( + imageData: linkPreviewDraft.jpegImageData, + mimeType: OWSMimeTypeImageJpeg + )? + .inserted(db) + .id + ).insert(db) + } + + // Prepare any attachments + try Attachment.process( + db, + data: Attachment.prepare(attachments: finalAttachments), + for: interactionId + ) + + // Prepare the message send data + return try MessageSender + .preparedSendData( + db, + interaction: interaction, + threadId: threadId, + threadVariant: threadVariant, + using: dependencies + ) + } + } .flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0, using: dependencies) } .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sinkUntilComplete( receiveCompletion: { [weak self] result in diff --git a/SessionSnodeKit/Jobs/GetSnodePoolJob.swift b/SessionSnodeKit/Jobs/GetSnodePoolJob.swift index 22cdad93d..72844ff82 100644 --- a/SessionSnodeKit/Jobs/GetSnodePoolJob.swift +++ b/SessionSnodeKit/Jobs/GetSnodePoolJob.swift @@ -23,7 +23,7 @@ public enum GetSnodePoolJob: JobExecutor { // but we want to succeed this job immediately (since it's marked as blocking), this allows us // to block if we have no Snode pool and prevent other jobs from failing but avoids having to // wait if we already have a potentially valid snode pool - guard !SnodeAPI.hasCachedSnodesInclusingExpired() else { + guard !SnodeAPI.hasCachedSnodesIncludingExpired() else { SNLog("[GetSnodePoolJob] Has valid cached pool, running async instead") SnodeAPI .getSnodePool() diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index 159d13fad..a2e9af6a8 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -141,7 +141,7 @@ public final class SnodeAPI { // MARK: - Public API - public static func hasCachedSnodesInclusingExpired() -> Bool { + public static func hasCachedSnodesIncludingExpired() -> Bool { loadSnodePoolIfNeeded() return !hasInsufficientSnodes @@ -1009,7 +1009,7 @@ public final class SnodeAPI { // MARK: - Internal API - private static func getNetworkTime( + public static func getNetworkTime( from snode: Snode, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { @@ -1024,7 +1024,14 @@ public final class SnodeAPI { using: dependencies ) .decoded(as: GetNetworkTimestampResponse.self, using: dependencies) - .map { _, response in response.timestamp } + .map { _, response in + // Assume we've fetched the networkTime in order to send a message to the specified snode, in + // which case we want to update the 'clockOffsetMs' value for subsequent requests + let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + SnodeAPI.clockOffsetMs.mutate { $0 = offset } + + return response.timestamp + } .eraseToAnyPublisher() } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index a6b1a8782..77b13ab15 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -16,6 +16,7 @@ public protocol AttachmentApprovalViewControllerDelegate: AnyObject { _ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, + threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies ) @@ -59,6 +60,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC private let mode: Mode private let threadId: String + private let threadVariant: SessionThread.Variant private let isAddMoreVisible: Bool public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? @@ -128,11 +130,13 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC required public init( mode: Mode, threadId: String, + threadVariant: SessionThread.Variant, attachments: [SignalAttachment] ) { assert(attachments.count > 0) self.mode = mode self.threadId = threadId + self.threadVariant = threadVariant let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} self.isAddMoreVisible = (mode == .sharedNavigation) @@ -162,10 +166,16 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC public class func wrappedInNavController( threadId: String, + threadVariant: SessionThread.Variant, attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate ) -> UINavigationController { - let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments) + let vc = AttachmentApprovalViewController( + mode: .modal, + threadId: threadId, + threadVariant: threadVariant, + attachments: attachments + ) vc.approvalDelegate = approvalDelegate let navController = StyledNavigationController(rootViewController: vc) @@ -674,7 +684,14 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { attachmentTextToolbar.isUserInteractionEnabled = false attachmentTextToolbar.isHidden = true - approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: attachmentTextToolbar.messageText, using: dependencies) + approvalDelegate?.attachmentApproval( + self, + didApproveAttachments: attachments, + forThreadId: threadId, + threadVariant: threadVariant, + messageText: attachmentTextToolbar.messageText, + using: dependencies + ) } func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) {