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
pull/612/head
Morgan Pretty 2 years ago
parent c56cc99d40
commit ff08579088

@ -366,14 +366,13 @@ extension ConversationVC:
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
// Create the interaction // Create the interaction
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
threadId: threadId, threadId: threadId,
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: text, body: text,
timestampMs: sentTimestampMs, timestampMs: sentTimestampMs,
hasMention: text.contains("@\(userPublicKey)"), hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text),
linkPreviewUrl: linkPreviewDraft?.urlString linkPreviewUrl: linkPreviewDraft?.urlString
).inserted(db) ).inserted(db)
@ -464,14 +463,13 @@ extension ConversationVC:
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
// Create the interaction // Create the interaction
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
threadId: threadId, threadId: threadId,
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: text, body: text,
timestampMs: sentTimestampMs, timestampMs: sentTimestampMs,
hasMention: text.contains("@\(userPublicKey)") hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text)
).inserted(db) ).inserted(db)
try MessageSender.send( try MessageSender.send(

@ -411,8 +411,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
name: UIResponder.keyboardWillHideNotification, name: UIResponder.keyboardWillHideNotification,
object: nil object: nil
) )
// notificationCenter.addObserver(self, selector: #selector(handleContactThreadReplaced(_:)), name: .contactThreadReplaced, object: nil)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -487,7 +485,36 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
viewModel.observableThreadData, viewModel.observableThreadData,
onError: { _ in }, onError: { _ in },
onChange: { [weak self] maybeThreadData 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 // The default scheduler emits changes on the main thread
self?.handleThreadUpdates(threadData) self?.handleThreadUpdates(threadData)
@ -1095,91 +1122,6 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
} }
} }
@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 // MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int { func numberOfSections(in tableView: UITableView) -> Int {

@ -30,7 +30,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
public static let pageSize: Int = 50 public static let pageSize: Int = 50
private let threadId: String private var threadId: String
public let initialThreadVariant: SessionThread.Variant public let initialThreadVariant: SessionThread.Variant
public var sentMessageBeforeUpdate: Bool = false public var sentMessageBeforeUpdate: Bool = false
public var lastSearchedText: String? 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 // 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 // doesn't stutter (it should load basically immediately but without this there is a
// distinct stutter) // 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, pagedTable: Interaction.self,
pageSize: ConversationViewModel.pageSize, pageSize: ConversationViewModel.pageSize,
idColumn: .id, idColumn: .id,
@ -113,58 +179,20 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
return 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)
// Run the initial query on a backgorund thread so we don't block the push transition guard let onInteractionChange: (([SectionModel]) -> ()) = self?.onInteractionChange else {
DispatchQueue.global(qos: .default).async { [weak self] in self?.unobservedInteractionDataChanges = updatedInteractionData
// 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 return
} }
self?.pagedDataObserver?.load(.initialPageAround(id: initialFocusedId)) onInteractionChange(updatedInteractionData)
}
} }
)
// 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] { private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true })
let sortedData: [MessageViewModel] = data 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 // MARK: - Audio Playback
public struct PlaybackInfo { public struct PlaybackInfo {

@ -35,8 +35,18 @@ public class MediaGalleryViewModel {
public private(set) var pagedDataObserver: PagedDatabaseObserver<Attachment, Item>? public private(set) var pagedDataObserver: PagedDatabaseObserver<Attachment, Item>?
/// This value is the current state of a gallery view /// This value is the current state of a gallery view
private var unobservedGalleryDataChanges: [SectionModel]?
public private(set) var galleryData: [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 // MARK: - Initialization
@ -78,7 +88,16 @@ public class MediaGalleryViewModel {
return 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)
} }
) )

@ -299,14 +299,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return return
} }
let navController: UINavigationController = OWSNavigationController( self.window?.rootViewController = OWSNavigationController(
rootViewController: (Identity.userExists() ? rootViewController: (Identity.userExists() ?
HomeVC() : HomeVC() :
LandingVC() LandingVC()
) )
) )
navController.isNavigationBarHidden = !(navController.viewControllers.first is HomeVC)
self.window?.rootViewController = navController
UIViewController.attemptRotationToDeviceOrientation() UIViewController.attemptRotationToDeviceOrientation()
} }

@ -183,7 +183,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// Don't fire the notification if the current user isn't mentioned // Don't fire the notification if the current user isn't mentioned
// and isOnlyNotifyingForMentions is on. // and isOnlyNotifyingForMentions is on.
guard !thread.onlyNotifyForMentions || interaction.isUserMentioned(db) else { return } guard !thread.onlyNotifyForMentions || interaction.hasMention else { return }
let notificationTitle: String? let notificationTitle: String?
var notificationBody: String? var notificationBody: String?
@ -445,14 +445,13 @@ class NotificationActionHandler {
} }
let promise: Promise<Void> = GRDBStorage.shared.write { db in let promise: Promise<Void> = GRDBStorage.shared.write { db in
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
threadId: thread.id, threadId: thread.id,
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: replyText, body: replyText,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
hasMention: replyText.contains("@\(currentUserPublicKey)") hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText)
).inserted(db) ).inserted(db)
try Interaction.markAsRead( try Interaction.markAsRead(

@ -833,9 +833,11 @@ enum _003_YDBToGRDBMigration: Migration {
timestampMs: Int64(legacyInteraction.timestamp), timestampMs: Int64(legacyInteraction.timestamp),
receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp), receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp),
wasRead: wasRead, wasRead: wasRead,
hasMention: ( hasMention: Interaction.isUserMentioned(
body?.contains("@\(currentUserPublicKey)") == true || db,
quotedMessage?.authorId == currentUserPublicKey threadId: threadId,
body: body,
quoteAuthorId: quotedMessage?.authorId
), ),
// For both of these '0' used to be equivalent to null // For both of these '0' used to be equivalent to null
expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ? expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ?

@ -112,7 +112,9 @@ public extension BlindedIdLookup {
guard lookup.sessionId == nil else { return lookup } 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 // 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 let blindedIdLookupCursor: RecordCursor<BlindedIdLookup> = try BlindedIdLookup
.filter(BlindedIdLookup.Columns.sessionId != nil) .filter(BlindedIdLookup.Columns.sessionId != nil)
.filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased()) .filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased())

@ -2,6 +2,7 @@
import Foundation import Foundation
import GRDB import GRDB
import Sodium
import SessionUtilitiesKit import SessionUtilitiesKit
public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
@ -588,20 +589,45 @@ public extension Interaction {
) )
} }
func isUserMentioned(_ db: Database) -> Bool { static func isUserMentioned(
guard variant == .standardIncoming else { return false } _ 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()
return ( 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
)
}
}
// 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 != 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 /// Use the `Interaction.previewText` method directly where possible rather than this method as it
/// makes it's own database queries /// makes it's own database queries

@ -590,11 +590,22 @@ public final class OpenGroupManager: NSObject {
) )
} }
} }
catch let 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).") SNLog("Couldn't receive inbox message due to error: \(error).")
} }
} }
} }
}
// MARK: - Convenience // MARK: - Convenience

@ -132,14 +132,13 @@ extension MessageReceiver {
else { else {
// The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to // The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to
// someone without approving them) // someone without approving them)
guard let contact: Contact = Contact.fetchOrCreate(db, id: senderSessionId)
let contact: Contact = try? Contact.fetchOne(db, id: senderSessionId),
!contact.didApproveMe
else { return }
try? contact guard !contact.didApproveMe else { return }
_ = try? contact
.with(didApproveMe: true) .with(didApproveMe: true)
.update(db) .saved(db)
} }
// Force a config sync to ensure all devices know the contact approval state if desired // Force a config sync to ensure all devices know the contact approval state if desired

@ -2,6 +2,7 @@
import Foundation import Foundation
import GRDB import GRDB
import Sodium
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
@ -48,11 +49,45 @@ extension MessageReceiver {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant) .fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
let variant: Interaction.Variant = (sender == currentUserPublicKey ? 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 : .standardOutgoing :
.standardIncoming .standardIncoming
) )
case .standard, .unblinded:
return (sender == currentUserPublicKey ?
.standardOutgoing :
.standardIncoming
)
}
}()
// Retrieve the disappearing messages config to set the 'expiresInSeconds' value // Retrieve the disappearing messages config to set the 'expiresInSeconds' value
// accoring to the config // accoring to the config
let disappearingMessagesConfiguration: DisappearingMessagesConfiguration = (try? thread.disappearingMessagesConfiguration.fetchOne(db)) let disappearingMessagesConfiguration: DisappearingMessagesConfiguration = (try? thread.disappearingMessagesConfiguration.fetchOne(db))
@ -74,9 +109,11 @@ extension MessageReceiver {
body: message.text, body: message.text,
timestampMs: Int64(messageSentTimestamp * 1000), timestampMs: Int64(messageSentTimestamp * 1000),
wasRead: (variant == .standardOutgoing), // Auto-mark sent messages as read wasRead: (variant == .standardOutgoing), // Auto-mark sent messages as read
hasMention: ( hasMention: Interaction.isUserMentioned(
message.text?.contains("@\(currentUserPublicKey)") == true || db,
dataMessage.quote?.author == currentUserPublicKey threadId: thread.id,
body: message.text,
quoteAuthorId: dataMessage.quote?.author
), ),
// Note: Ensure we don't ever expire open group messages // Note: Ensure we don't ever expire open group messages
expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ? expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ?

@ -67,11 +67,12 @@ public final class MessageSender {
let (promise, seal) = Promise<Void>.pending() let (promise, seal) = Promise<Void>.pending()
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
message.sentTimestamp = ( message.sentTimestamp = (
message.sentTimestamp ?? // Visible messages will already have their sent timestamp set message.sentTimestamp ?? // Visible messages will already have their sent timestamp set
UInt64(floor(Date().timeIntervalSince1970 * 1000)) UInt64(messageSendTimestamp)
) )
message.sender = userPublicKey message.sender = userPublicKey
message.recipient = { message.recipient = {
@ -196,13 +197,12 @@ public final class MessageSender {
// Send the result // Send the result
let base64EncodedData = wrappedMessage.base64EncodedString() let base64EncodedData = wrappedMessage.base64EncodedString()
let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset)
let snodeMessage = SnodeMessage( let snodeMessage = SnodeMessage(
recipient: message.recipient!, recipient: message.recipient!,
data: base64EncodedData, data: base64EncodedData,
ttl: message.ttl, ttl: message.ttl,
timestampMs: timestamp timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset)
) )
SnodeAPI SnodeAPI
@ -529,6 +529,8 @@ public final class MessageSender {
using: dependencies using: dependencies
) )
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in .done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
message.openGroupServerMessageId = UInt64(data.id)
dependencies.storage.write { transaction in dependencies.storage.write { transaction in
try MessageSender.handleSuccessfulMessageSend( try MessageSender.handleSuccessfulMessageSend(
db, db,

@ -50,7 +50,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
var notificationTitle: String = senderName var notificationTitle: String = senderName
if thread.variant == .closedGroup || thread.variant == .openGroup { 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 // Ignore PNs if the group is set to only notify for mentions
return return
} }

@ -4,6 +4,7 @@ import UIKit
import GRDB import GRDB
import PromiseKit import PromiseKit
import DifferenceKit import DifferenceKit
import Sodium
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
@ -228,14 +229,13 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
} }
// Create the interaction // Create the interaction
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
threadId: threadId, threadId: threadId,
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: body, body: body,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), 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) linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
).inserted(db) ).inserted(db)

@ -337,6 +337,13 @@ public final class GRDBStorage {
dbPool.add(transactionObserver: observer) 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 // MARK: - Promise Extensions

@ -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 // 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 } .filter { $0.kind != .delete }
.map { $0.rowId }
guard !rowIdsToQuery.isEmpty else { guard !changesToQuery.isEmpty else {
updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, !deletionChanges.isEmpty) updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, !deletionChanges.isEmpty)
return 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 // Fetch the indexes of the rowIds so we can determine whether they should be added to the screen
let itemIndexes: [Int64] = PagedData.indexes( let itemIndexes: [Int64] = PagedData.indexes(
db, db,
rowIds: rowIdsToQuery, rowIds: changesToQuery.map { $0.rowId },
tableName: pagedTableName, tableName: pagedTableName,
orderSQL: orderSQL, orderSQL: orderSQL,
filterSQL: filterSQL filterSQL: filterSQL
@ -224,17 +223,21 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
// added at once) // added at once)
let itemIndexesAreSequential: Bool = (itemIndexes.map { $0 - 1 }.dropFirst() == itemIndexes.dropLast()) let itemIndexesAreSequential: Bool = (itemIndexes.map { $0 - 1 }.dropFirst() == itemIndexes.dropLast())
let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in
index >= updatedPageInfo.pageOffset && index >= updatedPageInfo.pageOffset && (
index < updatedPageInfo.currentCount index < updatedPageInfo.currentCount ||
updatedPageInfo.currentCount == 0
)
}) })
let validRowIds: [Int64] = (itemIndexesAreSequential && hasOneValidIndex ? let validChanges: [PagedData.TrackedChange] = (itemIndexesAreSequential && hasOneValidIndex ?
rowIdsToQuery : changesToQuery :
zip(itemIndexes, rowIdsToQuery) zip(itemIndexes, changesToQuery)
.filter { index, _ -> Bool in .filter { index, _ -> Bool in
index >= updatedPageInfo.pageOffset && index >= updatedPageInfo.pageOffset && (
index < updatedPageInfo.currentCount 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 let countBefore: Int = itemIndexes.filter { $0 < updatedPageInfo.pageOffset }.count
@ -244,18 +247,18 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
pageSize: updatedPageInfo.pageSize, pageSize: updatedPageInfo.pageSize,
pageOffset: (updatedPageInfo.pageOffset + countBefore), pageOffset: (updatedPageInfo.pageOffset + countBefore),
currentCount: updatedPageInfo.currentCount, 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 // If there are no valid row ids then stop here (trigger updates though since the page info
// has changes) // has changes)
guard !validRowIds.isEmpty else { guard !validChanges.isEmpty else {
updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true) updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true)
return return
} }
// Fetch the inserted/updated rows // 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) let updatedItems: [T] = (try? dataQuery(additionalFilters, nil)
.fetchAll(db)) .fetchAll(db))
.defaulting(to: []) .defaulting(to: [])
@ -390,8 +393,9 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
) )
} }
// Otherwise load after // Otherwise load after (targetIndex is 0-indexed so we need to add 1 for this to
let finalIndex: Int = min(totalCount, (targetIndex + abs(padding))) // have the correct 'limit' value)
let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding)))
return ( return (
(finalIndex - cacheCurrentEndIndex), (finalIndex - cacheCurrentEndIndex),
@ -937,15 +941,19 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
let uniqueIndexes: [Int64] = itemIndexes.asSet().sorted() let uniqueIndexes: [Int64] = itemIndexes.asSet().sorted()
let itemIndexesAreSequential: Bool = (uniqueIndexes.map { $0 - 1 }.dropFirst() == uniqueIndexes.dropLast()) let itemIndexesAreSequential: Bool = (uniqueIndexes.map { $0 - 1 }.dropFirst() == uniqueIndexes.dropLast())
let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in let hasOneValidIndex: Bool = itemIndexes.contains(where: { index -> Bool in
index >= pageInfo.pageOffset && index >= pageInfo.pageOffset && (
index < pageInfo.currentCount index < pageInfo.currentCount ||
pageInfo.currentCount == 0
)
}) })
let validRowIds: [Int64] = (itemIndexesAreSequential && hasOneValidIndex ? let validRowIds: [Int64] = (itemIndexesAreSequential && hasOneValidIndex ?
rowIdsToQuery : rowIdsToQuery :
zip(itemIndexes, rowIdsToQuery) zip(itemIndexes, rowIdsToQuery)
.filter { index, _ -> Bool in .filter { index, _ -> Bool in
index >= pageInfo.pageOffset && index >= pageInfo.pageOffset && (
index < pageInfo.currentCount index < pageInfo.currentCount ||
pageInfo.currentCount == 0
)
} }
.map { _, rowId -> Int64 in rowId } .map { _, rowId -> Int64 in rowId }
) )

@ -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 // MARK: - ExpressibleBy Conformance
extension Updatable { extension Updatable {

Loading…
Cancel
Save