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
This commit is contained in:
Morgan Pretty 2022-06-16 13:14:56 +10:00
parent c56cc99d40
commit ff08579088
18 changed files with 310 additions and 204 deletions

View File

@ -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(

View File

@ -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..<viewControllerIndex]
// ),
// [conversationVC],
// (viewControllerIndex == (navController.viewControllers.count - 1) ?
// [] :
// navController.viewControllers[(viewControllerIndex + 1)..<navController.viewControllers.count]
// )
// ].flatMap { $0 }
//
// // If the top vew controller isn't the current one then we need to make sure to swap out child ones as well
// if !currentlyOnThisScreen {
// let maybeSettingsViewController: UIViewController? = navController
// .viewControllers[viewControllerIndex..<navController.viewControllers.count]
// .first(where: { $0 is OWSConversationSettingsViewController })
//
// // Update the settings screen (if there is one)
// if let settingsViewController: OWSConversationSettingsViewController = maybeSettingsViewController as? OWSConversationSettingsViewController {
// settingsViewController.configure(with: newThread, uiDatabaseConnection: OWSPrimaryStorage.shared().uiDatabaseConnection)
// }
// }
//
// // Try to minimise painful UX issues by keeping the 'first responder' state, current input text and
// // cursor position (Unfortunately there doesn't seem to be a way to prevent the keyboard from
// // flickering during the swap but other than that it's relatively seamless)
// if self.snInputView.inputTextViewIsFirstResponder {
// conversationVC.isReplacingThread = true
// conversationVC.snInputView.frame = self.snInputView.frame
// conversationVC.snInputView.text = self.snInputView.text
// conversationVC.snInputView.selectedRange = self.snInputView.selectedRange
//
// // Make the current snInputView invisible and add the new one the the UI
// self.snInputView.alpha = 0
// self.snInputView.superview?.addSubview(conversationVC.snInputView)
//
// // Add the old first responder to the window so it the keyboard won't get dismissed when the
// // OS removes it's parent view from the view hierarchy due to the view controller swap
// var maybeOldFirstResponderView: UIView?
//
// if let oldFirstResponderView: UIView = UIResponder.currentFirstResponder() as? UIView {
// maybeOldFirstResponderView = oldFirstResponderView
// self.view.window?.addSubview(oldFirstResponderView)
// }
//
// // On the next run loop setup the first responder state for the new screen and remove the
// // old first responder from the window
// DispatchQueue.main.async {
// UIView.performWithoutAnimation {
// conversationVC.isReplacingThread = false
// maybeOldFirstResponderView?.resignFirstResponder()
// maybeOldFirstResponderView?.removeFromSuperview()
// conversationVC.snInputView.removeFromSuperview()
//
// _ = conversationVC.becomeFirstResponder()
// conversationVC.snInputView.inputTextViewBecomeFirstResponder()
// }
// }
// }
// }
}
// MARK: - UITableViewDataSource

View File

@ -30,7 +30,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
public static let pageSize: Int = 50
private let threadId: String
private var threadId: String
public let initialThreadVariant: SessionThread.Variant
public var sentMessageBeforeUpdate: Bool = false
public var lastSearchedText: String?
@ -62,7 +62,73 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// also want to skip the initial query and trigger it async so that the push animation
// doesn't stutter (it should load basically immediately but without this there is a
// distinct stutter)
self.pagedDataObserver = PagedDatabaseObserver(
self.pagedDataObserver = self.setupPagedObserver(for: threadId)
// 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<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<SessionThreadViewModel?>>> = setupObservableThreadData(for: self.threadId)
private func setupObservableThreadData(for threadId: String) -> ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<SessionThreadViewModel?>>> {
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<Interaction, MessageViewModel>?
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<Interaction, MessageViewModel> {
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<Interaction, MessageViewModel>?
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 {

View File

@ -35,8 +35,18 @@ public class MediaGalleryViewModel {
public private(set) var pagedDataObserver: PagedDatabaseObserver<Attachment, Item>?
/// 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)
}
)

View File

@ -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()
}

View File

@ -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<Void> = 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(

View File

@ -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 ?

View File

@ -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<BlindedIdLookup> = try BlindedIdLookup
.filter(BlindedIdLookup.Columns.sessionId != nil)
.filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased())

View File

@ -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

View File

@ -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).")
}
}
}
}

View File

@ -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

View File

@ -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 ?

View File

@ -67,11 +67,12 @@ public final class MessageSender {
let (promise, seal) = Promise<Void>.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,

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -200,11 +200,10 @@ public class PagedDatabaseObserver<ObservedTable, T>: 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<ObservedTable, T>: 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<ObservedTable, T>: 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<ObservedTable, T>: 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<ObservedTable, T>: 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<T, PagedType>: 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 }
)

View File

@ -72,6 +72,14 @@ public func ?? <T>(updatable: Updatable<T>, existingValue: @autoclosure () throw
}
}
public func ?? <T>(updatable: Updatable<Optional<T>>, 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 {