mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Optimised the home screen query (~50% speed improvement)
Updated to the latest version of GRDB Renamed some variables for clarity Updated the "seed viewed" banner on the HomeVC to be driven by a database setting to be consistent with other UI changes
This commit is contained in:
parent
8ff542405c
commit
18d833f152
30 changed files with 300 additions and 317 deletions
|
@ -27,7 +27,7 @@ PODS:
|
|||
- DifferenceKit/Core (1.2.0)
|
||||
- DifferenceKit/UIKitExtension (1.2.0):
|
||||
- DifferenceKit/Core
|
||||
- GRDB.swift/SQLCipher (5.24.0):
|
||||
- GRDB.swift/SQLCipher (5.24.1):
|
||||
- SQLCipher (>= 3.4.0)
|
||||
- Mantle (2.1.0):
|
||||
- Mantle/extobjc (= 2.1.0)
|
||||
|
@ -203,7 +203,7 @@ SPEC CHECKSUMS:
|
|||
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||
DifferenceKit: 5659c430bb7fe45876fa32ce5cba5d6167f0c805
|
||||
GRDB.swift: 7ecc8799aaa97cf1fbbcfa9d75821aa920cb713f
|
||||
GRDB.swift: b3180ce2135fc06a453297889b746b1478c4d8c7
|
||||
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
|
||||
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
||||
OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2
|
||||
|
|
|
@ -180,9 +180,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
public var onInteractionChange: (([SectionModel]) -> ())?
|
||||
|
||||
private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
|
||||
let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator })
|
||||
let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true })
|
||||
let sortedData: [MessageViewModel] = data
|
||||
.filter { !$0.isTypingIndicator }
|
||||
.filter { $0.isTypingIndicator != true }
|
||||
.sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs }
|
||||
|
||||
// We load messages from newest to oldest so having a pageOffset larger than zero means
|
||||
|
|
|
@ -34,6 +34,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
result.subtitle = NSLocalizedString("view_seed_reminder_subtitle_1", comment: "")
|
||||
result.setProgress(0.8, animated: false)
|
||||
result.delegate = self
|
||||
result.isHidden = !self.viewModel.state.showViewedSeedBanner
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -131,13 +132,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
setUpNavBarSessionHeading()
|
||||
|
||||
// Recovery phrase reminder
|
||||
let hasViewedSeed = UserDefaults.standard[.hasViewedSeed]
|
||||
if !hasViewedSeed {
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
seedReminderView.pin(.top, to: .top, of: view)
|
||||
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
||||
}
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
seedReminderView.pin(.top, to: .top, of: view)
|
||||
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
||||
|
||||
// Loading conversations label
|
||||
view.addSubview(loadingConversationsLabel)
|
||||
|
@ -149,9 +147,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
// Table view
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(.leading, to: .leading, of: view)
|
||||
if !hasViewedSeed {
|
||||
if self.viewModel.state.showViewedSeedBanner {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
||||
}
|
||||
tableView.pin(.trailing, to: .trailing, of: view)
|
||||
|
@ -187,11 +186,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
name: UIApplication.didEnterBackgroundNotification, object: nil
|
||||
)
|
||||
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: .otherUsersProfileDidChange, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: .localProfileDidChange, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
||||
|
||||
// Start polling if needed (i.e. if the user just created or restored their Session ID)
|
||||
if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.startPollersIfNeeded()
|
||||
|
@ -235,21 +229,28 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
private func startObservingChanges() {
|
||||
// Start observing for data changes
|
||||
dataChangeObservable = GRDBStorage.shared.start(
|
||||
viewModel.observableViewData,
|
||||
viewModel.observableState,
|
||||
// If we haven't done the initial load the trigger it immediately (blocking the main
|
||||
// thread so we remain on the launch screen until it completes to be consistent with
|
||||
// the old behaviour)
|
||||
scheduling: (hasLoadedInitialData ?
|
||||
.async(onQueue: .main) :
|
||||
.immediate
|
||||
),
|
||||
onError: { _ in },
|
||||
onChange: { [weak self] viewData in
|
||||
onChange: { [weak self] state in
|
||||
// The default scheduler emits changes on the main thread
|
||||
self?.handleUpdates(viewData)
|
||||
self?.handleUpdates(state)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedViewData: [ArraySection<Section, Item>]) {
|
||||
private func handleUpdates(_ updatedState: HomeViewModel.State) {
|
||||
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
||||
// in from a frame of CGRect.zero)
|
||||
guard hasLoadedInitialData else {
|
||||
hasLoadedInitialData = true
|
||||
UIView.performWithoutAnimation { handleUpdates(updatedViewData) }
|
||||
UIView.performWithoutAnimation { handleUpdates(updatedState) }
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -258,48 +259,42 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
|
||||
// Show the empty state if there is no data
|
||||
emptyStateView.isHidden = (
|
||||
!updatedViewData.isEmpty &&
|
||||
updatedViewData.contains(where: { !$0.elements.isEmpty })
|
||||
!updatedState.sections.isEmpty &&
|
||||
updatedState.sections.contains(where: { !$0.elements.isEmpty })
|
||||
)
|
||||
|
||||
// Update the 'view seed' UI
|
||||
if updatedState.showViewedSeedBanner != self.viewModel.state.showViewedSeedBanner {
|
||||
tableViewTopConstraint.isActive = false
|
||||
seedReminderView.isHidden = !updatedState.showViewedSeedBanner
|
||||
|
||||
if updatedState.showViewedSeedBanner {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
||||
}
|
||||
else {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the table content (animate changes after the first load)
|
||||
tableView.reload(
|
||||
using: StagedChangeset(source: viewModel.viewData, target: updatedViewData),
|
||||
using: StagedChangeset(source: viewModel.state.sections, target: updatedState.sections),
|
||||
deleteSectionsAnimation: .none,
|
||||
insertSectionsAnimation: .none,
|
||||
reloadSectionsAnimation: .none,
|
||||
deleteRowsAnimation: .bottom,
|
||||
insertRowsAnimation: .top,
|
||||
reloadRowsAnimation: .none,
|
||||
interrupt: {
|
||||
print("Interrupt change check: \($0.changeCount)")
|
||||
return $0.changeCount > 100
|
||||
} // Prevent too many changes from causing performance issues
|
||||
) { [weak self] updatedData in
|
||||
self?.viewModel.updateData(updatedData)
|
||||
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
||||
) { [weak self] updatedSections in
|
||||
guard let currentState: HomeViewModel.State = self?.viewModel.state else { return }
|
||||
|
||||
self?.viewModel.updateState(currentState.with(sections: updatedSections))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
|
||||
DispatchQueue.main.async {
|
||||
self.updateNavBarButtons()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleSeedViewedNotification(_ notification: Notification) {
|
||||
tableViewTopConstraint.isActive = false
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
||||
seedReminderView.removeFromSuperview()
|
||||
}
|
||||
|
||||
@objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) {
|
||||
self.tableView.reloadData() // TODO: Just reload the affected cell
|
||||
|
||||
self.viewModel.updateState(
|
||||
self.viewModel.state.with(showViewedSeedBanner: updatedState.showViewedSeedBanner)
|
||||
)
|
||||
}
|
||||
|
||||
private func updateNavBarButtons() {
|
||||
|
@ -358,15 +353,15 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
// MARK: - UITableViewDataSource
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return viewModel.viewData.count
|
||||
return viewModel.state.sections.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.viewData[section].elements.count
|
||||
return viewModel.state.sections[section].elements.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let section: ArraySection<Section, Item> = viewModel.viewData[indexPath.section]
|
||||
let section: ArraySection<Section, Item> = viewModel.state.sections[indexPath.section]
|
||||
|
||||
switch section.model {
|
||||
case .messageRequests:
|
||||
|
@ -386,7 +381,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let section: ArraySection<Section, Item> = viewModel.viewData[indexPath.section]
|
||||
let section: ArraySection<Section, Item> = viewModel.state.sections[indexPath.section]
|
||||
|
||||
switch section.model {
|
||||
case .messageRequests:
|
||||
|
@ -404,11 +399,11 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
let section: ArraySection<Section, Item> = viewModel.viewData[indexPath.section]
|
||||
let section: ArraySection<Section, Item> = viewModel.state.sections[indexPath.section]
|
||||
|
||||
switch section.model {
|
||||
case .messageRequests:
|
||||
let hide = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_HIDE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { [weak self] _, _ in
|
||||
GRDBStorage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
||||
|
||||
// Animate the row removal
|
||||
|
|
|
@ -11,8 +11,26 @@ public class HomeViewModel {
|
|||
case threads
|
||||
}
|
||||
|
||||
public struct State: Equatable {
|
||||
let showViewedSeedBanner: Bool
|
||||
let sections: [ArraySection<Section, SessionThreadViewModel>]
|
||||
|
||||
func with(
|
||||
showViewedSeedBanner: Bool? = nil,
|
||||
sections: [ArraySection<Section, SessionThreadViewModel>]? = nil
|
||||
) -> State {
|
||||
return State(
|
||||
showViewedSeedBanner: (showViewedSeedBanner ?? self.showViewedSeedBanner),
|
||||
sections: (sections ?? self.sections)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// This value is the current state of the view
|
||||
public private(set) var viewData: [ArraySection<Section, SessionThreadViewModel>] = []
|
||||
public private(set) var state: State = State(
|
||||
showViewedSeedBanner: !GRDBStorage.shared[.hasViewedSeed],
|
||||
sections: []
|
||||
)
|
||||
|
||||
/// 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
|
||||
|
@ -21,7 +39,7 @@ public class HomeViewModel {
|
|||
/// 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 observableViewData = ValueObservation
|
||||
public lazy var observableState = ValueObservation
|
||||
.tracking(
|
||||
regions: [
|
||||
// We explicitly define the regions we want to track as the automatic detection
|
||||
|
@ -34,7 +52,10 @@ public class HomeViewModel {
|
|||
.mutedUntilTimestamp,
|
||||
.onlyNotifyForMentions
|
||||
),
|
||||
Setting.filter(id: Setting.BoolKey.hasHiddenMessageRequests.rawValue),
|
||||
Setting.filter(ids: [
|
||||
Setting.BoolKey.hasHiddenMessageRequests.rawValue,
|
||||
Setting.BoolKey.hasViewedSeed.rawValue
|
||||
]),
|
||||
Contact.select(.isBlocked, .isApproved), // 'isApproved' for message requests
|
||||
Profile.select(.name, .nickname, .profilePictureFileName),
|
||||
ClosedGroup.select(.name),
|
||||
|
@ -48,7 +69,8 @@ public class HomeViewModel {
|
|||
RecipientState.select(.state),
|
||||
ThreadTypingIndicator.select(.threadId)
|
||||
],
|
||||
fetch: { db -> [ArraySection<Section, SessionThreadViewModel>] in
|
||||
fetch: { db -> State in
|
||||
let hasViewedSeed: Bool = db[.hasViewedSeed]
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let unreadMessageRequestCount: Int = try SessionThread
|
||||
.unreadMessageRequestsCountQuery(userPublicKey: userPublicKey)
|
||||
|
@ -59,31 +81,34 @@ public class HomeViewModel {
|
|||
.homeQuery(userPublicKey: userPublicKey)
|
||||
.fetchAll(db)
|
||||
|
||||
return [
|
||||
ArraySection(
|
||||
model: .messageRequests,
|
||||
elements: [
|
||||
// If there are no unread message requests then hide the message request banner
|
||||
(finalUnreadMessageRequestCount == 0 ?
|
||||
nil :
|
||||
SessionThreadViewModel(
|
||||
unreadCount: UInt(finalUnreadMessageRequestCount)
|
||||
return State(
|
||||
showViewedSeedBanner: !hasViewedSeed,
|
||||
sections: [
|
||||
ArraySection(
|
||||
model: .messageRequests,
|
||||
elements: [
|
||||
// If there are no unread message requests then hide the message request banner
|
||||
(finalUnreadMessageRequestCount == 0 ?
|
||||
nil :
|
||||
SessionThreadViewModel(
|
||||
unreadCount: UInt(finalUnreadMessageRequestCount)
|
||||
)
|
||||
)
|
||||
)
|
||||
].compactMap { $0 }
|
||||
),
|
||||
ArraySection(
|
||||
model: .threads,
|
||||
elements: threads
|
||||
)
|
||||
]
|
||||
].compactMap { $0 }
|
||||
),
|
||||
ArraySection(
|
||||
model: .threads,
|
||||
elements: threads
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
.removeDuplicates()
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func updateData(_ updatedData: [ArraySection<Section, SessionThreadViewModel>]) {
|
||||
self.viewData = updatedData
|
||||
public func updateState(_ updatedState: State) {
|
||||
self.state = updatedState
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
|
||||
guard Date().timeIntervalSince1970 < (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||
|
||||
let isMessageRequest = thread.isMessageRequest(db)
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db)
|
||||
|
||||
// If the thread is a message request and the user hasn't hidden message requests then we need
|
||||
// to check if this is the only message request thread (group threads can't be message requests
|
||||
|
@ -160,8 +161,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// notification regardless of how many message requests there are)
|
||||
if thread.variant == .contact {
|
||||
if isMessageRequest && !db[.hasHiddenMessageRequests] {
|
||||
let numMessageRequestThreads: Int? = try? SessionThread.messageRequestThreads(db)
|
||||
.fetchCount(db)
|
||||
let numMessageRequestThreads: Int? = (try? SessionThread
|
||||
.messageRequestsCountQuery(userPublicKey: userPublicKey)
|
||||
.fetchOne(db))
|
||||
.defaulting(to: 0)
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard (numMessageRequestThreads ?? 0) == 0 else { return }
|
||||
|
@ -244,7 +247,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationBody = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
||||
}
|
||||
|
||||
assert((notificationBody ?? notificationTitle) != nil)
|
||||
guard notificationBody != nil || notificationTitle != nil else {
|
||||
SNLog("AppNotifications error: No notification content")
|
||||
return
|
||||
}
|
||||
|
||||
// Don't reply from lockscreen if anyone in this conversation is
|
||||
// "no longer verified".
|
||||
|
|
|
@ -28,7 +28,7 @@ enum Onboarding {
|
|||
|
||||
switch self {
|
||||
case .register:
|
||||
userDefaults[.hasViewedSeed] = false
|
||||
GRDBStorage.shared.write { db in db[.hasViewedSeed] = false }
|
||||
// Set hasSyncedInitialConfiguration to true so that when we hit the
|
||||
// home screen a configuration sync is triggered (yes, the logic is a
|
||||
// bit weird). This is needed so that if the user registers and
|
||||
|
@ -37,7 +37,7 @@ enum Onboarding {
|
|||
|
||||
case .recover, .link:
|
||||
// No need to show it again if the user is restoring or linking
|
||||
userDefaults[.hasViewedSeed] = true
|
||||
GRDBStorage.shared.write { db in db[.hasViewedSeed] = true }
|
||||
userDefaults[.hasSyncedInitialConfiguration] = false
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
final class SeedReminderView : UIView {
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class SeedReminderView: UIView {
|
||||
private let hasContinueButton: Bool
|
||||
var title = NSAttributedString(string: "") { didSet { titleLabel.attributedText = title } }
|
||||
var subtitle = "" { didSet { subtitleLabel.text = subtitle } }
|
||||
|
|
|
@ -170,8 +170,8 @@ final class SeedVC: BaseVC {
|
|||
self.seedReminderView.subtitle = NSLocalizedString("view_seed_reminder_subtitle_3", comment: "")
|
||||
}, completion: nil)
|
||||
seedReminderView.setProgress(1, animated: true)
|
||||
UserDefaults.standard[.hasViewedSeed] = true
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
|
||||
GRDBStorage.shared.write { db in db[.hasViewedSeed] = true }
|
||||
}
|
||||
|
||||
@objc private func copyMnemonic() {
|
||||
|
|
|
@ -71,9 +71,9 @@ final class SeedModal: Modal {
|
|||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
|
||||
// Mark seed as viewed
|
||||
UserDefaults.standard[.hasViewedSeed] = true
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
GRDBStorage.shared.write { db in db[.hasViewedSeed] = true }
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
|
|
@ -75,6 +75,7 @@ public enum SMKLegacy {
|
|||
internal static let soundsGlobalNotificationKey = "kOWSSoundsStorageGlobalNotificationKey"
|
||||
|
||||
internal static let userDefaultsHasHiddenMessageRequests = "hasHiddenMessageRequests"
|
||||
internal static let userDefaultsHasViewedSeedKey = "hasViewedSeed"
|
||||
|
||||
// MARK: - DatabaseMigration
|
||||
|
||||
|
|
|
@ -335,8 +335,10 @@ enum _001_InitialSetupMigration: Migration {
|
|||
try db.create(table: ThreadTypingIndicator.self) { t in
|
||||
t.column(.threadId, .text)
|
||||
.primaryKey()
|
||||
.references(SessionThread.self, onDelete: .cascade) // Delete if thread deleted
|
||||
.references(SessionThread.self, onDelete: .cascade) // Delete if thread deleted
|
||||
t.column(.timestampMs, .integer).notNull()
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,5 +51,7 @@ enum _002_SetupStandardJobs: Migration {
|
|||
behaviour: .recurringOnLaunch
|
||||
).inserted(db)
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1295,9 +1295,14 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
db[.areLinkPreviewsEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyAreLinkPreviewsEnabled] as? Bool == true)
|
||||
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
|
||||
.bool(forKey: SMKLegacy.userDefaultsHasHiddenMessageRequests)
|
||||
|
||||
// Note: The 'hasViewedSeed' was originally stored on standard user defaults
|
||||
db[.hasViewedSeed] = UserDefaults.standard.bool(forKey: SMKLegacy.userDefaultsHasViewedSeedKey)
|
||||
db[.hasSavedThread] = (legacyPreferences[SMKLegacy.preferencesKeyHasSavedThreadKey] as? Bool == true)
|
||||
db[.hasSentAMessage] = (legacyPreferences[SMKLegacy.preferencesKeyHasSentAMessageKey] as? Bool == true)
|
||||
db[.isReadyForAppExtensions] = CurrentAppContext().appUserDefaults().bool(forKey: SMKLegacy.preferencesKeyIsReadyForAppExtensions)
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
|
|
@ -27,12 +27,13 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
/// Whenever using this `linkPreview` association make sure to filter the result using
|
||||
/// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
|
||||
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
|
||||
public static let linkPreviewFilterLiteral: SQL = {
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
public static func linkPreviewFilterLiteral(
|
||||
timestampColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
|
||||
) -> SQL {
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
return "(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
|
||||
}()
|
||||
return "(ROUND((\(Interaction.self).\(timestampColumn) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
|
||||
}
|
||||
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
|
@ -354,7 +355,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
.aliased(interactionAlias)
|
||||
.joining(
|
||||
required: Interaction.linkPreview
|
||||
.filter(literal: Interaction.linkPreviewFilterLiteral)
|
||||
.filter(literal: Interaction.linkPreviewFilterLiteral())
|
||||
)
|
||||
.fetchCount(db)
|
||||
let tmp = try linkPreview.fetchAll(db)
|
||||
|
|
|
@ -90,14 +90,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
|
|||
}
|
||||
}
|
||||
|
||||
// Since it's possible this profile is currently being displayed, send notifications
|
||||
// indicating that it has been updated
|
||||
NotificationCenter.default.post(name: .profileUpdated, object: id)
|
||||
|
||||
if id == getUserHexEncodedPublicKey(db) {
|
||||
NotificationCenter.default.post(name: .localProfileDidChange, object: nil)
|
||||
}
|
||||
else {
|
||||
// FIXME: Remove this once the OWSConversationSettingsViewController has been refactored and is observing DB changes
|
||||
if id != getUserHexEncodedPublicKey(db) {
|
||||
let userInfo = [ Notification.Key.profileRecipientId.rawValue: id ]
|
||||
NotificationCenter.default.post(name: .otherUsersProfileDidChange, object: nil, userInfo: userInfo)
|
||||
}
|
||||
|
|
|
@ -22,20 +22,34 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
|
|||
}
|
||||
|
||||
public enum State: Int, Codable, Hashable, DatabaseValueConvertible {
|
||||
case failed
|
||||
/// These cases **MUST** remain in this order (even though having `failed` as `0` would be more logical) as the order
|
||||
/// is optimised for the desired "interactionState" grouping behaviour we want which makes the query to retrieve the interaction
|
||||
/// state run ~16 times than the alternate approach which required a sub-query (check git history to see the old approach at the
|
||||
/// bottom of this file if desired)
|
||||
///
|
||||
/// The expected behaviour of the grouped "interactionState" that both the `SessionThreadViewModel` and
|
||||
/// `MessageViewModel` should use is `IFNULL(MIN("recipientState"."state"), 'sending')` (joining on the
|
||||
/// `interaction.id` and `state != 'skipped'`):
|
||||
/// - The 'skipped' state should be ignored entirely
|
||||
/// - If there is no state (ie. interaction recipient records not yet created) then the interaction state should be 'sending'
|
||||
/// - If there is a single 'sending' state then the interaction state should be 'sending'
|
||||
/// - If there is a single 'failed' state and no 'sending' state then the interaction state should be 'failed'
|
||||
/// - If there are neither 'sending' or 'failed' states then the interaction state should be 'sent'
|
||||
case sending
|
||||
case failed
|
||||
case skipped
|
||||
case sent
|
||||
|
||||
func message(hasAttachments: Bool, hasAtLeastOneReadReceipt: Bool) -> String {
|
||||
switch self {
|
||||
case .failed: return "MESSAGE_STATUS_FAILED".localized()
|
||||
case .sending:
|
||||
guard hasAttachments else {
|
||||
return "MESSAGE_STATUS_SENDING".localized()
|
||||
}
|
||||
|
||||
return "MESSAGE_STATUS_UPLOADING".localized()
|
||||
|
||||
case .failed: return "MESSAGE_STATUS_FAILED".localized()
|
||||
|
||||
case .sent:
|
||||
guard hasAtLeastOneReadReceipt else {
|
||||
|
@ -117,28 +131,3 @@ public extension RecipientState {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GRDB Queries
|
||||
|
||||
public extension RecipientState {
|
||||
static func selectInteractionState(tableLiteral: SQL, idColumnLiteral: SQL) -> SQL {
|
||||
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
|
||||
|
||||
return """
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
\(recipientState[.interactionId]),
|
||||
\(recipientState[.state]),
|
||||
\(recipientState[.mostRecentFailureText])
|
||||
FROM \(RecipientState.self)
|
||||
WHERE \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) -- Ignore 'skipped'
|
||||
ORDER BY
|
||||
-- If there is a single 'sending' then should be 'sending', otherwise if there is a single
|
||||
-- 'failed' and there is no 'sending' then it should be 'failed'
|
||||
\(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)")) DESC,
|
||||
\(SQL("\(recipientState[.state]) = \(RecipientState.State.failed)")) DESC
|
||||
) AS \(tableLiteral)
|
||||
GROUP BY \(tableLiteral).\(idColumnLiteral)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,17 +185,6 @@ public extension SessionThread {
|
|||
return existingThread
|
||||
}
|
||||
|
||||
static func messageRequestThreads(_ db: Database) -> QueryInterfaceRequest<SessionThread> {
|
||||
return SessionThread
|
||||
.filter(Columns.shouldBeVisible == true)
|
||||
.filter(Columns.variant == Variant.contact)
|
||||
.filter(Columns.id != getUserHexEncodedPublicKey(db))
|
||||
.joining(
|
||||
optional: contact
|
||||
.filter(Contact.Columns.isApproved == false)
|
||||
)
|
||||
}
|
||||
|
||||
func isMessageRequest(_ db: Database) -> Bool {
|
||||
return (
|
||||
shouldBeVisible &&
|
||||
|
@ -209,23 +198,38 @@ public extension SessionThread {
|
|||
// MARK: - Convenience
|
||||
|
||||
public extension SessionThread {
|
||||
static func messageRequestsCountQuery(userPublicKey: String) -> SQLRequest<Int> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
|
||||
return """
|
||||
SELECT COUNT(\(thread[.id]))
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
WHERE (
|
||||
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey))
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
static func unreadMessageRequestsCountQuery(userPublicKey: String) -> SQLRequest<Int> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
let unreadInteractionLiteral: SQL = SQL(stringLiteral: "unreadInteraction")
|
||||
let interactionThreadIdColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
|
||||
|
||||
return """
|
||||
SELECT COUNT(\(thread[.id]))
|
||||
FROM \(SessionThread.self)
|
||||
JOIN (
|
||||
SELECT \(interaction[.threadId])
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
MIN(\(interaction[.wasRead])) AS \(SQL(stringLiteral: "\(Interaction.Columns.wasRead.name)"))
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(interaction[.wasRead]) = false
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(unreadInteractionLiteral) ON \(unreadInteractionLiteral).\(interactionThreadIdColumnLiteral) = \(thread[.id])
|
||||
) AS \(Interaction.self) ON (
|
||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||
\(interaction[.wasRead]) = false
|
||||
)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
WHERE (
|
||||
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey))
|
||||
|
@ -244,32 +248,13 @@ public extension SessionThread {
|
|||
return SQL(
|
||||
"""
|
||||
\(thread[.shouldBeVisible]) = true AND
|
||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND (
|
||||
/* Note: A '!= true' check doesn't work properly so we need to be explicit */
|
||||
\(contact[.isApproved]) IS NULL OR
|
||||
\(contact[.isApproved]) = false
|
||||
)
|
||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND
|
||||
IFNULL(\(contact[.isApproved]), false) = false
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
/// This method can be used to filter a thread query to exclude messages requests
|
||||
///
|
||||
/// **Note:** In order to use this filter you **MUST** have a `joining(required/optional:)` to the
|
||||
/// `SessionThread.contact` association or it won't work
|
||||
static func isNotMessageRequest(userPublicKey: String) -> SQLSpecificExpressible {
|
||||
let contactAlias: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
|
||||
return (
|
||||
SessionThread.Columns.shouldBeVisible == true && (
|
||||
SessionThread.Columns.variant != SessionThread.Variant.contact ||
|
||||
SessionThread.Columns.id == userPublicKey || // Note to self
|
||||
contactAlias[.isApproved] == true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func isNoteToSelf(_ db: Database? = nil) -> Bool {
|
||||
return (
|
||||
variant == .contact &&
|
||||
|
|
|
@ -4,15 +4,11 @@ import SessionUtilitiesKit
|
|||
|
||||
public extension Notification.Name {
|
||||
|
||||
static let profileUpdated = Notification.Name("profileUpdated")
|
||||
static let localProfileDidChange = Notification.Name("localProfileDidChange")
|
||||
static let otherUsersProfileDidChange = Notification.Name("otherUsersProfileDidChange")
|
||||
}
|
||||
|
||||
@objc public extension NSNotification {
|
||||
|
||||
@objc static let profileUpdated = Notification.Name.profileUpdated.rawValue as NSString
|
||||
@objc static let localProfileDidChange = Notification.Name.localProfileDidChange.rawValue as NSString
|
||||
@objc static let otherUsersProfileDidChange = Notification.Name.otherUsersProfileDidChange.rawValue as NSString
|
||||
}
|
||||
|
||||
|
|
|
@ -75,8 +75,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
public let state: RecipientState.State
|
||||
public let hasAtLeastOneReadReceipt: Bool
|
||||
public let mostRecentFailureText: String?
|
||||
public let isTypingIndicator: Bool
|
||||
public let isSenderOpenGroupModerator: Bool
|
||||
public let isTypingIndicator: Bool?
|
||||
public let profile: Profile?
|
||||
public let quote: Quote?
|
||||
public let quoteAttachment: Attachment?
|
||||
|
@ -169,7 +169,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
isLast: Bool
|
||||
) -> MessageViewModel {
|
||||
let cellType: CellType = {
|
||||
guard !self.isTypingIndicator else { return .typingIndicator }
|
||||
guard self.isTypingIndicator != true else { return .typingIndicator }
|
||||
guard self.variant != .standardIncomingDeleted else { return .textOnlyMessage }
|
||||
guard let attachment: Attachment = self.attachments?.first else { return .textOnlyMessage }
|
||||
|
||||
|
@ -208,7 +208,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
nickname: nil // Folded into 'authorName' within the Query
|
||||
)
|
||||
let shouldShowDateOnThisModel: Bool = {
|
||||
guard !self.isTypingIndicator else { return false }
|
||||
guard self.isTypingIndicator != true else { return false }
|
||||
guard let prevModel: ViewModel = prevModel else { return true }
|
||||
|
||||
return MessageViewModel.shouldShowDateBreak(
|
||||
|
@ -218,7 +218,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
}()
|
||||
let shouldShowDateOnNextModel: Bool = {
|
||||
// Should be nothing after a typing indicator
|
||||
guard !self.isTypingIndicator else { return false }
|
||||
guard self.isTypingIndicator != true else { return false }
|
||||
guard let nextModel: ViewModel = nextModel else { return false }
|
||||
|
||||
return MessageViewModel.shouldShowDateBreak(
|
||||
|
@ -404,18 +404,21 @@ public extension MessageViewModel {
|
|||
// MARK: - Convenience Initialization
|
||||
|
||||
public extension MessageViewModel {
|
||||
public static let genericId: Int64 = -2
|
||||
public static let typingIndicatorId: Int64 = -2
|
||||
static let genericId: Int64 = -2
|
||||
static let typingIndicatorId: Int64 = -2
|
||||
|
||||
// Note: This init method is only used system-created cells or empty states
|
||||
init(isTypingIndicator: Bool = false) {
|
||||
init(isTypingIndicator: Bool? = nil) {
|
||||
self.threadVariant = .contact
|
||||
self.threadIsTrusted = false
|
||||
self.threadHasDisappearingMessagesEnabled = false
|
||||
|
||||
// Interaction Info
|
||||
|
||||
let targetId: Int64 = (isTypingIndicator ? MessageViewModel.typingIndicatorId : MessageViewModel.genericId)
|
||||
let targetId: Int64 = (isTypingIndicator == true ?
|
||||
MessageViewModel.typingIndicatorId :
|
||||
MessageViewModel.genericId
|
||||
)
|
||||
self.rowId = targetId
|
||||
self.id = targetId
|
||||
self.variant = .standardOutgoing
|
||||
|
@ -513,7 +516,7 @@ public extension MessageViewModel {
|
|||
return { additionalFilters, limitSQL -> AdaptedFetchRequest<SQLRequest<ViewModel>> in
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let typingIndicator: TypedTableAlias<ThreadTypingIndicator> = TypedTableAlias()
|
||||
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
|
||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||
|
@ -522,7 +525,6 @@ public extension MessageViewModel {
|
|||
|
||||
let interactionStateTableLiteral: SQL = SQL(stringLiteral: "interactionState")
|
||||
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
|
||||
let interactionStateStateColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.state.name)
|
||||
let interactionStateMostRecentFailureTextColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.mostRecentFailureText.name)
|
||||
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
|
||||
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
|
||||
|
@ -543,7 +545,7 @@ public extension MessageViewModel {
|
|||
"""
|
||||
}()
|
||||
let finalLimitSQL: SQL = (limitSQL ?? SQL(stringLiteral: ""))
|
||||
let numColumnsBeforeLinkedRecords: Int = 17
|
||||
let numColumnsBeforeLinkedRecords: Int = 16
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
|
||||
|
@ -563,11 +565,10 @@ public extension MessageViewModel {
|
|||
\(interaction[.expiresInSeconds]),
|
||||
|
||||
-- Default to 'sending' assuming non-processed interaction when null
|
||||
IFNULL(\(interactionStateTableLiteral).\(interactionStateStateColumnLiteral), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey),
|
||||
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey),
|
||||
(\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey),
|
||||
\(interactionStateTableLiteral).\(interactionStateMostRecentFailureTextColumnLiteral) AS \(ViewModel.mostRecentFailureTextKey),
|
||||
|
||||
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.isTypingIndicatorKey),
|
||||
\(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey),
|
||||
|
||||
false AS \(ViewModel.isSenderOpenGroupModeratorKey),
|
||||
|
||||
\(ViewModel.profileKey).*,
|
||||
|
@ -587,7 +588,6 @@ public extension MessageViewModel {
|
|||
|
||||
FROM \(Interaction.self)
|
||||
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
|
||||
|
@ -595,20 +595,20 @@ public extension MessageViewModel {
|
|||
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId])
|
||||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
\(Interaction.linkPreviewFilterLiteral)
|
||||
\(Interaction.linkPreviewFilterLiteral())
|
||||
)
|
||||
LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumnLiteral) = \(linkPreview[.attachmentId])
|
||||
LEFT JOIN (
|
||||
\(RecipientState.selectInteractionState(
|
||||
tableLiteral: interactionStateTableLiteral,
|
||||
idColumnLiteral: interactionStateInteractionIdColumnLiteral
|
||||
))
|
||||
) AS \(interactionStateTableLiteral) ON \(interactionStateTableLiteral).\(interactionStateInteractionIdColumnLiteral) = \(interaction[.id])
|
||||
LEFT JOIN \(RecipientState.self) ON (
|
||||
-- Ignore 'skipped' states
|
||||
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
|
||||
\(recipientState[.interactionId]) = \(interaction[.id])
|
||||
)
|
||||
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
|
||||
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
|
||||
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
|
||||
)
|
||||
\(finalFilterSQL)
|
||||
GROUP BY \(interaction[.id])
|
||||
ORDER BY \(orderSQL)
|
||||
\(finalLimitSQL)
|
||||
"""
|
||||
|
|
|
@ -47,6 +47,7 @@ public struct SessionThreadViewModel: FetchableRecord, Decodable, Equatable, Has
|
|||
public static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue)
|
||||
public static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue)
|
||||
public static let interactionBodyKey: SQL = SQL(stringLiteral: CodingKeys.interactionBody.stringValue)
|
||||
public static let interactionStateKey: SQL = SQL(stringLiteral: CodingKeys.interactionState.stringValue)
|
||||
public static let interactionIsOpenGroupInvitationKey: SQL = SQL(stringLiteral: CodingKeys.interactionIsOpenGroupInvitation.stringValue)
|
||||
public static let interactionAttachmentDescriptionInfoKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentDescriptionInfo.stringValue)
|
||||
public static let interactionAttachmentCountKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentCount.stringValue)
|
||||
|
@ -262,27 +263,17 @@ public extension SessionThreadViewModel {
|
|||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
|
||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||
|
||||
let unreadCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadCountString)_table")
|
||||
let unreadMentionCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadMentionCountString)_table")
|
||||
let interactionThreadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
|
||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
||||
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
||||
let authorProfileLiteral: SQL = SQL(stringLiteral: "authorProfile")
|
||||
let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
|
||||
let attachmentVariantColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.variant.name)
|
||||
let attachmentContentTypeColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.contentType.name)
|
||||
let attachmentSourceFilenameColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.sourceFilename.name)
|
||||
let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment")
|
||||
let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
|
||||
let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
|
||||
let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
|
||||
let interactionStateTableLiteral: SQL = SQL(stringLiteral: "interactionState")
|
||||
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
|
||||
let interactionStateStateColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.state.name)
|
||||
|
||||
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
|
||||
/// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to
|
||||
|
@ -291,7 +282,6 @@ public extension SessionThreadViewModel {
|
|||
/// Explicitly set default values for the fields ignored for search results
|
||||
let numColumnsBeforeProfiles: Int = 11
|
||||
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 10 // The attachment info columns will be combined
|
||||
// TODO: Some testing around the subqueries in the joins to see if they impact performance ('Simulator1' device takes ~125ms to complete this query)
|
||||
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
|
@ -306,8 +296,8 @@ public extension SessionThreadViewModel {
|
|||
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
|
||||
|
||||
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey),
|
||||
\(unreadCountTableLiteral).\(ViewModel.threadUnreadCountKey) AS \(ViewModel.threadUnreadCountKey),
|
||||
\(unreadMentionCountTableLiteral).\(ViewModel.threadUnreadMentionCountKey) AS \(ViewModel.threadUnreadMentionCountKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadMentionCountKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
\(ViewModel.closedGroupProfileFrontKey).*,
|
||||
|
@ -318,59 +308,75 @@ public extension SessionThreadViewModel {
|
|||
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
|
||||
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
|
||||
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||
\(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey),
|
||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||
\(Interaction.self).\(ViewModel.interactionVariantKey),
|
||||
\(Interaction.self).\(ViewModel.interactionTimestampMsKey),
|
||||
\(Interaction.self).\(ViewModel.interactionBodyKey),
|
||||
|
||||
-- Default to 'sending' assuming non-processed interaction when null
|
||||
IFNULL(\(interactionStateTableLiteral).\(interactionStateStateColumnLiteral), \(SQL("\(RecipientState.State.sending)"))) AS \(interactionStateTableLiteral),
|
||||
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey),
|
||||
(\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey),
|
||||
\(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral),
|
||||
\(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentVariantColumnLiteral),
|
||||
\(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentContentTypeColumnLiteral),
|
||||
\(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentSourceFilenameColumnLiteral),
|
||||
|
||||
-- These 4 properties will be combined into 'Attachment.DescriptionInfo'
|
||||
\(attachment[.id]),
|
||||
\(attachment[.variant]),
|
||||
\(attachment[.contentType]),
|
||||
\(attachment[.sourceFilename]),
|
||||
COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey),
|
||||
|
||||
\(interaction[.authorId]),
|
||||
IFNULL(\(authorProfileLiteral).\(profileNicknameColumnLiteral), \(authorProfileLiteral).\(profileNameColumnLiteral)) AS \(ViewModel.authorNameInternalKey),
|
||||
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey),
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT *, MAX(\(interaction[.timestampMs]))
|
||||
-- Fetch all interaction-specific data in a subquery to be more efficient
|
||||
SELECT
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.threadId]),
|
||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey),
|
||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||
\(interaction[.authorId]),
|
||||
\(interaction[.linkPreviewUrl]),
|
||||
|
||||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey),
|
||||
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
|
||||
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
COUNT(*) AS \(ViewModel.threadUnreadCountKey)
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(interaction[.wasRead]) = false
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(unreadCountTableLiteral) ON \(unreadCountTableLiteral).\(interactionThreadIdLiteral) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
COUNT(*) AS \(ViewModel.threadUnreadMentionCountKey)
|
||||
FROM \(Interaction.self)
|
||||
WHERE (
|
||||
\(interaction[.wasRead]) = false AND
|
||||
\(interaction[.hasMention]) = true
|
||||
)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(unreadMentionCountTableLiteral) ON \(unreadMentionCountTableLiteral).\(interactionThreadIdLiteral) = \(thread[.id])
|
||||
|
||||
LEFT JOIN \(RecipientState.self) ON (
|
||||
-- Ignore 'skipped' states
|
||||
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
|
||||
\(recipientState[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
|
||||
)
|
||||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
||||
\(Interaction.linkPreviewFilterLiteral(timestampColumn: ViewModel.interactionTimestampMsKey))
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(Interaction.self).\(ViewModel.interactionIdKey)
|
||||
)
|
||||
LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
|
||||
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
|
||||
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
|
||||
|
||||
-- Thread naming & avatar content
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
|
||||
)
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
|
@ -400,25 +406,6 @@ public extension SessionThreadViewModel {
|
|||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)"))
|
||||
)
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(authorProfileLiteral) ON \(authorProfileLiteral).\(profileIdColumnLiteral) = \(interaction[.authorId])
|
||||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
||||
\(Interaction.linkPreviewFilterLiteral)
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id])
|
||||
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id])
|
||||
)
|
||||
LEFT JOIN \(Attachment.self) AS \(ViewModel.interactionAttachmentDescriptionInfoKey) ON \(ViewModel.interactionAttachmentDescriptionInfoKey).\(attachmentIdColumnLiteral) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
|
||||
LEFT JOIN (
|
||||
\(RecipientState.selectInteractionState(
|
||||
tableLiteral: interactionStateTableLiteral,
|
||||
idColumnLiteral: interactionStateInteractionIdColumnLiteral
|
||||
))
|
||||
) AS \(interactionStateTableLiteral) ON \(interactionStateTableLiteral).\(interactionStateInteractionIdColumnLiteral) = \(interaction[.id])
|
||||
|
||||
WHERE (
|
||||
\(filters)
|
||||
|
@ -452,7 +439,6 @@ public extension SessionThreadViewModel {
|
|||
static func homeQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
return baseQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
|
@ -465,11 +451,11 @@ public extension SessionThreadViewModel {
|
|||
) AND (
|
||||
-- Only show the 'Note to Self' thread if it has an interaction
|
||||
\(SQL("\(thread[.id]) != \(userPublicKey)")) OR
|
||||
\(interaction[.id]) IS NOT NULL
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey) IS NOT NULL
|
||||
)
|
||||
""",
|
||||
ordering: """
|
||||
\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), \(thread[.creationDateTimestamp])) DESC
|
||||
\(thread[.isPinned]) DESC, IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), \(thread[.creationDateTimestamp])) DESC
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
@ -477,7 +463,6 @@ public extension SessionThreadViewModel {
|
|||
static func messageRequestsQuery(userPublicKey: String) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
return baseQuery(
|
||||
userPublicKey: userPublicKey,
|
||||
|
@ -493,7 +478,7 @@ public extension SessionThreadViewModel {
|
|||
)
|
||||
""",
|
||||
ordering: """
|
||||
IFNULL(\(interaction[.timestampMs]), \(thread[.creationDateTimestamp])) DESC
|
||||
IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), \(thread[.creationDateTimestamp])) DESC
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
@ -510,8 +495,6 @@ public extension SessionThreadViewModel {
|
|||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
|
||||
let unreadCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadCountString)_table")
|
||||
let unreadMentionCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadUnreadMentionCountString)_table")
|
||||
let firstUnreadInteractionTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.threadFirstUnreadInteractionIdString)_table")
|
||||
let interactionIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.id.name)
|
||||
let interactionThreadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
|
||||
|
@ -555,8 +538,8 @@ public extension SessionThreadViewModel {
|
|||
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
|
||||
\(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey),
|
||||
|
||||
\(unreadCountTableLiteral).\(ViewModel.threadUnreadCountKey) AS \(ViewModel.threadUnreadCountKey),
|
||||
\(unreadMentionCountTableLiteral).\(ViewModel.threadUnreadMentionCountKey) AS \(ViewModel.threadUnreadMentionCountKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadMentionCountKey),
|
||||
\(firstUnreadInteractionTableLiteral).\(interactionIdLiteral) AS \(ViewModel.threadFirstUnreadInteractionIdKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
|
@ -569,12 +552,25 @@ public extension SessionThreadViewModel {
|
|||
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
|
||||
\(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey),
|
||||
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
-- Fetch all interaction-specific data in a subquery to be more efficient
|
||||
SELECT
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.threadId]),
|
||||
MAX(\(interaction[.timestampMs])),
|
||||
|
||||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey),
|
||||
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
|
||||
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.id]),
|
||||
|
@ -586,36 +582,9 @@ public extension SessionThreadViewModel {
|
|||
\(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||
)
|
||||
) AS \(firstUnreadInteractionTableLiteral) ON \(firstUnreadInteractionTableLiteral).\(interactionThreadIdLiteral) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT *, MAX(\(interaction[.timestampMs]))
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
COUNT(*) AS \(ViewModel.threadUnreadCountKey)
|
||||
FROM \(Interaction.self)
|
||||
WHERE (
|
||||
\(interaction[.wasRead]) = false AND
|
||||
\(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||
)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(unreadCountTableLiteral) ON \(unreadCountTableLiteral).\(interactionThreadIdLiteral) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
COUNT(*) AS \(ViewModel.threadUnreadMentionCountKey)
|
||||
FROM \(Interaction.self)
|
||||
WHERE (
|
||||
\(interaction[.wasRead]) = false AND
|
||||
\(interaction[.hasMention]) = true AND
|
||||
\(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||
)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(unreadMentionCountTableLiteral) ON \(unreadMentionCountTableLiteral).\(interactionThreadIdLiteral) = \(thread[.id])
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
|
||||
LEFT JOIN \(GroupMember.self) ON (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
|
@ -633,7 +602,6 @@ public extension SessionThreadViewModel {
|
|||
)
|
||||
GROUP BY \(groupMember[.groupId])
|
||||
) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)"))
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
|
||||
|
||||
WHERE \(SQL("\(thread[.id]) = \(threadId)"))
|
||||
GROUP BY \(thread[.id])
|
||||
|
|
|
@ -44,6 +44,9 @@ public extension Setting.BoolKey {
|
|||
/// Controls whether the notification sound should play while the app is in the foreground
|
||||
static let playNotificationSoundInForeground: Setting.BoolKey = "playNotificationSoundInForeground"
|
||||
|
||||
/// A flag indicating whether the user has ever viewed their seed
|
||||
static let hasViewedSeed: Setting.BoolKey = "hasViewedSeed"
|
||||
|
||||
/// A flag indicating whether the user has ever saved a thread
|
||||
static let hasSavedThread: Setting.BoolKey = "hasSavedThread"
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
|
||||
guard Date().timeIntervalSince1970 < (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||
|
||||
let isMessageRequest = thread.isMessageRequest(db)
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db)
|
||||
|
||||
// If the thread is a message request and the user hasn't hidden message requests then we need
|
||||
// to check if this is the only message request thread (group threads can't be message requests
|
||||
|
@ -19,8 +20,10 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// notification regardless of how many message requests there are)
|
||||
if thread.variant == .contact {
|
||||
if isMessageRequest && !db[.hasHiddenMessageRequests] {
|
||||
let numMessageRequestThreads: Int? = try? SessionThread.messageRequestThreads(db)
|
||||
.fetchCount(db)
|
||||
let numMessageRequestThreads: Int? = (try? SessionThread
|
||||
.messageRequestsCountQuery(userPublicKey: userPublicKey)
|
||||
.fetchOne(db))
|
||||
.defaulting(to: 0)
|
||||
|
||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||
guard (numMessageRequestThreads ?? 0) == 0 else { return }
|
||||
|
@ -34,7 +37,6 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
}
|
||||
|
||||
let senderPublicKey: String = interaction.authorId
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
|
||||
guard senderPublicKey != userPublicKey else {
|
||||
// Ignore PNs for messages sent by the current user
|
||||
|
|
|
@ -51,5 +51,7 @@ enum _001_InitialSetupMigration: Migration {
|
|||
|
||||
t.uniqueKey([.key, .hash])
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,5 +20,7 @@ enum _002_SetupStandardJobs: Migration {
|
|||
shouldBlockFirstRunEachSession: true
|
||||
).inserted(db)
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,5 +202,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
).inserted(db)
|
||||
}
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,5 +64,7 @@ enum _001_InitialSetupMigration: Migration {
|
|||
.primaryKey()
|
||||
t.column(.value, .blob).notNull()
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,5 +20,7 @@ enum _002_SetupStandardJobs: Migration {
|
|||
behaviour: .recurringOnLaunch
|
||||
).inserted(db)
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,5 +112,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
data: userX25519KeyPair.publicKey
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
GRDBStorage.shared.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ public enum SNUserDefaults {
|
|||
|
||||
public enum Bool : Swift.String {
|
||||
case hasSyncedInitialConfiguration = "hasSyncedConfiguration"
|
||||
case hasViewedSeed
|
||||
case hasSeenLinkPreviewSuggestion
|
||||
case isUsingFullAPNs
|
||||
case wasUnlinked
|
||||
|
|
|
@ -3,12 +3,9 @@ import Foundation
|
|||
public extension Notification.Name {
|
||||
|
||||
// State changes
|
||||
static let blockedContactsUpdated = Notification.Name("blockedContactsUpdated")
|
||||
static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged")
|
||||
static let threadDeleted = Notification.Name("threadDeleted")
|
||||
static let threadSessionRestoreDevicesChanged = Notification.Name("threadSessionRestoreDevicesChanged")
|
||||
// Onboarding
|
||||
static let seedViewed = Notification.Name("seedViewed")
|
||||
// Interaction
|
||||
static let dataNukeRequested = Notification.Name("dataNukeRequested")
|
||||
}
|
||||
|
@ -16,12 +13,9 @@ public extension Notification.Name {
|
|||
@objc public extension NSNotification {
|
||||
|
||||
// State changes
|
||||
@objc static let blockedContactsUpdated = Notification.Name.blockedContactsUpdated.rawValue as NSString
|
||||
@objc static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString
|
||||
@objc static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString
|
||||
@objc static let threadSessionRestoreDevicesChanged = Notification.Name.threadSessionRestoreDevicesChanged.rawValue as NSString
|
||||
// Onboarding
|
||||
@objc static let seedViewed = Notification.Name.seedViewed.rawValue as NSString
|
||||
// Interaction
|
||||
@objc static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue