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:
commit
6d990559b7
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
profilePictureView,
|
||||
replyButton,
|
||||
timerView,
|
||||
messageStatusImageView,
|
||||
messageStatusContainerView,
|
||||
reactionContainerView
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
|
||||
fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue
|
||||
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]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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
|
||||
|
||||
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
|
||||
// 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)
|
||||
"""
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,8 +223,42 @@ 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
|
||||
/// 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()
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -289,10 +325,9 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
using: dependencies
|
||||
)
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
}
|
||||
.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
|
||||
|
|
|
@ -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)") })
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -1351,9 +1351,10 @@ 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: [])
|
||||
|
||||
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
|
||||
|
@ -1364,6 +1365,11 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
|
|||
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
SNLog("[PagedDatabaseObserver] Error loading associated data: \(error)")
|
||||
return hasOtherChanges
|
||||
}
|
||||
}
|
||||
|
||||
public func clearCache(_ db: Database) {
|
||||
dataCache.mutate { $0 = DataCache() }
|
||||
|
|
|
@ -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] }
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue