From 2fa62bd2ca159b91dd86e2698ad9351758724522 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 20 Jan 2021 16:28:36 +1100 Subject: [PATCH] Fix threads not updating on home screen --- Session/Home/HomeVC.swift | 174 +++++++----------- .../MessageReceiver+Handling.swift | 6 + 2 files changed, 73 insertions(+), 107 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index d1c555f3c..c4d46e685 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -1,30 +1,24 @@ -final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate { - private var threadViewModelCache: [String:ThreadViewModel] = [:] - private var isObservingDatabase = true - private var isViewVisible = false { didSet { updateIsObservingDatabase() } } +// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and +// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for +// more information on database handling. + +final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate { + private var threads: YapDatabaseViewMappings! + private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var tableViewTopConstraint: NSLayoutConstraint! - private var wasDatabaseModifiedExternally = false - private var threads: YapDatabaseViewMappings = { - let result = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) - result.setIsReversed(true, forGroup: TSInboxGroup) - return result - }() + private var threadCount: UInt { + threads.numberOfItems(inGroup: TSInboxGroup) + } - private let uiDatabaseConnection: YapDatabaseConnection = { + private lazy var dbConnection: YapDatabaseConnection = { let result = OWSPrimaryStorage.shared().newDatabaseConnection() result.objectCacheLimit = 500 return result }() - private let editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection() - - private var threadCount: UInt { - threads.numberOfItems(inGroup: TSInboxGroup) - } - - // MARK: Components + // MARK: UI Components private lazy var seedReminderView: SeedReminderView = { let result = SeedReminderView(hasContinueButton: true) let title = "You're almost finished! 80%" @@ -36,9 +30,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol result.delegate = self return result }() - - private lazy var searchBar = SearchBar() - + private lazy var tableView: UITableView = { let result = UITableView() result.backgroundColor = .clear @@ -86,23 +78,26 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() + // Threads (part 1) + dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) + // Preparation SignalApp.shared().homeViewController = self + // Gradient & nav bar setUpGradientBackground() if navigationController?.navigationBar != nil { setUpNavBarStyle() } - updateNavigationBarButtons() + updateNavBarButtons() setNavBarTitle("Messages") - // Set up seed reminder view if needed - let userDefaults = UserDefaults.standard - let hasViewedSeed = userDefaults[.hasViewedSeed] + // 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) } - // Set up table view + // Table view tableView.dataSource = self tableView.delegate = self view.addSubview(tableView) @@ -120,52 +115,59 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol fadeView.pin(.top, to: .top, of: view, withInset: topInset) fadeView.pin(.trailing, to: .trailing, of: view) fadeView.pin(.bottom, to: .bottom, of: view) - // Set up empty state view + // Empty state view view.addSubview(emptyStateView) emptyStateView.center(.horizontal, in: view) let verticalCenteringConstraint = emptyStateView.center(.vertical, in: view) verticalCenteringConstraint.constant = -16 // Makes things appear centered visually - // Set up new conversation button set + // New conversation button set view.addSubview(newConversationButtonSet) newConversationButtonSet.center(.horizontal, in: view) newConversationButtonSet.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up - // Set up previewing + // Previewing if traitCollection.forceTouchCapability == .available { registerForPreviewing(with: self, sourceView: tableView) } - // Listen for notifications + // Notifications let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject) - notificationCenter.addObserver(self, selector: #selector(handleApplicationDidBecomeActiveNotification(_:)), name: .OWSApplicationDidBecomeActive, object: nil) - notificationCenter.addObserver(self, selector: #selector(handleApplicationWillResignActiveNotification(_:)), name: .OWSApplicationWillResignActive, object: nil) notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil) notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil) notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil) notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil) - // Set up public chats and RSS feeds if needed + // Threads (part 2) + threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point + threads.setIsReversed(true, forGroup: TSInboxGroup) + dbConnection.read { transaction in + self.threads.update(with: transaction) // Perform the initial update + } + // Pollers if OWSIdentityManager.shared().identityKeyPair() != nil { let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.startPollerIfNeeded() appDelegate.startClosedGroupPollerIfNeeded() appDelegate.startOpenGroupPollersIfNeeded() } - // Populate onion request path countries cache + // Onion request path countries cache DispatchQueue.global(qos: .utility).async { let _ = IP2Country.shared.populateCacheIfNeeded() } - // Do initial update - reload() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - isViewVisible = true + reload() UserDefaults.standard[.hasLaunchedOnce] = true - showKeyPairMigrationNudgeIfNeeded() + showKeyPairMigrationModalIfNeeded() showKeyPairMigrationSuccessModalIfNeeded() } - private func showKeyPairMigrationNudgeIfNeeded() { + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Migration + private func showKeyPairMigrationModalIfNeeded() { guard !KeyPairUtilities.hasV2KeyPair() else { return } let sheet = KeyPairMigrationSheet() sheet.modalPresentationStyle = .overFullScreen @@ -183,16 +185,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol UserDefaults.standard[.isMigratingToV2KeyPair] = false } - override func viewWillDisappear(_ animated: Bool) { - isViewVisible = false - super.viewWillDisappear(animated) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: Data + // MARK: Table View Data Source func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Int(threadCount) } @@ -204,44 +197,29 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol } // MARK: Updating - private func updateIsObservingDatabase() { - isObservingDatabase = isViewVisible && CurrentAppContext().isAppForegroundAndActive() - } - private func reload() { AssertIsOnMainThread() - uiDatabaseConnection.beginLongLivedReadTransaction() - uiDatabaseConnection.read { transaction in + dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + dbConnection.read { transaction in self.threads.update(with: transaction) } threadViewModelCache.removeAll() tableView.reloadData() emptyStateView.isHidden = (threadCount != 0) } - - @objc private func handleYapDatabaseModifiedNotification(_ notification: Notification) { + + @objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) { AssertIsOnMainThread() - let notifications = uiDatabaseConnection.beginLongLivedReadTransaction() - let ext = uiDatabaseConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection + let notifications = dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + guard !notifications.isEmpty else { return } + let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications) - guard isObservingDatabase else { - wasDatabaseModifiedExternally = hasChanges - return - } - guard hasChanges else { - uiDatabaseConnection.read { transaction in - self.threads.update(with: transaction) - } - return - } - // If changes were made in a different process (e.g. the Notification Service Extension) the thread mapping can be out of date - // at this point, causing the app to crash. The code below prevents that by force syncing the database before proceeding. - if notifications.count > 0 { - if let firstChangeSet = notifications[0].userInfo { - let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64 - if threads.snapshotOfLastUpdate != firstSnapshot - 1 { - return reload() - } + guard hasChanges else { return } + guard !notifications.isEmpty else { return } + if let firstChangeSet = notifications[0].userInfo { + let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64 + if threads.snapshotOfLastUpdate != firstSnapshot - 1 { + return reload() // The code below will crash if we try to process multiple commits at once } } var sectionChanges = NSArray() @@ -254,13 +232,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol let key = rowChange.collectionKey.key threadViewModelCache[key] = nil switch rowChange.type { - case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) - case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade) - case .move: - tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) - tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade) - case .update: - tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.none) + case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) + case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic) + case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!) + case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) default: break } } @@ -268,24 +243,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol emptyStateView.isHidden = (threadCount != 0) } - @objc private func handleApplicationDidBecomeActiveNotification(_ notification: Notification) { - updateIsObservingDatabase() - if wasDatabaseModifiedExternally { - reload() - wasDatabaseModifiedExternally = false - } - } - - @objc private func handleApplicationWillResignActiveNotification(_ notification: Notification) { - updateIsObservingDatabase() - } - @objc private func handleProfileDidChangeNotification(_ notification: Notification) { tableView.reloadData() // TODO: Just reload the affected cell } @objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) { - updateNavigationBarButtons() + updateNavBarButtons() } @objc private func handleSeedViewedNotification(_ notification: Notification) { @@ -298,7 +261,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol self.tableView.reloadData() // TODO: Just reload the affected cell } - private func updateNavigationBarButtons() { + private func updateNavBarButtons() { let profilePictureSize = Values.verySmallProfilePictureSize let profilePictureView = ProfilePictureView() profilePictureView.accessibilityLabel = "Settings button" @@ -353,10 +316,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol present(navigationController, animated: true, completion: nil) } - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - searchBar.resignFirstResponder() - } - func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { guard let indexPath = tableView.indexPathForRow(at: location), let thread = self.thread(at: indexPath.row) else { return nil } previewingContext.sourceRect = tableView.rectForRow(at: indexPath) @@ -473,8 +432,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol } @objc func joinOpenGroup() { - let joinPublicChatVC = JoinPublicChatVC() - let navigationController = OWSNavigationController(rootViewController: joinPublicChatVC) + let joinOpenGroupVC = JoinPublicChatVC() + let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC) present(navigationController, animated: true, completion: nil) } @@ -493,8 +452,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol // MARK: Convenience private func thread(at index: Int) -> TSThread? { var thread: TSThread? = nil - uiDatabaseConnection.read { transaction in - thread = ((transaction as YapDatabaseReadTransaction).ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction).object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? + dbConnection.read { transaction in + let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction + thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? } return thread } @@ -505,7 +465,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol return cachedThreadViewModel } else { var threadViewModel: ThreadViewModel? = nil - uiDatabaseConnection.read { transaction in + dbConnection.read { transaction in threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) } threadViewModelCache[thread.uniqueId!] = threadViewModel diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index fd7e6baab..85e6f49da 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -16,6 +16,12 @@ extension MessageReceiver { case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) default: fatalError() } + // Touch the thread to update the home screen preview + let storage = SNMessagingKitConfiguration.shared.storage + guard let threadID = storage.getOrCreateThread(for: message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } + thread.touch(with: transaction) } private static func handleReadReceipt(_ message: ReadReceipt, using transaction: Any) {