Merge pull request #888 from mpretty-cyro/fix/minor-outgoing-quote-bugs

Fix a couple of minor bugs and clean up query interface
This commit is contained in:
Morgan Pretty 2023-08-30 13:57:49 +10:00 committed by GitHub
commit 6d990559b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1152 additions and 788 deletions

View File

@ -635,6 +635,8 @@
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; }; FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; };
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.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 */; }; 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 */; }; 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 */; }; 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 */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; };
@ -1755,6 +1757,8 @@
FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = "<group>"; }; FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = "<group>"; };
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; }; FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; };
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = "<group>"; };
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = "<group>"; };
FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = "<group>"; }; FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = "<group>"; };
FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = "<group>"; }; FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = "<group>"; };
FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = "<group>"; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = "<group>"; };
@ -3693,6 +3697,8 @@
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */,
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */,
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */,
); );
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5666,6 +5672,7 @@
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */,
FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */,
FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */,
FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */,
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */,
FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */,
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
@ -5677,6 +5684,7 @@
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */,
FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */,
FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */,
FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */,
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,

View File

@ -154,6 +154,7 @@ extension ConversationVC:
_ sendMediaNavigationController: SendMediaNavigationController, _ sendMediaNavigationController: SendMediaNavigationController,
didApproveAttachments attachments: [SignalAttachment], didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String, forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?, messageText: String?,
using dependencies: Dependencies using dependencies: Dependencies
) { ) {
@ -180,7 +181,14 @@ extension ConversationVC:
// MARK: - AttachmentApprovalViewControllerDelegate // 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) sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies)
resetMentions() resetMentions()
@ -255,11 +263,13 @@ extension ConversationVC:
func handleLibraryButtonTapped() { func handleLibraryButtonTapped() {
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Permissions.requestLibraryPermissionIfNeeded { [weak self] in Permissions.requestLibraryPermissionIfNeeded { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
threadId: threadId threadId: threadId,
threadVariant: threadVariant
) )
sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.sendMediaNavDelegate = self
sendMediaNavController.modalPresentationStyle = .fullScreen sendMediaNavController.modalPresentationStyle = .fullScreen
@ -277,7 +287,10 @@ extension ConversationVC:
SNLog("Proceeding without microphone access. Any recorded video will be silent.") 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.sendMediaNavDelegate = self
sendMediaNavController.modalPresentationStyle = .fullScreen sendMediaNavController.modalPresentationStyle = .fullScreen
@ -363,6 +376,7 @@ extension ConversationVC:
func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) {
let navController = AttachmentApprovalViewController.wrappedInNavController( let navController = AttachmentApprovalViewController.wrappedInNavController(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
attachments: attachments, attachments: attachments,
approvalDelegate: self approvalDelegate: self
) )
@ -647,6 +661,7 @@ extension ConversationVC:
let approvalVC = AttachmentApprovalViewController.wrappedInNavController( let approvalVC = AttachmentApprovalViewController.wrappedInNavController(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
attachments: [ attachment ], attachments: [ attachment ],
approvalDelegate: self approvalDelegate: self
) )

View File

@ -156,7 +156,7 @@ final class QuoteView: UIView {
if attachment.isVisualMedia { if attachment.isVisualMedia {
attachment.thumbnail( attachment.thumbnail(
size: .small, size: .small,
success: { image, _ in success: { [imageView] image, _ in
guard Thread.isMainThread else { guard Thread.isMainThread else {
DispatchQueue.main.async { DispatchQueue.main.async {
imageView.image = image imageView.image = image
@ -234,8 +234,6 @@ final class QuoteView: UIView {
} }
// Label stack view // Label stack view
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
let isCurrentUser: Bool = [ let isCurrentUser: Bool = [
currentUserPublicKey, currentUserPublicKey,
currentUserBlinded15PublicKey, currentUserBlinded15PublicKey,
@ -288,9 +286,8 @@ final class QuoteView: UIView {
cancelButton.set(.height, to: cancelButtonSize) cancelButton.set(.height, to: cancelButtonSize)
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside) cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
addSubview(cancelButton) mainStackView.addArrangedSubview(cancelButton)
cancelButton.center(.vertical, in: self) cancelButton.center(.vertical, in: self)
cancelButton.pin(.right, to: .right, of: self)
} }
} }

View File

@ -52,7 +52,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
profilePictureView, profilePictureView,
replyButton, replyButton,
timerView, timerView,
messageStatusImageView, messageStatusContainerView,
reactionContainerView reactionContainerView
] ]

View File

@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
]) ])
} }
catch { 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) return .failure(error)
} }
} }

View File

@ -199,16 +199,18 @@ public class MediaGalleryViewModel {
} }
} }
public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible {
fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) public typealias Columns = CodingKeys
fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) case interactionId
fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) case interactionVariant
fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) case interactionAuthorId
fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) case interactionTimestampMs
fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue)
case rowId
fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue case attachmentAlbumIndex
case attachment
}
public var id: String { attachment.id } public var id: String { attachment.id }
public var differenceIdentifier: String { attachment.id } public var differenceIdentifier: String { attachment.id }
@ -306,7 +308,7 @@ public class MediaGalleryViewModel {
let finalFilterSQL: SQL = { let finalFilterSQL: SQL = {
guard let customFilters: SQL = customFilters else { guard let customFilters: SQL = customFilters else {
return """ return """
WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) WHERE \(attachment[.rowId]) IN \(rowIds)
""" """
} }
@ -318,14 +320,14 @@ public class MediaGalleryViewModel {
}() }()
let request: SQLRequest<Item> = """ let request: SQLRequest<Item> = """
SELECT SELECT
\(interaction[.id]) AS \(Item.interactionIdKey), \(interaction[.id]) AS \(Item.Columns.interactionId),
\(interaction[.variant]) AS \(Item.interactionVariantKey), \(interaction[.variant]) AS \(Item.Columns.interactionVariant),
\(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId),
\(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs),
\(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), \(attachment[.rowId]) AS \(Item.Columns.rowId),
\(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex),
\(Item.attachmentKey).* \(attachment.allColumns)
FROM \(Attachment.self) FROM \(Attachment.self)
\(joinSQL) \(joinSQL)
\(finalFilterSQL) \(finalFilterSQL)
@ -338,8 +340,8 @@ public class MediaGalleryViewModel {
Attachment.numberOfSelectedColumns(db) Attachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(Item.self, [
Item.attachmentString: adapters[1] .attachment: adapters[1]
]) ])
} }
} }

View File

@ -18,12 +18,14 @@ class SendMediaNavigationController: UINavigationController {
static let bottomButtonsCenterOffset: CGFloat = -50 static let bottomButtonsCenterOffset: CGFloat = -50
private let threadId: String private let threadId: String
private let threadVariant: SessionThread.Variant
private var disposables: Set<AnyCancellable> = Set() private var disposables: Set<AnyCancellable> = Set()
// MARK: - Initialization // MARK: - Initialization
init(threadId: String) { init(threadId: String, threadVariant: SessionThread.Variant) {
self.threadId = threadId self.threadId = threadId
self.threadVariant = threadVariant
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@ -74,17 +76,15 @@ class SendMediaNavigationController: UINavigationController {
public weak var sendMediaNavDelegate: SendMediaNavDelegate? public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc public class func showingCameraFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
public class func showingCameraFirst(threadId: String) -> SendMediaNavigationController { let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.captureViewController] navController.viewControllers = [navController.captureViewController]
return navController return navController
} }
@objc public class func showingMediaLibraryFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
public class func showingMediaLibraryFirst(threadId: String) -> SendMediaNavigationController { let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.mediaLibraryViewController] navController.viewControllers = [navController.mediaLibraryViewController]
return navController return navController
@ -233,6 +233,7 @@ class SendMediaNavigationController: UINavigationController {
let approvalViewController = AttachmentApprovalViewController( let approvalViewController = AttachmentApprovalViewController(
mode: .sharedNavigation, mode: .sharedNavigation,
threadId: self.threadId, threadId: self.threadId,
threadVariant: self.threadVariant,
attachments: self.attachments attachments: self.attachments
) )
approvalViewController.approvalDelegate = self approvalViewController.approvalDelegate = self
@ -431,8 +432,22 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
attachmentDraftCollection.remove(attachment: attachment) attachmentDraftCollection.remove(attachment: attachment)
} }
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) { func attachmentApproval(
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText, using: dependencies) _ 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) { func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -765,7 +780,7 @@ private class DoneButton: UIView {
protocol SendMediaNavDelegate: AnyObject { protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?) 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 sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?) func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)

View File

@ -258,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
// MARK: - DataModel // MARK: - DataModel
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
public static let profileString: String = CodingKeys.profile.stringValue case profile
}
public var differenceIdentifier: String { profile.id } public var differenceIdentifier: String { profile.id }
public var id: String { profile.id } public var id: String { profile.id }
@ -286,11 +287,11 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
let request: SQLRequest<DataModel> = """ let request: SQLRequest<DataModel> = """
SELECT SELECT
\(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), \(profile[.rowId]) AS \(DataModel.Columns.rowId),
\(DataModel.profileKey).* \(profile.allColumns)
FROM \(Profile.self) FROM \(Profile.self)
WHERE \(profile.alias[Column.rowID]) IN \(rowIds) WHERE \(profile[.rowId]) IN \(rowIds)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
""" """
@ -300,8 +301,8 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(DataModel.self, [
DataModel.profileString: adapters[1] .profile: adapters[1]
]) ])
} }
} }

View File

@ -522,7 +522,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
( (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
\(Interaction.linkPreviewFilterLiteral) \(Interaction.linkPreviewFilterLiteral())
) )
) )
@ -568,7 +568,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
( (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
\(Interaction.linkPreviewFilterLiteral) \(Interaction.linkPreviewFilterLiteral())
) )
) )

View File

@ -95,6 +95,19 @@ public extension ClosedGroup {
} }
} }
// MARK: - Search Queries
public extension ClosedGroup {
struct FullTextSearch: Decodable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case name
}
let name: String
}
}
// MARK: - Convenience // MARK: - Convenience
public extension ClosedGroup { public extension ClosedGroup {

View File

@ -29,13 +29,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
/// Whenever using this `linkPreview` association make sure to filter the result using /// Whenever using this `linkPreview` association make sure to filter the result using
/// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned /// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey) public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
public static var linkPreviewFilterLiteral: SQL = { public static func linkPreviewFilterLiteral(
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() interaction: TypedTableAlias<Interaction> = TypedTableAlias(),
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias() linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
) -> SQL {
let halfResolution: Double = LinkPreview.timstampResolution let halfResolution: Double = LinkPreview.timstampResolution
return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) * 1000 AND (\(linkPreview[.timestamp]) + \(halfResolution)) * 1000)" return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) * 1000 AND (\(linkPreview[.timestamp]) + \(halfResolution)) * 1000)"
}() }
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey) public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
public typealias Columns = CodingKeys public typealias Columns = CodingKeys
@ -695,6 +696,17 @@ public extension Interaction {
// MARK: - Search Queries // MARK: - Search Queries
public extension Interaction { public extension Interaction {
struct FullTextSearch: Decodable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case threadId
case body
}
let threadId: String
let body: String
}
struct TimestampInfo: FetchableRecord, Codable { struct TimestampInfo: FetchableRecord, Codable {
public let id: Int64 public let id: Int64
public let timestampMs: Int64 public let timestampMs: Int64
@ -710,8 +722,7 @@ public extension Interaction {
static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest<TimestampInfo> { static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest<TimestampInfo> {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) let interactionFullTextSearch: TypedTableAlias<FullTextSearch> = TypedTableAlias(name: Interaction.fullTextSearchTableName)
let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
let request: SQLRequest<TimestampInfo> = """ let request: SQLRequest<TimestampInfo> = """
SELECT SELECT
@ -719,9 +730,9 @@ public extension Interaction {
\(interaction[.timestampMs]) \(interaction[.timestampMs])
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(interactionFullTextSearch) ON ( JOIN \(interactionFullTextSearch) ON (
\(interactionFullTextSearch).rowid = \(interaction.alias[Column.rowID]) AND \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND
\(SQL("\(interactionFullTextSearch).\(threadIdLiteral) = \(threadId)")) AND \(SQL("\(interactionFullTextSearch[.threadId]) = \(threadId)")) AND
\(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) \(interactionFullTextSearch[.body]) MATCH \(pattern)
) )
ORDER BY \(interaction[.timestampMs].desc) ORDER BY \(interaction[.timestampMs].desc)

View File

@ -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 // MARK: - Convenience
public extension OpenGroup { public extension OpenGroup {

View File

@ -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 // MARK: - Convenience
public extension Profile { public extension Profile {

View File

@ -365,7 +365,7 @@ public extension SessionThread {
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
return """ return """
SELECT \(thread.allColumns()) SELECT \(thread.allColumns)
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
WHERE ( WHERE (

View File

@ -82,7 +82,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Interaction.self) DELETE FROM \(Interaction.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(interaction.alias[Column.rowID]) SELECT \(interaction[.rowId])
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON ( JOIN \(SessionThread.self) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
@ -90,7 +90,7 @@ public enum GarbageCollectionJob: JobExecutor {
) )
JOIN ( JOIN (
SELECT SELECT
COUNT(\(interaction.alias[Column.rowID])) AS interactionCount, COUNT(\(interaction[.rowId])) AS interactionCount,
\(interaction[.threadId]) \(interaction[.threadId])
FROM \(Interaction.self) FROM \(Interaction.self)
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
@ -112,7 +112,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Job.self) DELETE FROM \(Job.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(job.alias[Column.rowID]) SELECT \(job[.rowId])
FROM \(Job.self) FROM \(Job.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId])
LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId]) LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId])
@ -139,11 +139,11 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(LinkPreview.self) DELETE FROM \(LinkPreview.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(linkPreview.alias[Column.rowID]) SELECT \(linkPreview[.rowId])
FROM \(LinkPreview.self) FROM \(LinkPreview.self)
LEFT JOIN \(Interaction.self) ON ( LEFT JOIN \(Interaction.self) ON (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
\(Interaction.linkPreviewFilterLiteral) \(Interaction.linkPreviewFilterLiteral())
) )
WHERE \(interaction[.id]) IS NULL WHERE \(interaction[.id]) IS NULL
) )
@ -159,7 +159,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(OpenGroup.self) DELETE FROM \(OpenGroup.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(openGroup.alias[Column.rowID]) SELECT \(openGroup[.rowId])
FROM \(OpenGroup.self) FROM \(OpenGroup.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId])
WHERE ( WHERE (
@ -178,7 +178,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Capability.self) DELETE FROM \(Capability.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(capability.alias[Column.rowID]) SELECT \(capability[.rowId])
FROM \(Capability.self) FROM \(Capability.self)
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer])
WHERE \(openGroup[.threadId]) IS NULL WHERE \(openGroup[.threadId]) IS NULL
@ -195,7 +195,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(BlindedIdLookup.self) DELETE FROM \(BlindedIdLookup.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(blindedIdLookup.alias[Column.rowID]) SELECT \(blindedIdLookup[.rowId])
FROM \(BlindedIdLookup.self) FROM \(BlindedIdLookup.self)
LEFT JOIN \(SessionThread.self) ON ( LEFT JOIN \(SessionThread.self) ON (
\(thread[.id]) = \(blindedIdLookup[.blindedId]) OR \(thread[.id]) = \(blindedIdLookup[.blindedId]) OR
@ -222,7 +222,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Contact.self) DELETE FROM \(Contact.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(contact.alias[Column.rowID]) SELECT \(contact[.rowId])
FROM \(Contact.self) FROM \(Contact.self)
LEFT JOIN \(BlindedIdLookup.self) ON ( LEFT JOIN \(BlindedIdLookup.self) ON (
\(blindedIdLookup[.blindedId]) = \(contact[.id]) AND \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND
@ -243,7 +243,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Attachment.self) DELETE FROM \(Attachment.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(attachment.alias[Column.rowID]) SELECT \(attachment[.rowId])
FROM \(Attachment.self) FROM \(Attachment.self)
LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id])
LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id])
@ -269,7 +269,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Profile.self) DELETE FROM \(Profile.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(profile.alias[Column.rowID]) SELECT \(profile[.rowId])
FROM \(Profile.self) FROM \(Profile.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id])
LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id]) LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id])
@ -310,7 +310,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(SessionThread.self) DELETE FROM \(SessionThread.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(thread.alias[Column.rowID]) SELECT \(thread[.rowId])
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])

View File

@ -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( try MessageReceiver.handle(
db, db,
threadId: (lookup.sessionId ?? lookup.blindedId), threadId: (lookup.sessionId ?? lookup.blindedId),
threadVariant: .contact, // Technically not open group messages threadVariant: .contact, // Technically not open group messages
message: messageInfo.message, message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), associatedWithProto: proto,
using: dependencies using: dependencies
) )
} }

View File

@ -3,12 +3,14 @@
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
public struct MentionInfo: FetchableRecord, Decodable { public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible {
fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) public typealias Columns = CodingKeys
fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) case profile
case threadVariant
fileprivate static let profileString: String = CodingKeys.profile.stringValue case openGroupServer
case openGroupRoomToken
}
public let profile: Profile public let profile: Profile
public let threadVariant: SessionThread.Variant public let threadVariant: SessionThread.Variant
@ -79,7 +81,7 @@ public extension MentionInfo {
return SQLRequest(""" return SQLRequest("""
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)"))
\(targetJoin) \(targetJoin)
\(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)"))
@ -89,7 +91,7 @@ public extension MentionInfo {
return SQLRequest(""" return SQLRequest("""
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)"))
\(targetJoin) \(targetJoin)
JOIN \(GroupMember.self) ON ( JOIN \(GroupMember.self) ON (
@ -107,9 +109,9 @@ public extension MentionInfo {
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting)
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")),
\(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer),
\(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken)
\(targetJoin) \(targetJoin)
JOIN \(Interaction.self) ON ( JOIN \(Interaction.self) ON (
@ -130,8 +132,8 @@ public extension MentionInfo {
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(MentionInfo.self, [
MentionInfo.profileString: adapters[0] .profile: adapters[0]
]) ])
} }
} }

View File

@ -11,42 +11,66 @@ fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInt
fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo
fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo
public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) public typealias Columns = CodingKeys
public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let threadIsTrustedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsTrusted.stringValue) case threadId
public static let threadHasDisappearingMessagesEnabledKey: SQL = SQL(stringLiteral: CodingKeys.threadHasDisappearingMessagesEnabled.stringValue) case threadVariant
public static let threadOpenGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupServer.stringValue) case threadIsTrusted
public static let threadOpenGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupPublicKey.stringValue) case threadHasDisappearingMessagesEnabled
public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) case threadOpenGroupServer
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) case threadOpenGroupPublicKey
public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) case threadContactNameInternal
public static let stateKey: SQL = SQL(stringLiteral: CodingKeys.state.stringValue)
public static let hasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.hasAtLeastOneReadReceipt.stringValue) // Interaction Info
public static let mostRecentFailureTextKey: SQL = SQL(stringLiteral: CodingKeys.mostRecentFailureText.stringValue)
public static let isTypingIndicatorKey: SQL = SQL(stringLiteral: CodingKeys.isTypingIndicator.stringValue) case rowId
public static let isSenderOpenGroupModeratorKey: SQL = SQL(stringLiteral: CodingKeys.isSenderOpenGroupModerator.stringValue) case id
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) case openGroupServerMessageId
public static let quoteKey: SQL = SQL(stringLiteral: CodingKeys.quote.stringValue) case variant
public static let quoteAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.quoteAttachment.stringValue) case timestampMs
public static let linkPreviewKey: SQL = SQL(stringLiteral: CodingKeys.linkPreview.stringValue) case receivedAtTimestampMs
public static let linkPreviewAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.linkPreviewAttachment.stringValue) case authorId
public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) case authorNameInternal
public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) case body
public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) case rawBody
public static let canHaveProfileKey: SQL = SQL(stringLiteral: CodingKeys.canHaveProfile.stringValue) case expiresStartedAtMs
public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) case expiresInSeconds
public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue)
public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) case state
public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) case hasAtLeastOneReadReceipt
public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) case mostRecentFailureText
public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) case isSenderOpenGroupModerator
case isTypingIndicator
public static let profileString: String = CodingKeys.profile.stringValue case profile
public static let quoteString: String = CodingKeys.quote.stringValue case quote
public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue case quoteAttachment
public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue case linkPreview
public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue 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 { public enum CellType: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible {
case textOnlyMessage case textOnlyMessage
@ -462,13 +486,13 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
// MARK: - AttachmentInteractionInfo // MARK: - AttachmentInteractionInfo
public extension MessageViewModel { public extension MessageViewModel {
struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable { struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let interactionAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachment.stringValue) case rowId
case attachment
public static let attachmentString: String = CodingKeys.attachment.stringValue case interactionAttachment
public static let interactionAttachmentString: String = CodingKeys.interactionAttachment.stringValue }
public let rowId: Int64 public let rowId: Int64
public let attachment: Attachment public let attachment: Attachment
@ -491,13 +515,13 @@ public extension MessageViewModel {
// MARK: - ReactionInfo // MARK: - ReactionInfo
public extension MessageViewModel { public extension MessageViewModel {
struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) case rowId
case reaction
public static let reactionString: String = CodingKeys.reaction.stringValue case profile
public static let profileString: String = CodingKeys.profile.stringValue }
public let rowId: Int64 public let rowId: Int64
public let reaction: Reaction public let reaction: Reaction
@ -522,9 +546,12 @@ public extension MessageViewModel {
// MARK: - TypingIndicatorInfo // MARK: - TypingIndicatorInfo
public extension MessageViewModel { public extension MessageViewModel {
struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
case threadId
}
public let rowId: Int64 public let rowId: Int64
public let threadId: String public let threadId: String
@ -776,59 +803,48 @@ public extension MessageViewModel {
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias() let profile: TypedTableAlias<Profile> = TypedTableAlias()
let threadProfile: TypedTableAlias<Profile> = TypedTableAlias(name: "threadProfile")
let quote: TypedTableAlias<Quote> = TypedTableAlias() let quote: TypedTableAlias<Quote> = TypedTableAlias()
let quoteInteraction: TypedTableAlias<Interaction> = TypedTableAlias(name: "quoteInteraction")
let quoteInteractionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias(
name: "quoteInteractionAttachment"
)
let quoteLinkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias(name: "quoteLinkPreview")
let quoteAttachment: TypedTableAlias<Attachment> = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue)
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias() let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let linkPreviewAttachment: TypedTableAlias<Attachment> = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment)
let threadProfile: SQL = SQL(stringLiteral: "threadProfile") let readReceipt: TypedTableAlias<RecipientState> = TypedTableAlias(name: "readReceipt")
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)
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 numColumnsBeforeLinkedRecords: Int = 22 let numColumnsBeforeLinkedRecords: Int = 22
let finalGroupSQL: SQL = (groupSQL ?? "") let finalGroupSQL: SQL = (groupSQL ?? "")
let request: SQLRequest<ViewModel> = """ let request: SQLRequest<ViewModel> = """
SELECT SELECT
\(thread[.id]) AS \(ViewModel.threadIdKey), \(thread[.id]) AS \(ViewModel.Columns.threadId),
\(thread[.variant]) AS \(ViewModel.threadVariantKey), \(thread[.variant]) AS \(ViewModel.Columns.threadVariant),
-- Default to 'true' for non-contact threads -- 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 -- Default to 'false' when no contact exists
IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.Columns.threadHasDisappearingMessagesEnabled),
\(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), \(openGroup[.server]) AS \(ViewModel.Columns.threadOpenGroupServer),
\(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), \(openGroup[.publicKey]) AS \(ViewModel.Columns.threadOpenGroupPublicKey),
IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), 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[.id]),
\(interaction[.openGroupServerMessageId]), \(interaction[.openGroupServerMessageId]),
\(interaction[.variant]), \(interaction[.variant]),
\(interaction[.timestampMs]), \(interaction[.timestampMs]),
\(interaction[.receivedAtTimestampMs]), \(interaction[.receivedAtTimestampMs]),
\(interaction[.authorId]), \(interaction[.authorId]),
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal),
\(interaction[.body]), \(interaction[.body]),
\(interaction[.expiresStartedAtMs]), \(interaction[.expiresStartedAtMs]),
\(interaction[.expiresInSeconds]), \(interaction[.expiresInSeconds]),
-- Default to 'sending' assuming non-processed interaction when null -- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.state),
(\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.hasAtLeastOneReadReceipt),
\(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), \(recipientState[.mostRecentFailureText]) AS \(ViewModel.Columns.mostRecentFailureText),
EXISTS ( EXISTS (
SELECT 1 SELECT 1
@ -839,46 +855,46 @@ public extension MessageViewModel {
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])"))
) )
) AS \(ViewModel.isSenderOpenGroupModeratorKey), ) AS \(ViewModel.Columns.isSenderOpenGroupModerator),
\(ViewModel.profileKey).*, \(profile.allColumns),
\(quote[.interactionId]), \(quote[.interactionId]),
\(quote[.authorId]), \(quote[.authorId]),
\(quote[.timestampMs]), \(quote[.timestampMs]),
\(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn), \(quoteInteraction[.body]),
\(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), \(quoteInteractionAttachment[.attachmentId]),
\(ViewModel.quoteAttachmentKey).*, \(quoteAttachment.allColumns),
\(ViewModel.linkPreviewKey).*, \(linkPreview.allColumns),
\(ViewModel.linkPreviewAttachmentKey).*, \(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 -- 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 -- query from crashing when decoding we need to provide default values
\(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), \(CellType.textOnlyMessage) AS \(ViewModel.Columns.cellType),
'' AS \(ViewModel.authorNameKey), '' AS \(ViewModel.Columns.authorName),
false AS \(ViewModel.canHaveProfileKey), false AS \(ViewModel.Columns.canHaveProfile),
false AS \(ViewModel.shouldShowProfileKey), false AS \(ViewModel.Columns.shouldShowProfile),
false AS \(ViewModel.shouldShowDateHeaderKey), false AS \(ViewModel.Columns.shouldShowDateHeader),
\(Position.middle) AS \(ViewModel.positionInClusterKey), \(Position.middle) AS \(ViewModel.Columns.positionInCluster),
false AS \(ViewModel.isOnlyMessageInClusterKey), false AS \(ViewModel.Columns.isOnlyMessageInCluster),
false AS \(ViewModel.isLastKey), false AS \(ViewModel.Columns.isLast),
false AS \(ViewModel.isLastOutgoingKey) false AS \(ViewModel.Columns.isLastOutgoing)
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])
LEFT JOIN \(Contact.self) ON \(contact[.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 \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id]) LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id])
LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON ( LEFT JOIN \(quoteInteraction) ON (
\(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND ( \(quoteInteraction[.timestampMs]) = \(quote[.timestampMs]) AND (
\(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR ( \(quoteInteraction[.authorId]) = \(quote[.authorId]) OR (
-- A users outgoing message is stored in some cases using their standard id -- 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 -- 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]) = \(blinded15PublicKey ?? "''") OR
\(quote[.authorId]) = \(blinded25PublicKey ?? "''") \(quote[.authorId]) = \(blinded25PublicKey ?? "''")
@ -886,27 +902,38 @@ public extension MessageViewModel {
) )
) )
) )
LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( LEFT JOIN \(quoteInteractionAttachment) ON (
\(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND \(quoteInteractionAttachment[.interactionId]) = \(quoteInteraction[.id]) AND
\(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 \(quoteInteractionAttachment[.albumIndex]) = 0
)
LEFT JOIN \(quoteLinkPreview) ON (
\(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND
\(Interaction.linkPreviewFilterLiteral(
interaction: quoteInteraction,
linkPreview: quoteLinkPreview
))
)
LEFT JOIN \(quoteAttachment) ON (
\(quoteAttachment[.id]) = \(quoteInteractionAttachment[.attachmentId]) OR
\(quoteAttachment[.id]) = \(quoteLinkPreview[.attachmentId]) OR
\(quoteAttachment[.id]) = \(quote[.attachmentId])
) )
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn)
LEFT JOIN \(LinkPreview.self) ON ( LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(Interaction.linkPreviewFilterLiteral) \(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 ( LEFT JOIN \(RecipientState.self) ON (
-- Ignore 'skipped' states -- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
\(recipientState[.interactionId]) = \(interaction[.id]) \(recipientState[.interactionId]) = \(interaction[.id])
) )
LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( LEFT JOIN \(readReceipt) ON (
\(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND \(readReceipt[.readTimestampMs]) IS NOT NULL AND
\(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) \(readReceipt[.interactionId]) = \(interaction[.id])
) )
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) WHERE \(interaction[.rowId]) IN \(rowIds)
\(finalGroupSQL) \(finalGroupSQL)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
""" """
@ -921,12 +948,12 @@ public extension MessageViewModel {
Attachment.numberOfSelectedColumns(db) Attachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(ViewModel.self, [
ViewModel.profileString: adapters[1], .profile: adapters[1],
ViewModel.quoteString: adapters[2], .quote: adapters[2],
ViewModel.quoteAttachmentString: adapters[3], .quoteAttachment: adapters[3],
ViewModel.linkPreviewString: adapters[4], .linkPreview: adapters[4],
ViewModel.linkPreviewAttachmentString: adapters[5] .linkPreviewAttachment: adapters[5]
]) ])
} }
} }
@ -953,9 +980,9 @@ public extension MessageViewModel.AttachmentInteractionInfo {
let numColumnsBeforeLinkedRecords: Int = 1 let numColumnsBeforeLinkedRecords: Int = 1
let request: SQLRequest<AttachmentInteractionInfo> = """ let request: SQLRequest<AttachmentInteractionInfo> = """
SELECT SELECT
\(attachment.alias[Column.rowID]) AS \(AttachmentInteractionInfo.rowIdKey), \(attachment[.rowId]) AS \(AttachmentInteractionInfo.Columns.rowId),
\(AttachmentInteractionInfo.attachmentKey).*, \(attachment.allColumns),
\(AttachmentInteractionInfo.interactionAttachmentKey).* \(interactionAttachment.allColumns)
FROM \(Attachment.self) FROM \(Attachment.self)
JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id]) JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
\(finalFilterSQL) \(finalFilterSQL)
@ -968,9 +995,9 @@ public extension MessageViewModel.AttachmentInteractionInfo {
InteractionAttachment.numberOfSelectedColumns(db) InteractionAttachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(AttachmentInteractionInfo.self, [
AttachmentInteractionInfo.attachmentString: adapters[1], .attachment: adapters[1],
AttachmentInteractionInfo.interactionAttachmentString: adapters[2] .interactionAttachment: adapters[2]
]) ])
} }
} }
@ -1034,9 +1061,9 @@ public extension MessageViewModel.ReactionInfo {
let numColumnsBeforeLinkedRecords: Int = 1 let numColumnsBeforeLinkedRecords: Int = 1
let request: SQLRequest<ReactionInfo> = """ let request: SQLRequest<ReactionInfo> = """
SELECT SELECT
\(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), \(reaction[.rowId]) AS \(ReactionInfo.Columns.rowId),
\(ReactionInfo.reactionKey).*, \(reaction.allColumns),
\(ReactionInfo.profileKey).* \(profile.allColumns)
FROM \(Reaction.self) FROM \(Reaction.self)
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId])
\(finalFilterSQL) \(finalFilterSQL)
@ -1049,9 +1076,9 @@ public extension MessageViewModel.ReactionInfo {
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(ReactionInfo.self, [
ReactionInfo.reactionString: adapters[1], .reaction: adapters[1],
ReactionInfo.profileString: adapters[2] .profile: adapters[2]
]) ])
} }
} }
@ -1117,8 +1144,8 @@ public extension MessageViewModel.TypingIndicatorInfo {
}() }()
let request: SQLRequest<MessageViewModel.TypingIndicatorInfo> = """ let request: SQLRequest<MessageViewModel.TypingIndicatorInfo> = """
SELECT SELECT
\(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), \(threadTypingIndicator[.rowId]),
\(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) \(threadTypingIndicator[.threadId])
FROM \(ThreadTypingIndicator.self) FROM \(ThreadTypingIndicator.self)
\(finalFilterSQL) \(finalFilterSQL)
""" """

View File

@ -45,8 +45,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
.replacingMentions(for: thread.id)) .replacingMentions(for: thread.id))
.defaulting(to: "APN_Message".localized()) .defaulting(to: "APN_Message".localized())
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] let userInfo: [String: Any] = [
userInfo[NotificationServiceExtension.threadIdKey] = thread.id NotificationServiceExtension.isFromRemoteKey: true,
NotificationServiceExtension.threadIdKey: thread.id,
NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue
]
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = userInfo notificationContent.userInfo = userInfo
@ -145,8 +148,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
// Only notify missed calls // Only notify missed calls
guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return } guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return }
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] let userInfo: [String: Any] = [
userInfo[NotificationServiceExtension.threadIdKey] = thread.id NotificationServiceExtension.isFromRemoteKey: true,
NotificationServiceExtension.threadIdKey: thread.id,
NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue
]
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = userInfo notificationContent.userInfo = userInfo
@ -206,8 +212,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
default: notificationBody = NotificationStrings.incomingMessageBody default: notificationBody = NotificationStrings.incomingMessageBody
} }
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] let userInfo: [String: Any] = [
userInfo[NotificationServiceExtension.threadIdKey] = thread.id NotificationServiceExtension.isFromRemoteKey: true,
NotificationServiceExtension.threadIdKey: thread.id,
NotificationServiceExtension.threadVariantRaw: thread.variant.rawValue
]
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = userInfo notificationContent.userInfo = userInfo

View File

@ -18,6 +18,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
public static let isFromRemoteKey = "remote" public static let isFromRemoteKey = "remote"
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId" public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter" public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
// MARK: Did receive a remote push notification request // MARK: Did receive a remote push notification request

View File

@ -36,10 +36,6 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
SetCurrentAppContext(appContext) 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("") Logger.info("")
_ = AppVersion.sharedInstance() _ = AppVersion.sharedInstance()
@ -66,6 +62,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
case .failure: SNLog("[SessionShareExtension] Failed to complete migrations") case .failure: SNLog("[SessionShareExtension] Failed to complete migrations")
case .success: case .success:
DispatchQueue.main.async { 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 // performUpdateCheck must be invoked after Environment has been initialized because
// upgrade process may depend on Environment. // upgrade process may depend on Environment.
self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync) self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync)

View File

@ -185,6 +185,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController( let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController(
threadId: strongSelf.viewModel.viewData[indexPath.row].threadId, threadId: strongSelf.viewModel.viewData[indexPath.row].threadId,
threadVariant: strongSelf.viewModel.viewData[indexPath.row].threadVariant,
attachments: attachments, attachments: attachments,
approvalDelegate: strongSelf approvalDelegate: strongSelf
) )
@ -197,6 +198,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
_ attachmentApproval: AttachmentApprovalViewController, _ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment], didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String, forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?, messageText: String?,
using dependencies: Dependencies = Dependencies() 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 ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
Storage.resumeDatabaseAccess() Storage.resumeDatabaseAccess()
dependencies.storage /// When we prepare the message we set the timestamp to be the `SnodeAPI.currentOffsetTimestampMs()`
.writePublisher { db -> MessageSender.PreparedSendData in /// but won't actually have a value because the share extension won't have talked to a service node yet which can cause
guard /// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate
let threadVariant: SessionThread.Variant = try SessionThread Just(())
.filter(id: threadId) .setFailureType(to: Error.self)
.select(.variant) .flatMap { _ in
.asRequest(of: SessionThread.Variant.self) // We may not have sufficient snodes, so rather than failing we try to load/fetch
.fetchOne(db) // them if needed
else { throw MessageSenderError.noThread } guard !SnodeAPI.hasCachedSnodesIncludingExpired() else {
return Just(())
// Create the interaction .setFailureType(to: Error.self)
let interaction: Interaction = try Interaction( .eraseToAnyPublisher()
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 return SnodeAPI.getSnodePool()
// one then add it now .map { _ in () }
if .eraseToAnyPublisher()
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
)
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .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.performUploadsIfNeeded(preparedSendData: $0, using: dependencies) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in

View File

@ -28,6 +28,7 @@ public class ThreadPickerViewModel {
.shareQuery(userPublicKey: userPublicKey) .shareQuery(userPublicKey: userPublicKey)
.fetchAll(db) .fetchAll(db)
} }
.map { threads -> [SessionThreadViewModel] in threads.filter { $0.canWrite } } // Exclude unwritable threads
.removeDuplicates() .removeDuplicates()
.handleEvents(didFail: { SNLog("[ThreadPickerViewModel] Observation failed with error: \($0)") }) .handleEvents(didFail: { SNLog("[ThreadPickerViewModel] Observation failed with error: \($0)") })

View File

@ -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 // 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 // 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 // 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") SNLog("[GetSnodePoolJob] Has valid cached pool, running async instead")
SnodeAPI SnodeAPI
.getSnodePool() .getSnodePool()

View File

@ -141,7 +141,7 @@ public final class SnodeAPI {
// MARK: - Public API // MARK: - Public API
public static func hasCachedSnodesInclusingExpired() -> Bool { public static func hasCachedSnodesIncludingExpired() -> Bool {
loadSnodePoolIfNeeded() loadSnodePoolIfNeeded()
return !hasInsufficientSnodes return !hasInsufficientSnodes
@ -1009,7 +1009,7 @@ public final class SnodeAPI {
// MARK: - Internal API // MARK: - Internal API
private static func getNetworkTime( public static func getNetworkTime(
from snode: Snode, from snode: Snode,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<UInt64, Error> { ) -> AnyPublisher<UInt64, Error> {
@ -1024,7 +1024,14 @@ public final class SnodeAPI {
using: dependencies using: dependencies
) )
.decoded(as: GetNetworkTimestampResponse.self, 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() .eraseToAnyPublisher()
} }

View File

@ -1351,18 +1351,24 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
// Fetch the inserted/updated rows // Fetch the inserted/updated rows
let additionalFilters: SQL = SQL(rowIds.contains(Column.rowID)) 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 do {
// preview) then trigger the update callback (if there were deletions) and stop here let updatedItems: [T] = try dataQuery(additionalFilters)
guard !updatedItems.isEmpty else { return hasOtherChanges } .fetchAll(db)
// Process the upserted data (assume at least one value changed) // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link
dataCache.mutate { $0 = $0.upserting(items: updatedItems) } // preview) then trigger the update callback (if there were deletions) and stop here
guard !updatedItems.isEmpty else { return hasOtherChanges }
return true
// 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) { public func clearCache(_ db: Database) {

View File

@ -3,22 +3,65 @@
import Foundation import Foundation
import GRDB import GRDB
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible { public struct TypedTableAlias<T: ColumnExpressible> {
public let alias: TableAlias = TableAlias(name: T.databaseTableName) public enum RowIdColumn {
case rowId
}
public init() {} internal let name: String
internal let tableName: String?
internal let alias: TableAlias
public var allColumns: SQLSelection { alias[AllColumns().sqlSelection] }
public var never: NeverJoiningTypedTableAlias<T> { NeverJoiningTypedTableAlias<T>(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<VM: ColumnExpressible>(_ viewModel: VM.Type, column: VM.Columns, tableName: String?) {
self.name = column.name
self.tableName = tableName
self.alias = TableAlias(name: name)
}
public init<VM: ColumnExpressible>(_ 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 { public subscript(_ column: T.Columns) -> SQLExpression {
return alias[column.name] return alias[column.name]
} }
/// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will public subscript(_ column: RowIdColumn) -> SQLSelection {
/// throw when trying to decode return alias[Column.rowID]
public func allColumns() -> SQLSelection {
return alias[AllColumns().sqlSelection]
} }
} }
// MARK: - NeverJoiningTypedTableAlias
public struct NeverJoiningTypedTableAlias<T: ColumnExpressible> {
internal let alias: TypedTableAlias<T>
}
// MARK: - Extensions
extension QueryInterfaceRequest { extension QueryInterfaceRequest {
public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> Self { public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> Self {
return aliased(typedAlias.alias) return aliased(typedAlias.alias)
@ -32,7 +75,5 @@ extension Association {
} }
extension TableAlias { extension TableAlias {
public func allColumns() -> SQLSelection { public var allColumns: SQLSelection { self[AllColumns().sqlSelection] }
return self[AllColumns().sqlSelection]
}
} }

View File

@ -0,0 +1,69 @@
// 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<T> = TypedTableAlias()
/// let request: SQLRequest<Player> = "SELECT * FROM \(player)"
@_disfavoredOverload
mutating func appendInterpolation<T>(_ typedTableAlias: TypedTableAlias<T>) {
let name: String = typedTableAlias.name
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<Player> = TypedTableAlias()
/// let testTeam: TypedTableAlias<Team> = TypedTableAlias(name: "testTeam")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(testTeam.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) 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<Player> = TypedTableAlias()
/// let teamInfo: TypedTableAlias<TeamInfo> = TypedTableAlias(name: "teamInfo")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) 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<Player> = TypedTableAlias()
/// let teamInfo: TypedTableAlias<TeamInfo> = TypedTableAlias(name: "teamInfo")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) {
appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false")
}
private func generateSelection<T: ColumnExpressible>(for type: T.Type) -> String where T.Columns: CaseIterable {
return T.Columns.allCases
.map { "NULL AS \($0.name)" }
.joined(separator: ", ")
}
private func generateSelection<T: ColumnExpressible>(for type: T.Type) -> String {
return "SELECT 1"
}
}

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension ScopeAdapter {
static func with<VM: ColumnExpressible>(
_ viewModel: VM.Type,
_ scopes: [VM.Columns: RowAdapter]
) -> ScopeAdapter {
return ScopeAdapter(scopes.reduce(into: [:]) { result, next in result[next.key.name] = next.value })
}
}

View File

@ -16,6 +16,7 @@ public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
_ attachmentApproval: AttachmentApprovalViewController, _ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment], didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String, forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?, messageText: String?,
using dependencies: Dependencies using dependencies: Dependencies
) )
@ -59,6 +60,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
private let mode: Mode private let mode: Mode
private let threadId: String private let threadId: String
private let threadVariant: SessionThread.Variant
private let isAddMoreVisible: Bool private let isAddMoreVisible: Bool
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
@ -128,11 +130,13 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
required public init( required public init(
mode: Mode, mode: Mode,
threadId: String, threadId: String,
threadVariant: SessionThread.Variant,
attachments: [SignalAttachment] attachments: [SignalAttachment]
) { ) {
assert(attachments.count > 0) assert(attachments.count > 0)
self.mode = mode self.mode = mode
self.threadId = threadId self.threadId = threadId
self.threadVariant = threadVariant
let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
self.isAddMoreVisible = (mode == .sharedNavigation) self.isAddMoreVisible = (mode == .sharedNavigation)
@ -162,10 +166,16 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
public class func wrappedInNavController( public class func wrappedInNavController(
threadId: String, threadId: String,
threadVariant: SessionThread.Variant,
attachments: [SignalAttachment], attachments: [SignalAttachment],
approvalDelegate: AttachmentApprovalViewControllerDelegate approvalDelegate: AttachmentApprovalViewControllerDelegate
) -> UINavigationController { ) -> UINavigationController {
let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments) let vc = AttachmentApprovalViewController(
mode: .modal,
threadId: threadId,
threadVariant: threadVariant,
attachments: attachments
)
vc.approvalDelegate = approvalDelegate vc.approvalDelegate = approvalDelegate
let navController = StyledNavigationController(rootViewController: vc) let navController = StyledNavigationController(rootViewController: vc)
@ -674,7 +684,14 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
attachmentTextToolbar.isUserInteractionEnabled = false attachmentTextToolbar.isUserInteractionEnabled = false
attachmentTextToolbar.isHidden = true 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) { func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) {