From 70ce967df6ce20566969a1e2c83f420405c51519 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Apr 2023 16:50:35 +1000 Subject: [PATCH 1/3] Made a few optimisations and fixes Made a couple of DB query optimisations for the Home and Conversation screens Removed some compiler-complex global generic functions Increased the timeout for file uploads Fixed a few import issues Fixed an issue preventing calls on the simulator from working (disable CallKit on the simulator) Fixed an issue where opening a conversation with a draft would result in a typing indicator notification being sent (if enabled) Fixed a truncation issue on the CallVC --- Session/Calls/CallVC.swift | 5 +- .../ConversationVC+Interaction.swift | 4 + Session/Conversations/ConversationVC.swift | 14 +- .../Settings/OWSMessageTimerView.m | 1 + Session/Utilities/IP2Country.swift | 2 +- .../Database/Models/Attachment.swift | 6 +- .../Database/Models/Interaction.swift | 18 +- .../File Server/FileServerAPI.swift | 25 ++- .../Jobs/Types/GarbageCollectionJob.swift | 2 +- .../Open Groups/OpenGroupAPI.swift | 4 +- .../Open Groups/OpenGroupManager.swift | 6 +- .../Sending & Receiving/MessageReceiver.swift | 14 +- .../Shared Models/MessageViewModel.swift | 110 ++++++----- .../SessionThreadViewModel.swift | 179 ++++++++++-------- .../Utilities/Preferences.swift | 7 + SessionSnodeKit/SnodeAPI.swift | 6 +- SessionUIKit/Utilities/UIView+Utilities.swift | 4 +- SessionUtilitiesKit/General/General.swift | 11 -- SessionUtilitiesKit/General/UIView+OWS.h | 1 - SessionUtilitiesKit/General/UIView+OWS.m | 1 + .../AttachmentTextToolbar.swift | 1 + 21 files changed, 228 insertions(+), 193 deletions(-) diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index f98901d45..44dab72c8 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -413,7 +413,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { if shouldRestartCamera { cameraManager.prepare() } - touch(call.videoCapturer) + _ = call.videoCapturer // Force the lazy var to instantiate titleLabel.text = self.call.contactName AppEnvironment.shared.callManager.startCall(call) { [weak self] error in DispatchQueue.main.async { @@ -468,7 +468,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate { view.addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.center(.vertical, in: minimizeButton) - titleLabel.center(.horizontal, in: view) + titleLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing) + titleLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing) // Response Panel view.addSubview(responsePanel) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index a266e5b83..b6b802eb9 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -661,6 +661,10 @@ extension ConversationVC: } func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + // Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to + // appear (as that is not expected/correct behaviour) + guard !viewIsAppearing else { return } + let newText: String = (inputTextView.text ?? "") if !newText.isEmpty { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index f4e2adf2a..746213a43 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -441,11 +441,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // Flag that the initial layout has been completed (the flag blocks and unblocks a number - // of different behaviours) - didFinishInitialLayout = true - viewIsAppearing = false - if delayFirstResponder || isShowingSearchUI { delayFirstResponder = false @@ -457,7 +452,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } } - recoverInputView() + recoverInputView { [weak self] in + // Flag that the initial layout has been completed (the flag blocks and unblocks a number + // of different behaviours) + self?.didFinishInitialLayout = true + self?.viewIsAppearing = false + } } override func viewWillDisappear(_ animated: Bool) { @@ -1261,7 +1261,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view) } - func recoverInputView() { + func recoverInputView(completion: (() -> ())? = nil) { // This is a workaround for an issue where the textview is not scrollable // after the app goes into background and goes back in foreground. DispatchQueue.main.async { diff --git a/Session/Conversations/Settings/OWSMessageTimerView.m b/Session/Conversations/Settings/OWSMessageTimerView.m index bfe57d7e3..ad2a924cf 100644 --- a/Session/Conversations/Settings/OWSMessageTimerView.m +++ b/Session/Conversations/Settings/OWSMessageTimerView.m @@ -6,6 +6,7 @@ #import "OWSMath.h" #import "UIView+OWS.h" #import +#import #import #import #import diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index a1f13ebab..ca24549b6 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -40,7 +40,7 @@ final class IP2Country { private func cacheCountry(for ip: String) -> String { if let result = countryNamesCache[ip] { return result } let ipAsInt = IPv4.toInt(ip) - guard let ipv4TableIndex = given(ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }), { $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted + guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex] guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" } let result = countryNamesTable["country_name"]![countryNamesTableIndex] diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index ab5a4024c..17cab5424 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -520,8 +520,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - /* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */ - (ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp]) + \(Interaction.linkPreviewFilterLiteral) ) ) @@ -566,8 +565,7 @@ extension Attachment { \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - /* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */ - (ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp]) + \(Interaction.linkPreviewFilterLiteral) ) ) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 0fd032333..a26b052ac 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -29,13 +29,13 @@ 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 func linkPreviewFilterLiteral( - timestampColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - ) -> SQL { + public static var linkPreviewFilterLiteral: SQL = { + let interaction: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() - - return "(ROUND((\(Interaction.self).\(timestampColumn) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])" - } + let halfResolution: Double = LinkPreview.timstampResolution + + return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) AND (\(linkPreview[.timestamp]) + \(halfResolution)))" + }() public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey) public typealias Columns = CodingKeys @@ -246,10 +246,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu public var linkPreview: QueryInterfaceRequest { /// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic + let halfResolution: Double = LinkPreview.timstampResolution let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000) return request(for: Interaction.linkPreview) - .filter(LinkPreview.Columns.timestamp == roundedTimestamp) + .filter( + (Interaction.Columns.timestampMs >= (LinkPreview.Columns.timestamp - halfResolution)) && + (Interaction.Columns.timestampMs <= (LinkPreview.Columns.timestamp + halfResolution)) + ) } public var recipientStates: QueryInterfaceRequest { diff --git a/SessionMessagingKit/File Server/FileServerAPI.swift b/SessionMessagingKit/File Server/FileServerAPI.swift index 5c0363133..964a09ffe 100644 --- a/SessionMessagingKit/File Server/FileServerAPI.swift +++ b/SessionMessagingKit/File Server/FileServerAPI.swift @@ -19,8 +19,9 @@ public final class FileServerAPI: NSObject { /// exactly will be fine but a single byte more will result in an error public static let maxFileSize = 10_000_000 - /// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files - public static let fileTimeout: TimeInterval = 30 + /// Standard timeout is 10 seconds which is a little too short for file upload/download with slightly larger files + public static let fileDownloadTimeout: TimeInterval = 30 + public static let fileUploadTimeout: TimeInterval = 60 // MARK: - File Storage @@ -36,7 +37,7 @@ public final class FileServerAPI: NSObject { body: Array(file) ) - return send(request, serverPublicKey: serverPublicKey) + return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileUploadTimeout) .decoded(as: FileUploadResponse.self, on: .global(qos: .userInitiated)) } @@ -47,7 +48,7 @@ public final class FileServerAPI: NSObject { endpoint: .fileIndividual(fileId: fileId) ) - return send(request, serverPublicKey: serverPublicKey) + return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileDownloadTimeout) } public static func getVersion(_ platform: String) -> Promise { @@ -59,14 +60,18 @@ public final class FileServerAPI: NSObject { ] ) - return send(request, serverPublicKey: serverPublicKey) + return send(request, serverPublicKey: serverPublicKey, timeout: HTTP.timeout) .decoded(as: VersionResponse.self, on: .global(qos: .userInitiated)) .map { response in response.version } } // MARK: - Convenience - private static func send(_ request: Request, serverPublicKey: String) -> Promise { + private static func send( + _ request: Request, + serverPublicKey: String, + timeout: TimeInterval + ) -> Promise { let urlRequest: URLRequest do { @@ -76,7 +81,13 @@ public final class FileServerAPI: NSObject { return Promise(error: error) } - return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout) + return OnionRequestAPI + .sendOnionRequest( + urlRequest, + to: request.server, + with: serverPublicKey, + timeout: timeout + ) .map2 { _, response in guard let response: Data = response else { throw HTTP.Error.parsingFailed } diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 512e61bfd..6c0adf6c0 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -141,7 +141,7 @@ public enum GarbageCollectionJob: JobExecutor { FROM \(LinkPreview.self) LEFT JOIN \(Interaction.self) ON ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND - \(Interaction.linkPreviewFilterLiteral()) + \(Interaction.linkPreviewFilterLiteral) ) WHERE \(interaction[.id]) IS NULL ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 9b4984627..f4af87fe4 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -871,7 +871,7 @@ public enum OpenGroupAPI { ], body: bytes ), - timeout: FileServerAPI.fileTimeout, + timeout: FileServerAPI.fileUploadTimeout, using: dependencies ) .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) @@ -891,7 +891,7 @@ public enum OpenGroupAPI { server: server, endpoint: .roomFileIndividual(roomToken, fileId) ), - timeout: FileServerAPI.fileTimeout, + timeout: FileServerAPI.fileDownloadTimeout, using: dependencies ) .map { responseInfo, maybeData in diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 928ffaaea..e2076f350 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -1083,7 +1083,11 @@ public final class OpenGroupManager: NSObject { } public static func parseOpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? { - guard let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }), let query = url.query else { return nil } + guard + let url = URL(string: string), + let host = (url.host ?? string.split(separator: "/").first.map({ String($0) })), + let query = url.query + else { return nil } // Inputs that should work: // https://sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // https://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 2f00d7d4d..20aa57cdc 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -327,10 +327,9 @@ public enum MessageReceiver { if let name = name, !name.isEmpty, name != profile.name { let shouldUpdate: Bool if isCurrentUser { - shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) { - sentTimestamp > $0.timeIntervalSince1970 - } - .defaulting(to: true) + shouldUpdate = UserDefaults.standard[.lastDisplayNameUpdate] + .map { sentTimestamp > $0.timeIntervalSince1970 } + .defaulting(to: true) } else { shouldUpdate = true @@ -354,10 +353,9 @@ public enum MessageReceiver { { let shouldUpdate: Bool if isCurrentUser { - shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) { - sentTimestamp > $0.timeIntervalSince1970 - } - .defaulting(to: true) + shouldUpdate = UserDefaults.standard[.lastProfilePictureUpdate] + .map { sentTimestamp > $0.timeIntervalSince1970 } + .defaulting(to: true) } else { shouldUpdate = true diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index e2a1917cc..f45b3a6a4 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -637,27 +637,32 @@ public extension MessageViewModel { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let groupMember: TypedTableAlias = TypedTableAlias() let recipientState: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() let quote: TypedTableAlias = TypedTableAlias() - let interactionAttachment: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() - let threadProfileTableLiteral: SQL = SQL(stringLiteral: "threadProfile") - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") - let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name) - let groupMemberModeratorTableLiteral: SQL = SQL(stringLiteral: "groupMemberModerator") - let groupMemberAdminTableLiteral: SQL = SQL(stringLiteral: "groupMemberAdmin") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) - let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) + 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 numColumnsBeforeLinkedRecords: Int = 20 let finalGroupSQL: SQL = (groupSQL ?? "") @@ -671,7 +676,7 @@ public extension MessageViewModel { IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), \(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), \(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), - IFNULL(\(threadProfileTableLiteral).\(profileNicknameColumnLiteral), \(threadProfileTableLiteral).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), + IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), \(interaction[.id]), @@ -685,20 +690,30 @@ public extension MessageViewModel { -- Default to 'sending' assuming non-processed interaction when null IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), - (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), + (\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), \(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), - ( - \(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL OR - \(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(interaction[.threadId]) AND + \(groupMember[.profileId]) = \(interaction[.authorId]) AND + \(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND + \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) + ) ) AS \(ViewModel.isSenderOpenGroupModeratorKey), \(ViewModel.profileKey).*, - \(ViewModel.quoteKey).*, + \(quote[.interactionId]), + \(quote[.authorId]), + \(quote[.timestampMs]), + \(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn), + \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), \(ViewModel.quoteAttachmentKey).*, \(ViewModel.linkPreviewKey).*, \(ViewModel.linkPreviewAttachmentKey).*, - + \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey), -- All of the below properties are set in post-query processing but to prevent the @@ -715,54 +730,35 @@ public extension MessageViewModel { 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 \(threadProfileTableLiteral) ON \(threadProfileTableLiteral).\(profileIdColumnLiteral) = \(interaction[.threadId]) + LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(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 ( - SELECT \(quote[.interactionId]), - \(quote[.authorId]), - \(quote[.timestampMs]), - \(interaction[.body]) AS \(Quote.Columns.body), - \(interactionAttachment[.attachmentId]) AS \(Quote.Columns.attachmentId) - FROM \(Quote.self) - LEFT JOIN \(Interaction.self) ON ( - ( - \(quote[.authorId]) = \(interaction[.authorId]) OR ( - \(quote[.authorId]) = \(blindedPublicKey ?? "") AND - \(userPublicKey) = \(interaction[.authorId]) - ) - ) AND - \(quote[.timestampMs]) = \(interaction[.timestampMs]) + 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 ( + \(quoteInteraction).\(authorIdColumn) = '' AND + \(quoteInteraction).\(authorIdColumn) = \(userPublicKey) + ) ) - LEFT JOIN \(InteractionAttachment.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId]) - ) AS \(ViewModel.quoteKey) ON \(quote[.interactionId]) = \(interaction[.id]) - LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId]) + ) + LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) + 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).\(attachmentIdColumnLiteral) = \(linkPreview[.attachmentId]) + LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(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 \(readReceiptTableLiteral) ON ( - \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND - \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) - ) - LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON ( - \(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND - \(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND - \(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND - \(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)")) - ) - LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON ( - \(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND - \(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND - \(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND - \(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) + LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( + \(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND + \(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) ) WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) \(finalGroupSQL) diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 6b20a0fe0..01c249d6f 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -448,7 +448,8 @@ public extension SessionThreadViewModel { let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) + let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") + let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) @@ -459,9 +460,7 @@ public extension SessionThreadViewModel { let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) - let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) - let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) + /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -470,124 +469,136 @@ public extension SessionThreadViewModel { /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 12 let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined - let request: SQLRequest = """ SELECT \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), \(thread[.id]) AS \(ViewModel.threadIdKey), \(thread[.variant]) AS \(ViewModel.threadVariantKey), \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), \(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey), \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), - + (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), - \(Interaction.self).\(ViewModel.threadUnreadCountKey), - \(Interaction.self).\(ViewModel.threadUnreadMentionCountKey), - + \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), + \(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey), + \(ViewModel.contactProfileKey).*, \(ViewModel.closedGroupProfileFrontKey).*, \(ViewModel.closedGroupProfileBackKey).*, \(ViewModel.closedGroupProfileBackFallbackKey).*, \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - (\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey), - (\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND + \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + ) + ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND + \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + ) + ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), - - \(Interaction.self).\(ViewModel.interactionIdKey), - \(Interaction.self).\(ViewModel.interactionVariantKey), - \(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey), - \(Interaction.self).\(ViewModel.interactionBodyKey), - + + \(interaction[.id]) AS \(ViewModel.interactionIdKey), + \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), + \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), + \(interaction[.body]) AS \(ViewModel.interactionBodyKey), + -- Default to 'sending' assuming non-processed interaction when null - IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey), + IFNULL(( + SELECT \(recipientState[.state]) + FROM \(RecipientState.self) + WHERE ( + \(recipientState[.interactionId]) = \(interaction[.id]) AND + -- Ignore 'skipped' states + \(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)")) + ) + LIMIT 1 + ), 0) AS \(ViewModel.interactionStateKey), + (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey), (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), - + -- These 4 properties will be combined into 'Attachment.DescriptionInfo' \(attachment[.id]), \(attachment[.variant]), \(attachment[.contentType]), \(attachment[.sourceFilename]), COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), - + \(interaction[.authorId]), IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) - + FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id]) + LEFT JOIN ( - -- Fetch all interaction-specific data in a subquery to be more efficient SELECT \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral), - \(interaction[.body]) AS \(ViewModel.interactionBodyKey), - \(interaction[.authorId]), - \(interaction[.linkPreviewUrl]), - + \(interaction[.threadId]) AS \(ViewModel.threadIdKey), + MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral), SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey), SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) - FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) + ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) - LEFT JOIN \(RecipientState.self) ON ( - -- Ignore 'skipped' states - \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND - \(recipientState[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey) + LEFT JOIN \(Interaction.self) ON ( + \(interaction[.threadId]) = \(thread[.id]) AND + \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) ) + LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( - \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND - \(Interaction.self).\(ViewModel.interactionIdKey) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) + \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND + \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND - \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND - \(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral)) + \(Interaction.linkPreviewFilterLiteral) AND + \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) ) LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( - \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND - \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(Interaction.self).\(ViewModel.interactionIdKey) + \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND + \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 ) LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) - LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey) + LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) - + -- Thread naming & avatar content - + LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON ( - \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND - \(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND - \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) - ) - LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON ( - \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND - \(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND - \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) - ) - + LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( - \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(SQL("\(groupMember[.profileId]) != \(userPublicKey)")) ) ) @@ -599,8 +610,8 @@ public extension SessionThreadViewModel { FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( - \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(SQL("\(groupMember[.profileId]) != \(userPublicKey)")) ) ) @@ -610,7 +621,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) ) - + WHERE \(thread.alias[Column.rowID]) IN \(rowIds) \(groupSQL) ORDER BY \(orderSQL) @@ -643,14 +654,14 @@ public extension SessionThreadViewModel { let contact: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() - let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) + let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) return """ LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT \(interaction[.threadId]), - MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral) + MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) @@ -701,7 +712,10 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() - return SQL("\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC") + return SQL(""" + \(thread[.isPinned]) DESC, + CASE WHEN \(interaction[.timestampMs]) IS NOT NULL THEN \(interaction[.timestampMs]) ELSE (\(thread[.creationDateTimestamp]) * 1000) END DESC + """) }() static let messageRequetsOrderSQL: SQL = { @@ -725,6 +739,8 @@ public extension SessionThreadViewModel { let openGroup: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() + let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") + let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) @@ -760,12 +776,22 @@ public extension SessionThreadViewModel { \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), - \(Interaction.self).\(ViewModel.threadUnreadCountKey), + \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), \(ViewModel.contactProfileKey).*, \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), \(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey), - (\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + + EXISTS ( + SELECT 1 + FROM \(GroupMember.self) + WHERE ( + \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND + \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND + \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + ) + ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), @@ -773,33 +799,28 @@ public extension SessionThreadViewModel { \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), - \(Interaction.self).\(ViewModel.interactionIdKey), + \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey), \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( - -- Fetch all interaction-specific data in a subquery to be more efficient SELECT \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]), - MAX(\(interaction[.timestampMs])), - + \(interaction[.threadId]) AS \(ViewModel.threadIdKey), + MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral), SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) - FROM \(Interaction.self) - WHERE \(SQL("\(interaction[.threadId]) = \(threadId)")) - ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) - + WHERE ( + \(SQL("\(interaction[.threadId]) = \(threadId)")) AND + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) + ) + ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(GroupMember.self) ON ( - \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND - \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND - \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) - ) LEFT JOIN ( SELECT \(groupMember[.groupId]), @@ -1583,7 +1604,7 @@ public extension SessionThreadViewModel { FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( - SELECT *, MAX(\(interaction[.timestampMs])) + SELECT \(interaction[.threadId]), MAX(\(interaction[.timestampMs])) FROM \(Interaction.self) GROUP BY \(interaction[.threadId]) ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 293c529c5..0b3c91209 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -308,9 +308,16 @@ public enum Preferences { } public static var isCallKitSupported: Bool { +#if targetEnvironment(simulator) + /// The iOS simulator doesn't support CallKit, when receiving a call on the simulator and routing it via CallKit it + /// will immediately trigger a hangup making it difficult to test - instead we just should just avoid using CallKit + /// entirely on the simulator + return false +#else guard let regionCode: String = NSLocale.current.regionCode else { return false } guard !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false } return true +#endif } } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 1fb102ea2..f4ce3ab39 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -312,9 +312,9 @@ public final class SnodeAPI { public static func getSnodePool() -> Promise> { loadSnodePoolIfNeeded() let now = Date() - let hasSnodePoolExpired = given(Storage.shared[.lastSnodePoolRefreshDate]) { - now.timeIntervalSince($0) > 2 * 60 * 60 - }.defaulting(to: true) + let hasSnodePoolExpired: Bool = Storage.shared[.lastSnodePoolRefreshDate] + .map { now.timeIntervalSince($0) > 2 * 60 * 60 } + .defaulting(to: true) let snodePool: Set = SnodeAPI.snodePool.wrappedValue guard hasInsufficientSnodes || hasSnodePoolExpired else { diff --git a/SessionUIKit/Utilities/UIView+Utilities.swift b/SessionUIKit/Utilities/UIView+Utilities.swift index 7d37a2185..5e3e5af30 100644 --- a/SessionUIKit/Utilities/UIView+Utilities.swift +++ b/SessionUIKit/Utilities/UIView+Utilities.swift @@ -17,13 +17,13 @@ public extension UIView { class func spacer(withWidth width: CGFloat) -> UIView { let view = UIView() - view.autoSetDimension(.width, toSize: width) + view.set(.width, to: width) return view } class func spacer(withHeight height: CGFloat) -> UIView { let view = UIView() - view.autoSetDimension(.height, toSize: height) + view.set(.height, to: height) return view } diff --git a/SessionUtilitiesKit/General/General.swift b/SessionUtilitiesKit/General/General.swift index 72576fb56..b901af73a 100644 --- a/SessionUtilitiesKit/General/General.swift +++ b/SessionUtilitiesKit/General/General.swift @@ -35,14 +35,3 @@ public func getUserHexEncodedPublicKey(_ db: Database? = nil, dependencies: Depe return "" } - -/// Does nothing, but is never inlined and thus evaluating its argument will never be optimized away. -/// -/// Useful for forcing the instantiation of lazy properties like globals. -@inline(never) -public func touch(_ value: Value) { /* Do nothing */ } - -/// Returns `f(x!)` if `x != nil`, or `nil` otherwise. -public func given(_ x: T?, _ f: (T) throws -> U) rethrows -> U? { return try x.map(f) } - -public func with(_ x: T, _ f: (T) throws -> U) rethrows -> U { return try f(x) } diff --git a/SessionUtilitiesKit/General/UIView+OWS.h b/SessionUtilitiesKit/General/UIView+OWS.h index 70bb155b2..77d68311e 100644 --- a/SessionUtilitiesKit/General/UIView+OWS.h +++ b/SessionUtilitiesKit/General/UIView+OWS.h @@ -2,7 +2,6 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -#import #import NS_ASSUME_NONNULL_BEGIN diff --git a/SessionUtilitiesKit/General/UIView+OWS.m b/SessionUtilitiesKit/General/UIView+OWS.m index 490ab1efd..7c831764a 100644 --- a/SessionUtilitiesKit/General/UIView+OWS.m +++ b/SessionUtilitiesKit/General/UIView+OWS.m @@ -5,6 +5,7 @@ #import "UIView+OWS.h" #import "OWSMath.h" +#import #import NS_ASSUME_NONNULL_BEGIN diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index f290c43f5..842fb68c6 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -5,6 +5,7 @@ import Foundation import UIKit import SessionUIKit +import PureLayout // Coincides with Android's max text message length let kMaxMessageBodyCharacterCount = 2000 From cac2831208ea1acbfc02b29f8cee725996576047 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Apr 2023 09:26:34 +1000 Subject: [PATCH 2/3] Fixed a few issues with albums and attachments Fixed an issue where the JobRunner could schedule dependent jobs which were already running (eg. uploading attachments multiple times) Fixed an issue where the image used for quotes might not be the first in an album message Fixed an issue where sending an album message wouldn't send attachments in the correct order Fixed an issue where album attachments wouldn't be downloaded in the correct order --- .../Database/Models/Attachment.swift | 7 ++-- .../Jobs/Types/MessageSendJob.swift | 1 + .../Visible Messages/VisibleMessage.swift | 13 +++++-- .../Shared Models/MessageViewModel.swift | 6 +++- SessionUtilitiesKit/JobRunner/JobRunner.swift | 34 ++++++++++++++----- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 17cab5424..3f661fc20 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -493,6 +493,7 @@ extension Attachment { public let interactionId: Int64 public let state: Attachment.State public let downloadUrl: String? + public let albumIndex: Int } public static func stateInfo(authorId: String, state: State? = nil) -> SQLRequest { @@ -510,7 +511,8 @@ extension Attachment { \(attachment[.id]) AS attachmentId, \(interaction[.id]) AS interactionId, \(attachment[.state]) AS state, - \(attachment[.downloadUrl]) AS downloadUrl + \(attachment[.downloadUrl]) AS downloadUrl, + IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex FROM \(Attachment.self) @@ -555,7 +557,8 @@ extension Attachment { \(attachment[.id]) AS attachmentId, \(interaction[.id]) AS interactionId, \(attachment[.state]) AS state, - \(attachment[.downloadUrl]) AS downloadUrl + \(attachment[.downloadUrl]) AS downloadUrl, + IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex FROM \(Attachment.self) diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index b835cbca5..eac85ddfb 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -57,6 +57,7 @@ public enum MessageSendJob: JobExecutor { .stateInfo(interactionId: interactionId) .fetchAll(db) let maybeFileIds: [String?] = allAttachmentStateInfo + .sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex } .map { Attachment.fileId(for: $0.downloadUrl) } let fileIds: [String] = maybeFileIds.compactMap { $0 } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 2780ae0eb..7132723b9 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -160,14 +160,21 @@ public final class VisibleMessage: Message { // Attachments - let attachments: [Attachment]? = try? Attachment.fetchAll(db, ids: self.attachmentIds) + let attachmentIdIndexes: [String: Int] = (try? InteractionAttachment + .filter(self.attachmentIds.contains(InteractionAttachment.Columns.attachmentId)) + .fetchAll(db)) + .defaulting(to: []) + .reduce(into: [:]) { result, next in result[next.attachmentId] = next.albumIndex } + let attachments: [Attachment] = (try? Attachment.fetchAll(db, ids: self.attachmentIds)) + .defaulting(to: []) + .sorted { lhs, rhs in (attachmentIdIndexes[lhs.id] ?? 0) < (attachmentIdIndexes[rhs.id] ?? 0) } - if !(attachments ?? []).allSatisfy({ $0.state == .uploaded }) { + if !attachments.allSatisfy({ $0.state == .uploaded }) { #if DEBUG preconditionFailure("Sending a message before all associated attachments have been uploaded.") #endif } - let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() } + let attachmentProtos = attachments.compactMap { $0.buildProto() } dataMessage.setAttachments(attachmentProtos) // Open group invitation diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index f45b3a6a4..0523224ed 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -663,6 +663,7 @@ public extension MessageViewModel { let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name) let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) + let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) let numColumnsBeforeLinkedRecords: Int = 20 let finalGroupSQL: SQL = (groupSQL ?? "") @@ -743,7 +744,10 @@ public extension MessageViewModel { ) ) ) - LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) + LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( + \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND + \(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 + ) LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) LEFT JOIN \(LinkPreview.self) ON ( diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index e1f1408a8..25d352a1d 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -591,12 +591,17 @@ private final class JobQueue { } fileprivate func appDidBecomeActive(with jobs: [Job], canStart: Bool) { + let currentlyRunningJobIds: Set = jobsCurrentlyRunning.wrappedValue + queue.mutate { queue in // Avoid re-adding jobs to the queue that are already in it (this can // happen if the user sends the app to the background before the 'onActive' // jobs and then brings it back to the foreground) let jobsNotAlreadyInQueue: [Job] = jobs - .filter { job in !queue.contains(where: { $0.id == job.id }) } + .filter { job in + !currentlyRunningJobIds.contains(job.id ?? -1) && + !queue.contains(where: { $0.id == job.id }) + } queue.append(contentsOf: jobsNotAlreadyInQueue) } @@ -784,14 +789,20 @@ private final class JobQueue { guard dependencyInfo.jobs.isEmpty else { SNLog("[JobRunner] \(queueContext) found job with \(dependencyInfo.jobs.count) dependencies, running those first") - /// Remove all jobs this one is dependant on from the queue and re-insert them at the start of the queue + /// Remove all jobs this one is dependant on that aren't currently running from the queue and re-insert them at the start + /// of the queue /// /// **Note:** We don't add the current job back the the queue because it should only be re-added if it's dependencies /// are successfully completed + let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys) + let dependencyJobsNotCurrentlyRunning: [Job] = dependencyInfo.jobs + .filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) } + .sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) } + queue.mutate { queue in queue = queue - .filter { !dependencyInfo.jobs.contains($0) } - .inserting(contentsOf: Array(dependencyInfo.jobs), at: 0) + .filter { !dependencyJobsNotCurrentlyRunning.contains($0) } + .inserting(contentsOf: dependencyJobsNotCurrentlyRunning, at: 0) } handleJobDeferred(nextJob) return @@ -960,17 +971,22 @@ private final class JobQueue { default: break } - /// Now that the job has been completed we want to insert any jobs that were dependant on it to the start of the queue (the - /// most likely case is that we want an entire job chain to be completed at the same time rather than being blocked by other - /// unrelated jobs) + /// Now that the job has been completed we want to insert any jobs that were dependant on it, that aren't already running + /// to the start of the queue (the most likely case is that we want an entire job chain to be completed at the same time rather + /// than being blocked by other unrelated jobs) /// /// **Note:** If any of these `dependantJobs` have other dependencies then when they attempt to start they will be /// removed from the queue, replaced by their dependencies if !dependantJobs.isEmpty { + let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys) + let dependantJobsNotCurrentlyRunning: [Job] = dependantJobs + .filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) } + .sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) } + queue.mutate { queue in queue = queue - .filter { !dependantJobs.contains($0) } - .inserting(contentsOf: dependantJobs, at: 0) + .filter { !dependantJobsNotCurrentlyRunning.contains($0) } + .inserting(contentsOf: dependantJobsNotCurrentlyRunning, at: 0) } } From 5cbb4f632d5e3757ad87ec985d53704961b301c7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Apr 2023 11:03:47 +1000 Subject: [PATCH 3/3] Fix for PR comment --- SessionMessagingKit/Shared Models/MessageViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 0523224ed..c7261aafa 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -739,7 +739,9 @@ public extension MessageViewModel { LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON ( \(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND ( \(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR ( - \(quoteInteraction).\(authorIdColumn) = '' AND + -- 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 + \(quote[.authorId]) = \(blindedPublicKey ?? "''") AND \(quoteInteraction).\(authorIdColumn) = \(userPublicKey) ) )