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:
parent
c56cc99d40
commit
ff08579088
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue