Merge pull request #337 from oxen-io/database

Improved Database Handling
This commit is contained in:
Niels Andriesse 2021-01-21 15:29:36 +11:00 committed by GitHub
commit 4758fe7f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 119 deletions

View File

@ -5264,7 +5264,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5285,7 +5285,7 @@
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5333,7 +5333,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5359,7 +5359,7 @@
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5394,7 +5394,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5413,7 +5413,7 @@
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -5464,7 +5464,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5488,7 +5488,7 @@
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6483,7 +6483,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6519,7 +6519,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -6551,7 +6551,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 161;
CURRENT_PROJECT_VERSION = 162;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6587,7 +6587,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.7.4;
MARKETING_VERSION = 1.7.5;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -257,6 +257,9 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
return showError(title: "Couldn't Update Group", message: "Can't leave while adding or removing other members.")
}
}
guard members.count <= 100 else {
return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
}
Storage.write(with: { [weak self] transaction in
do {
if !members.contains(getUserHexEncodedPublicKey()) {

View File

@ -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

View File

@ -468,6 +468,8 @@ static NSTimeInterval launchStartedAt;
// Only mark the app as ready once
return;
}
[SNConfiguration performMainSetup];
// TODO: Once "app ready" logic is moved into AppSetup, move this line there.
[self.profileManager ensureLocalProfileCached];

View File

@ -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) {