Fix threads not updating on home screen

pull/337/head
Niels Andriesse 4 years ago
parent 6dd2d2e72f
commit 2fa62bd2ca

@ -1,30 +1,24 @@
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate { // See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and
private var threadViewModelCache: [String:ThreadViewModel] = [:] // https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
private var isObservingDatabase = true // more information on database handling.
private var isViewVisible = false { didSet { updateIsObservingDatabase() } }
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 tableViewTopConstraint: NSLayoutConstraint!
private var wasDatabaseModifiedExternally = false
private var threads: YapDatabaseViewMappings = { private var threadCount: UInt {
let result = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) threads.numberOfItems(inGroup: TSInboxGroup)
result.setIsReversed(true, forGroup: TSInboxGroup) }
return result
}()
private let uiDatabaseConnection: YapDatabaseConnection = { private lazy var dbConnection: YapDatabaseConnection = {
let result = OWSPrimaryStorage.shared().newDatabaseConnection() let result = OWSPrimaryStorage.shared().newDatabaseConnection()
result.objectCacheLimit = 500 result.objectCacheLimit = 500
return result return result
}() }()
private let editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection() // MARK: UI Components
private var threadCount: UInt {
threads.numberOfItems(inGroup: TSInboxGroup)
}
// MARK: Components
private lazy var seedReminderView: SeedReminderView = { private lazy var seedReminderView: SeedReminderView = {
let result = SeedReminderView(hasContinueButton: true) let result = SeedReminderView(hasContinueButton: true)
let title = "You're almost finished! 80%" let title = "You're almost finished! 80%"
@ -36,9 +30,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
result.delegate = self result.delegate = self
return result return result
}() }()
private lazy var searchBar = SearchBar()
private lazy var tableView: UITableView = { private lazy var tableView: UITableView = {
let result = UITableView() let result = UITableView()
result.backgroundColor = .clear result.backgroundColor = .clear
@ -86,23 +78,26 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
// MARK: Lifecycle // MARK: Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
super.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 SignalApp.shared().homeViewController = self
// Gradient & nav bar
setUpGradientBackground() setUpGradientBackground()
if navigationController?.navigationBar != nil { if navigationController?.navigationBar != nil {
setUpNavBarStyle() setUpNavBarStyle()
} }
updateNavigationBarButtons() updateNavBarButtons()
setNavBarTitle("Messages") setNavBarTitle("Messages")
// Set up seed reminder view if needed // Recovery phrase reminder
let userDefaults = UserDefaults.standard let hasViewedSeed = UserDefaults.standard[.hasViewedSeed]
let hasViewedSeed = userDefaults[.hasViewedSeed]
if !hasViewedSeed { if !hasViewedSeed {
view.addSubview(seedReminderView) view.addSubview(seedReminderView)
seedReminderView.pin(.leading, to: .leading, of: view) seedReminderView.pin(.leading, to: .leading, of: view)
seedReminderView.pin(.top, to: .top, of: view) seedReminderView.pin(.top, to: .top, of: view)
seedReminderView.pin(.trailing, to: .trailing, of: view) seedReminderView.pin(.trailing, to: .trailing, of: view)
} }
// Set up table view // Table view
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
view.addSubview(tableView) 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(.top, to: .top, of: view, withInset: topInset)
fadeView.pin(.trailing, to: .trailing, of: view) fadeView.pin(.trailing, to: .trailing, of: view)
fadeView.pin(.bottom, to: .bottom, of: view) fadeView.pin(.bottom, to: .bottom, of: view)
// Set up empty state view // Empty state view
view.addSubview(emptyStateView) view.addSubview(emptyStateView)
emptyStateView.center(.horizontal, in: view) emptyStateView.center(.horizontal, in: view)
let verticalCenteringConstraint = emptyStateView.center(.vertical, in: view) let verticalCenteringConstraint = emptyStateView.center(.vertical, in: view)
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
// Set up new conversation button set // New conversation button set
view.addSubview(newConversationButtonSet) view.addSubview(newConversationButtonSet)
newConversationButtonSet.center(.horizontal, in: view) newConversationButtonSet.center(.horizontal, in: view)
newConversationButtonSet.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up 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 { if traitCollection.forceTouchCapability == .available {
registerForPreviewing(with: self, sourceView: tableView) registerForPreviewing(with: self, sourceView: tableView)
} }
// Listen for notifications // Notifications
let notificationCenter = NotificationCenter.default let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject) 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(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(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil) notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, 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 { if OWSIdentityManager.shared().identityKeyPair() != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded() appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPollerIfNeeded() appDelegate.startClosedGroupPollerIfNeeded()
appDelegate.startOpenGroupPollersIfNeeded() appDelegate.startOpenGroupPollersIfNeeded()
} }
// Populate onion request path countries cache // Onion request path countries cache
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
let _ = IP2Country.shared.populateCacheIfNeeded() let _ = IP2Country.shared.populateCacheIfNeeded()
} }
// Do initial update
reload()
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
isViewVisible = true reload()
UserDefaults.standard[.hasLaunchedOnce] = true UserDefaults.standard[.hasLaunchedOnce] = true
showKeyPairMigrationNudgeIfNeeded() showKeyPairMigrationModalIfNeeded()
showKeyPairMigrationSuccessModalIfNeeded() showKeyPairMigrationSuccessModalIfNeeded()
} }
private func showKeyPairMigrationNudgeIfNeeded() { deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Migration
private func showKeyPairMigrationModalIfNeeded() {
guard !KeyPairUtilities.hasV2KeyPair() else { return } guard !KeyPairUtilities.hasV2KeyPair() else { return }
let sheet = KeyPairMigrationSheet() let sheet = KeyPairMigrationSheet()
sheet.modalPresentationStyle = .overFullScreen sheet.modalPresentationStyle = .overFullScreen
@ -183,16 +185,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
UserDefaults.standard[.isMigratingToV2KeyPair] = false UserDefaults.standard[.isMigratingToV2KeyPair] = false
} }
override func viewWillDisappear(_ animated: Bool) { // MARK: Table View Data Source
isViewVisible = false
super.viewWillDisappear(animated)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Int(threadCount) return Int(threadCount)
} }
@ -204,44 +197,29 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
} }
// MARK: Updating // MARK: Updating
private func updateIsObservingDatabase() {
isObservingDatabase = isViewVisible && CurrentAppContext().isAppForegroundAndActive()
}
private func reload() { private func reload() {
AssertIsOnMainThread() AssertIsOnMainThread()
uiDatabaseConnection.beginLongLivedReadTransaction() dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
uiDatabaseConnection.read { transaction in dbConnection.read { transaction in
self.threads.update(with: transaction) self.threads.update(with: transaction)
} }
threadViewModelCache.removeAll() threadViewModelCache.removeAll()
tableView.reloadData() tableView.reloadData()
emptyStateView.isHidden = (threadCount != 0) emptyStateView.isHidden = (threadCount != 0)
} }
@objc private func handleYapDatabaseModifiedNotification(_ notification: Notification) { @objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
AssertIsOnMainThread() AssertIsOnMainThread()
let notifications = uiDatabaseConnection.beginLongLivedReadTransaction() let notifications = dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
let ext = uiDatabaseConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications) let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
guard isObservingDatabase else { guard hasChanges else { return }
wasDatabaseModifiedExternally = hasChanges guard !notifications.isEmpty else { return }
return if let firstChangeSet = notifications[0].userInfo {
} let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
guard hasChanges else { if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
uiDatabaseConnection.read { transaction in return reload() // The code below will crash if we try to process multiple commits at once
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()
}
} }
} }
var sectionChanges = NSArray() var sectionChanges = NSArray()
@ -254,13 +232,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
let key = rowChange.collectionKey.key let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil threadViewModelCache[key] = nil
switch rowChange.type { switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade) case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
case .move: case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade)
case .update:
tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.none)
default: break default: break
} }
} }
@ -268,24 +243,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
emptyStateView.isHidden = (threadCount != 0) 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) { @objc private func handleProfileDidChangeNotification(_ notification: Notification) {
tableView.reloadData() // TODO: Just reload the affected cell tableView.reloadData() // TODO: Just reload the affected cell
} }
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) { @objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
updateNavigationBarButtons() updateNavBarButtons()
} }
@objc private func handleSeedViewedNotification(_ notification: Notification) { @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 self.tableView.reloadData() // TODO: Just reload the affected cell
} }
private func updateNavigationBarButtons() { private func updateNavBarButtons() {
let profilePictureSize = Values.verySmallProfilePictureSize let profilePictureSize = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView() let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button" profilePictureView.accessibilityLabel = "Settings button"
@ -353,10 +316,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
present(navigationController, animated: true, completion: nil) present(navigationController, animated: true, completion: nil)
} }
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
searchBar.resignFirstResponder()
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { 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 } guard let indexPath = tableView.indexPathForRow(at: location), let thread = self.thread(at: indexPath.row) else { return nil }
previewingContext.sourceRect = tableView.rectForRow(at: indexPath) previewingContext.sourceRect = tableView.rectForRow(at: indexPath)
@ -473,8 +432,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
} }
@objc func joinOpenGroup() { @objc func joinOpenGroup() {
let joinPublicChatVC = JoinPublicChatVC() let joinOpenGroupVC = JoinPublicChatVC()
let navigationController = OWSNavigationController(rootViewController: joinPublicChatVC) let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
present(navigationController, animated: true, completion: nil) present(navigationController, animated: true, completion: nil)
} }
@ -493,8 +452,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
// MARK: Convenience // MARK: Convenience
private func thread(at index: Int) -> TSThread? { private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil var thread: TSThread? = nil
uiDatabaseConnection.read { transaction in dbConnection.read { transaction in
thread = ((transaction as YapDatabaseReadTransaction).ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction).object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
} }
return thread return thread
} }
@ -505,7 +465,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
return cachedThreadViewModel return cachedThreadViewModel
} else { } else {
var threadViewModel: ThreadViewModel? = nil var threadViewModel: ThreadViewModel? = nil
uiDatabaseConnection.read { transaction in dbConnection.read { transaction in
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
} }
threadViewModelCache[thread.uniqueId!] = threadViewModel threadViewModelCache[thread.uniqueId!] = threadViewModel

@ -16,6 +16,12 @@ extension MessageReceiver {
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
default: fatalError() 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) { private static func handleReadReceipt(_ message: ReadReceipt, using transaction: Any) {

Loading…
Cancel
Save