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

View File

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

View File

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

View File

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

View File

@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
])
}
catch {
// Don't log the 'interrupt' error as that's just the user typing too fast
if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT {
SNLog("[GlobalSearch] Failed to find results due to error: \(error)")
}
return .failure(error)
}
}

View File

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

View File

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

View File

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

View File

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

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

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
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
public extension Profile {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,10 +36,6 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
SetCurrentAppContext(appContext)
}
// Need to manually trigger these since we don't have a "mainWindow" here and the current theme
// might have been changed since the share extension was last opened
ThemeManager.applySavedTheme()
Logger.info("")
_ = AppVersion.sharedInstance()
@ -66,6 +62,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
case .failure: SNLog("[SessionShareExtension] Failed to complete migrations")
case .success:
DispatchQueue.main.async {
// Need to manually trigger these since we don't have a "mainWindow" here
// and the current theme might have been changed since the share extension
// was last opened
ThemeManager.applySavedTheme()
// performUpdateCheck must be invoked after Environment has been initialized because
// upgrade process may depend on Environment.
self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync)

View File

@ -185,6 +185,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController(
threadId: strongSelf.viewModel.viewData[indexPath.row].threadId,
threadVariant: strongSelf.viewModel.viewData[indexPath.row].threadVariant,
attachments: attachments,
approvalDelegate: strongSelf
)
@ -197,6 +198,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
_ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies = Dependencies()
) {
@ -221,78 +223,111 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
Storage.resumeDatabaseAccess()
dependencies.storage
.writePublisher { db -> MessageSender.PreparedSendData in
guard
let threadVariant: SessionThread.Variant = try SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
else { throw MessageSenderError.noThread }
// Create the interaction
let interaction: Interaction = try Interaction(
threadId: threadId,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: body,
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds)
.filter(id: threadId)
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
.asRequest(of: TimeInterval.self)
.fetchOne(db),
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
throw StorageError.failedToSave
/// When we prepare the message we set the timestamp to be the `SnodeAPI.currentOffsetTimestampMs()`
/// but won't actually have a value because the share extension won't have talked to a service node yet which can cause
/// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate
Just(())
.setFailureType(to: Error.self)
.flatMap { _ in
// We may not have sufficient snodes, so rather than failing we try to load/fetch
// them if needed
guard !SnodeAPI.hasCachedSnodesIncludingExpired() else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing
// one then add it now
if
isSharingUrl,
let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft,
(try? interaction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: LinkPreview
.generateAttachmentIfPossible(
imageData: linkPreviewDraft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg
)?
.inserted(db)
.id
).insert(db)
}
// Prepare any attachments
try Attachment.process(
db,
data: Attachment.prepare(attachments: finalAttachments),
for: interactionId
)
// Prepare the message send data
return try MessageSender
.preparedSendData(
db,
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant,
using: dependencies
)
return SnodeAPI.getSnodePool()
.map { _ in () }
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { _ in
SnodeAPI
.getSwarm(
for: {
switch threadVariant {
case .contact, .legacyGroup, .group: return threadId
case .community: return getUserHexEncodedPublicKey(using: dependencies)
}
}(),
using: dependencies
)
.tryFlatMapWithRandomSnode { SnodeAPI.getNetworkTime(from: $0, using: dependencies) }
.map { _ in () }
.eraseToAnyPublisher()
}
.flatMap { _ in
dependencies.storage.writePublisher { db -> MessageSender.PreparedSendData in
guard
let threadVariant: SessionThread.Variant = try SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
else { throw MessageSenderError.noThread }
// Create the interaction
let interaction: Interaction = try Interaction(
threadId: threadId,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: body,
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds)
.filter(id: threadId)
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
.asRequest(of: TimeInterval.self)
.fetchOne(db),
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
throw StorageError.failedToSave
}
// If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing
// one then add it now
if
isSharingUrl,
let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft,
(try? interaction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: LinkPreview
.generateAttachmentIfPossible(
imageData: linkPreviewDraft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg
)?
.inserted(db)
.id
).insert(db)
}
// Prepare any attachments
try Attachment.process(
db,
data: Attachment.prepare(attachments: finalAttachments),
for: interactionId
)
// Prepare the message send data
return try MessageSender
.preparedSendData(
db,
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant,
using: dependencies
)
}
}
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0, using: dependencies) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { [weak self] result in

View File

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

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
// to block if we have no Snode pool and prevent other jobs from failing but avoids having to
// wait if we already have a potentially valid snode pool
guard !SnodeAPI.hasCachedSnodesInclusingExpired() else {
guard !SnodeAPI.hasCachedSnodesIncludingExpired() else {
SNLog("[GetSnodePoolJob] Has valid cached pool, running async instead")
SnodeAPI
.getSnodePool()

View File

@ -141,7 +141,7 @@ public final class SnodeAPI {
// MARK: - Public API
public static func hasCachedSnodesInclusingExpired() -> Bool {
public static func hasCachedSnodesIncludingExpired() -> Bool {
loadSnodePoolIfNeeded()
return !hasInsufficientSnodes
@ -1009,7 +1009,7 @@ public final class SnodeAPI {
// MARK: - Internal API
private static func getNetworkTime(
public static func getNetworkTime(
from snode: Snode,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<UInt64, Error> {
@ -1024,7 +1024,14 @@ public final class SnodeAPI {
using: dependencies
)
.decoded(as: GetNetworkTimestampResponse.self, using: dependencies)
.map { _, response in response.timestamp }
.map { _, response in
// Assume we've fetched the networkTime in order to send a message to the specified snode, in
// which case we want to update the 'clockOffsetMs' value for subsequent requests
let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000)))
SnodeAPI.clockOffsetMs.mutate { $0 = offset }
return response.timestamp
}
.eraseToAnyPublisher()
}

View File

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

View File

@ -3,22 +3,65 @@
import Foundation
import GRDB
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible {
public let alias: TableAlias = TableAlias(name: T.databaseTableName)
public struct TypedTableAlias<T: ColumnExpressible> {
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 {
return alias[column.name]
}
/// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will
/// throw when trying to decode
public func allColumns() -> SQLSelection {
return alias[AllColumns().sqlSelection]
public subscript(_ column: RowIdColumn) -> SQLSelection {
return alias[Column.rowID]
}
}
// MARK: - NeverJoiningTypedTableAlias
public struct NeverJoiningTypedTableAlias<T: ColumnExpressible> {
internal let alias: TypedTableAlias<T>
}
// MARK: - Extensions
extension QueryInterfaceRequest {
public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> Self {
return aliased(typedAlias.alias)
@ -32,7 +75,5 @@ extension Association {
}
extension TableAlias {
public func allColumns() -> SQLSelection {
return self[AllColumns().sqlSelection]
}
public var allColumns: SQLSelection { 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,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies
)
@ -59,6 +60,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
private let mode: Mode
private let threadId: String
private let threadVariant: SessionThread.Variant
private let isAddMoreVisible: Bool
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
@ -128,11 +130,13 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
required public init(
mode: Mode,
threadId: String,
threadVariant: SessionThread.Variant,
attachments: [SignalAttachment]
) {
assert(attachments.count > 0)
self.mode = mode
self.threadId = threadId
self.threadVariant = threadVariant
let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
self.isAddMoreVisible = (mode == .sharedNavigation)
@ -162,10 +166,16 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
public class func wrappedInNavController(
threadId: String,
threadVariant: SessionThread.Variant,
attachments: [SignalAttachment],
approvalDelegate: AttachmentApprovalViewControllerDelegate
) -> UINavigationController {
let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments)
let vc = AttachmentApprovalViewController(
mode: .modal,
threadId: threadId,
threadVariant: threadVariant,
attachments: attachments
)
vc.approvalDelegate = approvalDelegate
let navController = StyledNavigationController(rootViewController: vc)
@ -674,7 +684,14 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
attachmentTextToolbar.isUserInteractionEnabled = false
attachmentTextToolbar.isHidden = true
approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: attachmentTextToolbar.messageText, using: dependencies)
approvalDelegate?.attachmentApproval(
self,
didApproveAttachments: attachments,
forThreadId: threadId,
threadVariant: threadVariant,
messageText: attachmentTextToolbar.messageText,
using: dependencies
)
}
func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) {