From ff08579088a8dc53cffd8e159bf3e63f4b4184e0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 16 Jun 2022 13:14:56 +1000 Subject: [PATCH] Added logic to for unblinding current conversation & bug fixes Added logic to handle unblinding the conversation the user currently has open Fixed a bug where the nav bar wouldn't appear when creating a new account Fixed a bug where messages send to an open group inbox weren't getting their open group server id set (causing duplicates) Fixed a bug where the interaction/gallery data might not get updated in certain cases Fixed an issue where visible messages which were getting sent over 24 hours than when they were originally meant to be sent would fail due to clock offset issues --- .../ConversationVC+Interaction.swift | 6 +- Session/Conversations/ConversationVC.swift | 118 ++++---------- .../Conversations/ConversationViewModel.swift | 148 ++++++++++++------ .../MediaGalleryViewModel.swift | 23 ++- Session/Meta/AppDelegate.swift | 4 +- Session/Notifications/AppNotifications.swift | 7 +- .../Migrations/_003_YDBToGRDBMigration.swift | 8 +- .../Database/Models/BlindedIdLookup.swift | 4 +- .../Database/Models/Interaction.swift | 40 ++++- .../Open Groups/OpenGroupManager.swift | 15 +- .../MessageReceiver+MessageRequests.swift | 11 +- .../MessageReceiver+VisibleMessages.swift | 51 +++++- .../Sending & Receiving/MessageSender.swift | 8 +- .../NSENotificationPresenter.swift | 2 +- SessionShareExtension/ThreadPickerVC.swift | 4 +- .../Database/GRDBStorage.swift | 7 + .../Types/PagedDatabaseObserver.swift | 50 +++--- SessionUtilitiesKit/Media/Updatable.swift | 8 + 18 files changed, 310 insertions(+), 204 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e328ead7c..10b4cf8d5 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -366,14 +366,13 @@ extension ConversationVC: .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) // Create the interaction - let userPublicKey: String = getUserHexEncodedPublicKey(db) let interaction: Interaction = try Interaction( threadId: threadId, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: text, timestampMs: sentTimestampMs, - hasMention: text.contains("@\(userPublicKey)"), + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text), linkPreviewUrl: linkPreviewDraft?.urlString ).inserted(db) @@ -464,14 +463,13 @@ extension ConversationVC: .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) // Create the interaction - let userPublicKey: String = getUserHexEncodedPublicKey(db) let interaction: Interaction = try Interaction( threadId: threadId, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: text, timestampMs: sentTimestampMs, - hasMention: text.contains("@\(userPublicKey)") + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text) ).inserted(db) try MessageSender.send( diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 1904c914f..cac1a3ccc 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -411,8 +411,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers name: UIResponder.keyboardWillHideNotification, object: nil ) - -// notificationCenter.addObserver(self, selector: #selector(handleContactThreadReplaced(_:)), name: .contactThreadReplaced, object: nil) } override func viewWillAppear(_ animated: Bool) { @@ -487,7 +485,36 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers viewModel.observableThreadData, onError: { _ in }, onChange: { [weak self] maybeThreadData in - guard let threadData: SessionThreadViewModel = maybeThreadData else { return } + guard let threadData: SessionThreadViewModel = maybeThreadData else { + // If the thread data is null and the id was blinded then we just unblinded the thread + // and need to swap over to the new one + guard + let sessionId: String = self?.viewModel.threadData.threadId, + SessionId.Prefix(from: sessionId) == .blinded, + let blindedLookup: BlindedIdLookup = GRDBStorage.shared.read({ db in + try BlindedIdLookup + .filter(id: sessionId) + .fetchOne(db) + }), + let unblindedId: String = blindedLookup.sessionId + else { + // If we don't have an unblinded id then something has gone very wrong so pop to the HomeVC + self?.navigationController?.popToRootViewController(animated: true) + return + } + + // Stop observing changes + self?.stopObservingChanges() + GRDBStorage.shared.removeObserver(self?.viewModel.pagedDataObserver) + + // Swap the observing to the updated thread + self?.viewModel.swapToThread(updatedThreadId: unblindedId) + + // Start observing changes again + GRDBStorage.shared.addObserver(self?.viewModel.pagedDataObserver) + self?.startObservingChanges() + return + } // The default scheduler emits changes on the main thread self?.handleThreadUpdates(threadData) @@ -1094,91 +1121,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers self.snInputView.text = self.snInputView.text } } - - @objc private func handleContactThreadReplaced(_ notification: Notification) { - print("ASDASDASD") -// // Ensure the current thread is one of the removed ones -// guard let newThreadId: String = notification.userInfo?[NotificationUserInfoKey.threadId] as? String else { return } -// guard let removedThreadIds: [String] = notification.userInfo?[NotificationUserInfoKey.removedThreadIds] as? [String] else { -// return -// } -// guard let threadId: String = thread.uniqueId, removedThreadIds.contains(threadId) else { return } -// -// // Then look to swap the current ConversationVC with a replacement one with the new thread -// DispatchQueue.main.async { -// guard let navController: UINavigationController = self.navigationController else { return } -// guard let viewControllerIndex: Int = navController.viewControllers.firstIndex(of: self) else { return } -// guard let newThread: TSContactThread = TSContactThread.fetch(uniqueId: newThreadId) else { return } -// -// // Let the view controller know we are replacing the thread -// self.isReplacingThread = true -// -// // Create the new ConversationVC and swap the old one out for it -// let conversationVC: ConversationVC = ConversationVC(thread: newThread) -// let currentlyOnThisScreen: Bool = (navController.topViewController == self) -// -// navController.viewControllers = [ -// (viewControllerIndex == 0 ? -// [] : -// navController.viewControllers[0..>> = setupObservableThreadData(for: self.threadId) + + private func setupObservableThreadData(for threadId: String) -> ValueObservation>> { + return ValueObservation + .trackingConstantRegion { db -> SessionThreadViewModel? in + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + return try SessionThreadViewModel + .conversationQuery(threadId: threadId, userPublicKey: userPublicKey) + .fetchOne(db) + } + .removeDuplicates() + } + + public func updateThreadData(_ updatedData: SessionThreadViewModel) { + self.threadData = updatedData + } + + // MARK: - Interaction Data + + public private(set) var unobservedInteractionDataChanges: [SectionModel]? + public private(set) var interactionData: [SectionModel] = [] + public private(set) var pagedDataObserver: PagedDatabaseObserver? + + public var onInteractionChange: (([SectionModel]) -> ())? { + didSet { + // When starting to observe interaction changes we want to trigger a UI update just in case the + // data was changed while we weren't observing + if let unobservedInteractionDataChanges: [SectionModel] = self.unobservedInteractionDataChanges { + onInteractionChange?(unobservedInteractionDataChanges) + self.unobservedInteractionDataChanges = nil + } + } + } + + private func setupPagedObserver(for threadId: String) -> PagedDatabaseObserver { + return PagedDatabaseObserver( pagedTable: Interaction.self, pageSize: ConversationViewModel.pageSize, idColumn: .id, @@ -113,58 +179,20 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { return } - self?.onInteractionChange?(updatedInteractionData) + // If we have the 'onInteractionChanged' callback then trigger it, otherwise just store the changes + // to be sent to the callback if we ever start observing again (when we have the callback it needs + // to do the data updating as it's tied to UI updates and can cause crashes if not updated in the + // correct order) + guard let onInteractionChange: (([SectionModel]) -> ()) = self?.onInteractionChange else { + self?.unobservedInteractionDataChanges = updatedInteractionData + return + } + + onInteractionChange(updatedInteractionData) } ) - - // Run the initial query on a backgorund thread so we don't block the push transition - DispatchQueue.global(qos: .default).async { [weak self] in - // If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query - // from a `0` offset) - guard let initialFocusedId: Int64 = focusedInteractionId else { - self?.pagedDataObserver?.load(.pageBefore) - return - } - - self?.pagedDataObserver?.load(.initialPageAround(id: initialFocusedId)) - } - } - - // MARK: - Thread Data - - /// This value is the current state of the view - public private(set) var threadData: SessionThreadViewModel = SessionThreadViewModel() - - /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise - /// performance https://github.com/groue/GRDB.swift#valueobservation-performance - /// - /// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static - /// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries - /// - /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) - /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own - /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) - /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this - public lazy var observableThreadData = ValueObservation - .trackingConstantRegion { [threadId = self.threadId] db -> SessionThreadViewModel? in - let userPublicKey: String = getUserHexEncodedPublicKey(db) - - return try SessionThreadViewModel - .conversationQuery(threadId: threadId, userPublicKey: userPublicKey) - .fetchOne(db) - } - .removeDuplicates() - - public func updateThreadData(_ updatedData: SessionThreadViewModel) { - self.threadData = updatedData } - // MARK: - Interaction Data - - public private(set) var interactionData: [SectionModel] = [] - public private(set) var pagedDataObserver: PagedDatabaseObserver? - public var onInteractionChange: (([SectionModel]) -> ())? - private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let sortedData: [MessageViewModel] = data @@ -361,6 +389,26 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { } } + public func swapToThread(updatedThreadId: String) { + let oldestMessageId: Int64? = self.interactionData + .filter { $0.model == .messages } + .first? + .elements + .first? + .id + + self.threadId = updatedThreadId + self.observableThreadData = self.setupObservableThreadData(for: updatedThreadId) + self.pagedDataObserver = self.setupPagedObserver(for: updatedThreadId) + + // Try load everything up to the initial visible message, fallback to just the initial page of messages + // if we don't have one + switch oldestMessageId { + case .some(let id): self.pagedDataObserver?.load(.untilInclusive(id: id, padding: 0)) + case .none: self.pagedDataObserver?.load(.pageBefore) + } + } + // MARK: - Audio Playback public struct PlaybackInfo { diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 86e2de343..b7334eeae 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -35,8 +35,18 @@ public class MediaGalleryViewModel { public private(set) var pagedDataObserver: PagedDatabaseObserver? /// This value is the current state of a gallery view + private var unobservedGalleryDataChanges: [SectionModel]? public private(set) var galleryData: [SectionModel] = [] - public var onGalleryChange: (([SectionModel]) -> ())? + public var onGalleryChange: (([SectionModel]) -> ())? { + didSet { + // When starting to observe interaction changes we want to trigger a UI update just in case the + // data was changed while we weren't observing + if let unobservedGalleryDataChanges: [SectionModel] = self.unobservedGalleryDataChanges { + onGalleryChange?(unobservedGalleryDataChanges) + self.unobservedGalleryDataChanges = nil + } + } + } // MARK: - Initialization @@ -78,7 +88,16 @@ public class MediaGalleryViewModel { return } - self?.onGalleryChange?(updatedGalleryData) + // If we have the 'onGalleryChange' callback then trigger it, otherwise just store the changes + // to be sent to the callback if we ever start observing again (when we have the callback it needs + // to do the data updating as it's tied to UI updates and can cause crashes if not updated in the + // correct order) + guard let onGalleryChange: (([SectionModel]) -> ()) = self?.onGalleryChange else { + self?.unobservedGalleryDataChanges = updatedGalleryData + return + } + + onGalleryChange(updatedGalleryData) } ) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 27f6c6680..82bee6cff 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -299,14 +299,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } - let navController: UINavigationController = OWSNavigationController( + self.window?.rootViewController = OWSNavigationController( rootViewController: (Identity.userExists() ? HomeVC() : LandingVC() ) ) - navController.isNavigationBarHidden = !(navController.viewControllers.first is HomeVC) - self.window?.rootViewController = navController UIViewController.attemptRotationToDeviceOrientation() } diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index b9cc2d4cf..50fe281b6 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -183,8 +183,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { // Don't fire the notification if the current user isn't mentioned // and isOnlyNotifyingForMentions is on. - guard !thread.onlyNotifyForMentions || interaction.isUserMentioned(db) else { return } - + guard !thread.onlyNotifyForMentions || interaction.hasMention else { return } + let notificationTitle: String? var notificationBody: String? @@ -445,14 +445,13 @@ class NotificationActionHandler { } let promise: Promise = GRDBStorage.shared.write { db in - let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let interaction: Interaction = try Interaction( threadId: thread.id, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: replyText, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), - hasMention: replyText.contains("@\(currentUserPublicKey)") + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText) ).inserted(db) try Interaction.markAsRead( diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 4d97e3661..7fd2a55db 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -833,9 +833,11 @@ enum _003_YDBToGRDBMigration: Migration { timestampMs: Int64(legacyInteraction.timestamp), receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp), wasRead: wasRead, - hasMention: ( - body?.contains("@\(currentUserPublicKey)") == true || - quotedMessage?.authorId == currentUserPublicKey + hasMention: Interaction.isUserMentioned( + db, + threadId: threadId, + body: body, + quoteAuthorId: quotedMessage?.authorId ), // For both of these '0' used to be equivalent to null expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ? diff --git a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift index 081042fe3..bba1bb48e 100644 --- a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift +++ b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift @@ -112,7 +112,9 @@ public extension BlindedIdLookup { guard lookup.sessionId == nil else { return lookup } // Lastly loop through existing id lookups (in case the user is looking at a different SOGS but once had - // a thread with this contact in a different SOGS and had cached the lookup) + // a thread with this contact in a different SOGS and had cached the lookup) - we really should never hit + // this case since the contact approval status is sync'ed (the only situation I can think of is a config + // message hasn't been handled correctly?) let blindedIdLookupCursor: RecordCursor = try BlindedIdLookup .filter(BlindedIdLookup.Columns.sessionId != nil) .filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased()) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 62af0107d..43c63b5c1 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import Sodium import SessionUtilitiesKit public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { @@ -588,19 +589,44 @@ public extension Interaction { ) } - func isUserMentioned(_ db: Database) -> Bool { - guard variant == .standardIncoming else { return false } + static func isUserMentioned( + _ db: Database, + threadId: String, + body: String?, + quoteAuthorId: String? = nil + ) -> Bool { + var publicKeysToCheck: [String] = [ + getUserHexEncodedPublicKey(db) + ] - let userPublicKey: String = getUserHexEncodedPublicKey(db) + // If the thread is an open group then add the blinded id as a key to check + if let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: threadId) { + let sodium: Sodium = Sodium() + + if + let userEd25519KeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db), + let blindedKeyPair: Box.KeyPair = sodium.blindedKeyPair( + serverPublicKey: openGroup.publicKey, + edKeyPair: userEd25519KeyPair, + genericHash: sodium.genericHash + ) + { + publicKeysToCheck.append( + SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString + ) + } + } - return ( + // A user is mentioned if their public key is in the body of a message or one of their messages + // was quoted + return publicKeysToCheck.contains { publicKey in ( body != nil && - (body ?? "").contains("@\(userPublicKey)") + (body ?? "").contains("@\(publicKey)") ) || ( - (try? quote.fetchOne(db))?.authorId == userPublicKey + quoteAuthorId == publicKey ) - ) + } } /// Use the `Interaction.previewText` method directly where possible rather than this method as it diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 9c63fc773..e9e96e518 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -590,8 +590,19 @@ public final class OpenGroupManager: NSObject { ) } } - catch let error { - SNLog("Couldn't receive inbox message due to error: \(error).") + catch { + switch error { + // Ignore duplicate and self-send errors (we will always receive a duplicate message back + // whenever we send a message so this ends up being spam otherwise) + case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, + MessageReceiverError.duplicateMessage, + MessageReceiverError.duplicateControlMessage, + MessageReceiverError.selfSend: + break + + default: + SNLog("Couldn't receive inbox message due to error: \(error).") + } } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index a90aa1db4..8f38073d2 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -132,14 +132,13 @@ extension MessageReceiver { else { // The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to // someone without approving them) - guard - let contact: Contact = try? Contact.fetchOne(db, id: senderSessionId), - !contact.didApproveMe - else { return } + let contact: Contact = Contact.fetchOrCreate(db, id: senderSessionId) + + guard !contact.didApproveMe else { return } - try? contact + _ = try? contact .with(didApproveMe: true) - .update(db) + .saved(db) } // Force a config sync to ensure all devices know the contact approval state if desired diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index f42f9878d..ebaf49fe8 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import Sodium import SignalCoreKit import SessionUtilitiesKit @@ -48,10 +49,44 @@ extension MessageReceiver { let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let thread: SessionThread = try SessionThread .fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant) - let variant: Interaction.Variant = (sender == currentUserPublicKey ? - .standardOutgoing : - .standardIncoming - ) + let variant: Interaction.Variant = { + guard + let openGroupId: String = openGroupId, + let senderSessionId: SessionId = SessionId(from: sender), + let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: openGroupId) + else { + return (sender == currentUserPublicKey ? + .standardOutgoing : + .standardIncoming + ) + } + + // Need to check if the blinded id matches for open groups + switch senderSessionId.prefix { + case .blinded: + let sodium: Sodium = Sodium() + + guard + let userEdKeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db), + let blindedKeyPair: Box.KeyPair = sodium.blindedKeyPair( + serverPublicKey: openGroup.publicKey, + edKeyPair: userEdKeyPair, + genericHash: sodium.genericHash + ) + else { return .standardIncoming } + + return (sender == SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString ? + .standardOutgoing : + .standardIncoming + ) + + case .standard, .unblinded: + return (sender == currentUserPublicKey ? + .standardOutgoing : + .standardIncoming + ) + } + }() // Retrieve the disappearing messages config to set the 'expiresInSeconds' value // accoring to the config @@ -74,9 +109,11 @@ extension MessageReceiver { body: message.text, timestampMs: Int64(messageSentTimestamp * 1000), wasRead: (variant == .standardOutgoing), // Auto-mark sent messages as read - hasMention: ( - message.text?.contains("@\(currentUserPublicKey)") == true || - dataMessage.quote?.author == currentUserPublicKey + hasMention: Interaction.isUserMentioned( + db, + threadId: thread.id, + body: message.text, + quoteAuthorId: dataMessage.quote?.author ), // Note: Ensure we don't ever expire open group messages expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ? diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index ca80c1015..eaf209a87 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -67,11 +67,12 @@ public final class MessageSender { let (promise, seal) = Promise.pending() let userPublicKey: String = getUserHexEncodedPublicKey(db) let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) + let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) // Set the timestamp, sender and recipient message.sentTimestamp = ( message.sentTimestamp ?? // Visible messages will already have their sent timestamp set - UInt64(floor(Date().timeIntervalSince1970 * 1000)) + UInt64(messageSendTimestamp) ) message.sender = userPublicKey message.recipient = { @@ -196,13 +197,12 @@ public final class MessageSender { // Send the result let base64EncodedData = wrappedMessage.base64EncodedString() - let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset) let snodeMessage = SnodeMessage( recipient: message.recipient!, data: base64EncodedData, ttl: message.ttl, - timestampMs: timestamp + timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset) ) SnodeAPI @@ -529,6 +529,8 @@ public final class MessageSender { using: dependencies ) .done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in + message.openGroupServerMessageId = UInt64(data.id) + dependencies.storage.write { transaction in try MessageSender.handleSuccessfulMessageSend( db, diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 1c1cdb1f7..491c55275 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -50,7 +50,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { var notificationTitle: String = senderName if thread.variant == .closedGroup || thread.variant == .openGroup { - if thread.onlyNotifyForMentions && !interaction.isUserMentioned(db) { + if thread.onlyNotifyForMentions && !interaction.hasMention { // Ignore PNs if the group is set to only notify for mentions return } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 40f76ef27..e444bbe48 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -4,6 +4,7 @@ import UIKit import GRDB import PromiseKit import DifferenceKit +import Sodium import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit @@ -228,14 +229,13 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } // Create the interaction - let userPublicKey: String = getUserHexEncodedPublicKey(db) let interaction: Interaction = try Interaction( threadId: threadId, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: body, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), - hasMention: (body?.contains("@\(userPublicKey)") == true), + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) ).inserted(db) diff --git a/SessionUtilitiesKit/Database/GRDBStorage.swift b/SessionUtilitiesKit/Database/GRDBStorage.swift index 1b4ef46ec..11f74acec 100644 --- a/SessionUtilitiesKit/Database/GRDBStorage.swift +++ b/SessionUtilitiesKit/Database/GRDBStorage.swift @@ -337,6 +337,13 @@ public final class GRDBStorage { dbPool.add(transactionObserver: observer) } + + public func removeObserver(_ observer: TransactionObserver?) { + guard isValid, let dbPool: DatabasePool = dbPool else { return } + guard let observer: TransactionObserver = observer else { return } + + dbPool.remove(transactionObserver: observer) + } } // MARK: - Promise Extensions diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index 25e67d4ec..7aee892a3 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -200,11 +200,10 @@ public class PagedDatabaseObserver: TransactionObserver where } // If there are no inserted/updated rows then trigger the update callback and stop here - let rowIdsToQuery: [Int64] = relevantChanges + let changesToQuery: [PagedData.TrackedChange] = relevantChanges .filter { $0.kind != .delete } - .map { $0.rowId } - guard !rowIdsToQuery.isEmpty else { + guard !changesToQuery.isEmpty else { updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, !deletionChanges.isEmpty) return } @@ -212,7 +211,7 @@ public class PagedDatabaseObserver: TransactionObserver where // Fetch the indexes of the rowIds so we can determine whether they should be added to the screen let itemIndexes: [Int64] = PagedData.indexes( db, - rowIds: rowIdsToQuery, + rowIds: changesToQuery.map { $0.rowId }, tableName: pagedTableName, orderSQL: orderSQL, filterSQL: filterSQL @@ -224,17 +223,21 @@ public class PagedDatabaseObserver: TransactionObserver where // added at once) let itemIndexesAreSequential: Bool = (itemIndexes.map { $0 - 1 }.dropFirst() == itemIndexes.dropLast()) let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in - index >= updatedPageInfo.pageOffset && - index < updatedPageInfo.currentCount + index >= updatedPageInfo.pageOffset && ( + index < updatedPageInfo.currentCount || + updatedPageInfo.currentCount == 0 + ) }) - let validRowIds: [Int64] = (itemIndexesAreSequential && hasOneValidIndex ? - rowIdsToQuery : - zip(itemIndexes, rowIdsToQuery) + let validChanges: [PagedData.TrackedChange] = (itemIndexesAreSequential && hasOneValidIndex ? + changesToQuery : + zip(itemIndexes, changesToQuery) .filter { index, _ -> Bool in - index >= updatedPageInfo.pageOffset && - index < updatedPageInfo.currentCount + index >= updatedPageInfo.pageOffset && ( + index < updatedPageInfo.currentCount || + updatedPageInfo.currentCount == 0 + ) } - .map { _, rowId -> Int64 in rowId } + .map { _, change -> PagedData.TrackedChange in change } ) let countBefore: Int = itemIndexes.filter { $0 < updatedPageInfo.pageOffset }.count @@ -244,18 +247,18 @@ public class PagedDatabaseObserver: TransactionObserver where pageSize: updatedPageInfo.pageSize, pageOffset: (updatedPageInfo.pageOffset + countBefore), currentCount: updatedPageInfo.currentCount, - totalCount: (updatedPageInfo.totalCount + itemIndexes.count) + totalCount: (updatedPageInfo.totalCount + validChanges.filter { $0.kind == .insert }.count) ) // If there are no valid row ids then stop here (trigger updates though since the page info // has changes) - guard !validRowIds.isEmpty else { + guard !validChanges.isEmpty else { updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true) return } // Fetch the inserted/updated rows - let additionalFilters: SQL = SQL(validRowIds.contains(Column.rowID)) + let additionalFilters: SQL = SQL(validChanges.map { $0.rowId }.contains(Column.rowID)) let updatedItems: [T] = (try? dataQuery(additionalFilters, nil) .fetchAll(db)) .defaulting(to: []) @@ -390,8 +393,9 @@ public class PagedDatabaseObserver: TransactionObserver where ) } - // Otherwise load after - let finalIndex: Int = min(totalCount, (targetIndex + abs(padding))) + // Otherwise load after (targetIndex is 0-indexed so we need to add 1 for this to + // have the correct 'limit' value) + let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding))) return ( (finalIndex - cacheCurrentEndIndex), @@ -937,15 +941,19 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet let uniqueIndexes: [Int64] = itemIndexes.asSet().sorted() let itemIndexesAreSequential: Bool = (uniqueIndexes.map { $0 - 1 }.dropFirst() == uniqueIndexes.dropLast()) let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in - index >= pageInfo.pageOffset && - index < pageInfo.currentCount + index >= pageInfo.pageOffset && ( + index < pageInfo.currentCount || + pageInfo.currentCount == 0 + ) }) let validRowIds: [Int64] = (itemIndexesAreSequential && hasOneValidIndex ? rowIdsToQuery : zip(itemIndexes, rowIdsToQuery) .filter { index, _ -> Bool in - index >= pageInfo.pageOffset && - index < pageInfo.currentCount + index >= pageInfo.pageOffset && ( + index < pageInfo.currentCount || + pageInfo.currentCount == 0 + ) } .map { _, rowId -> Int64 in rowId } ) diff --git a/SessionUtilitiesKit/Media/Updatable.swift b/SessionUtilitiesKit/Media/Updatable.swift index ccd7fa03b..4a4a39495 100644 --- a/SessionUtilitiesKit/Media/Updatable.swift +++ b/SessionUtilitiesKit/Media/Updatable.swift @@ -72,6 +72,14 @@ public func ?? (updatable: Updatable, existingValue: @autoclosure () throw } } +public func ?? (updatable: Updatable>, existingValue: @autoclosure () throws -> T?) rethrows -> T? { + switch updatable { + case .remove: return nil + case .existing: return try existingValue() + case .update(let newValue): return newValue + } +} + // MARK: - ExpressibleBy Conformance extension Updatable {