From 70ce967df6ce20566969a1e2c83f420405c51519 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Apr 2023 16:50:35 +1000 Subject: [PATCH] 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