Reworked the app startup process
Shifted the initial HomeVC population to a background thread to avoid blocking launch processing Added some logging for database 'ABORT' errors to better identify cases of deadlocks Added a launch timeout modal to allow users to share their logs if the startup process happens to hang Updated the notification handling (and cancelling) so it could run on background threads (seemed to take up a decent chunk of main thread time) Fixed an issue where the IP2Country population was running sync which could cause a hang on startup Fixed an issue where the code checking if the UIPasteBoard contained an image was explicitly advised against by the documentation (caused some reported hangs) Fixed a hang which could be caused by a redundant function when the ImagePickerController appeared
This commit is contained in:
parent
244fe9d7ae
commit
b6328f79b9
|
@ -13,7 +13,9 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
private static let loadingHeaderHeight: CGFloat = 40
|
||||
|
||||
internal let viewModel: ConversationViewModel
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var dataChangeObservable: DatabaseCancellable? {
|
||||
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
|
||||
}
|
||||
private var hasLoadedInitialThreadData: Bool = false
|
||||
private var hasLoadedInitialInteractionData: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
@ -518,6 +520,16 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
/// When the `ConversationVC` is on the screen we want to store it so we can avoid sending notification without accessing the
|
||||
/// main thread (we don't currently care if it's still in the nav stack though - so if a user is on a conversation settings screen this should
|
||||
/// get cleared within `viewWillDisappear`)
|
||||
///
|
||||
/// **Note:** We do this on an async queue because `Atomic<T>` can block if something else is mutating it and we want to avoid
|
||||
/// the risk of blocking the conversation transition
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
SessionApp.currentlyOpenConversationViewController.mutate { $0 = self }
|
||||
}
|
||||
|
||||
if delayFirstResponder || isShowingSearchUI {
|
||||
delayFirstResponder = false
|
||||
|
||||
|
@ -540,6 +552,16 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
/// When the `ConversationVC` is on the screen we want to store it so we can avoid sending notification without accessing the
|
||||
/// main thread (we don't currently care if it's still in the nav stack though - so if a user leaves a conversation settings screen we clear
|
||||
/// it, and if a user moves to a different `ConversationVC` this will get updated to that one within `viewDidAppear`)
|
||||
///
|
||||
/// **Note:** We do this on an async queue because `Atomic<T>` can block if something else is mutating it and we want to avoid
|
||||
/// the risk of blocking the conversation transition
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
SessionApp.currentlyOpenConversationViewController.mutate { $0 = nil }
|
||||
}
|
||||
|
||||
viewIsDisappearing = true
|
||||
|
||||
// Don't set the draft or resign the first responder if we are replacing the thread (want the keyboard
|
||||
|
@ -605,7 +627,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges(didReturnFromBackground: Bool = false) {
|
||||
// Start observing for data changes
|
||||
guard dataChangeObservable == nil else { return }
|
||||
|
||||
dataChangeObservable = Storage.shared.start(
|
||||
viewModel.observableThreadData,
|
||||
onError: { _ in },
|
||||
|
@ -675,8 +698,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
}
|
||||
|
||||
func stopObservingChanges() {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
self.dataChangeObservable = nil
|
||||
self.viewModel.onInteractionChange = nil
|
||||
}
|
||||
|
||||
|
|
|
@ -217,8 +217,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
didSet {
|
||||
// When starting to observe interaction changes we want to trigger a UI update just in case the
|
||||
// data was changed while we weren't observing
|
||||
if let unobservedInteractionDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedInteractionDataChanges {
|
||||
onInteractionChange?(unobservedInteractionDataChanges.0, unobservedInteractionDataChanges.1)
|
||||
if let changes: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedInteractionDataChanges {
|
||||
let performChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? = onInteractionChange
|
||||
|
||||
switch Thread.isMainThread {
|
||||
case true: performChange?(changes.0, changes.1)
|
||||
case false: DispatchQueue.main.async { performChange?(changes.0, changes.1) }
|
||||
}
|
||||
|
||||
self.unobservedInteractionDataChanges = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ public final class InputTextView: UITextView, UITextViewDelegate {
|
|||
|
||||
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(paste(_:)) {
|
||||
if let _ = UIPasteboard.general.image {
|
||||
if UIPasteboard.general.hasImages {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
public static let newConversationButtonSize: CGFloat = 60
|
||||
|
||||
private let viewModel: HomeViewModel = HomeViewModel()
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var dataChangeObservable: DatabaseCancellable? {
|
||||
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
|
||||
}
|
||||
private var hasLoadedInitialStateData: Bool = false
|
||||
private var hasLoadedInitialThreadData: Bool = false
|
||||
private var isLoadingMore: Bool = false
|
||||
|
@ -327,26 +329,31 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
|
||||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges(didReturnFromBackground: Bool = false) {
|
||||
// Start observing for data changes
|
||||
public func startObservingChanges(didReturnFromBackground: Bool = false, onReceivedInitialChange: (() -> ())? = nil) {
|
||||
guard dataChangeObservable == nil else { return }
|
||||
|
||||
var runAndClearInitialChangeCallback: (() -> ())? = nil
|
||||
|
||||
runAndClearInitialChangeCallback = { [weak self] in
|
||||
guard self?.hasLoadedInitialStateData == true && self?.hasLoadedInitialThreadData == true else { return }
|
||||
|
||||
onReceivedInitialChange?()
|
||||
runAndClearInitialChangeCallback = nil
|
||||
}
|
||||
|
||||
dataChangeObservable = Storage.shared.start(
|
||||
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: (hasLoadedInitialStateData ?
|
||||
.async(onQueue: .main) :
|
||||
.immediate
|
||||
),
|
||||
onError: { _ in },
|
||||
onChange: { [weak self] state in
|
||||
// The default scheduler emits changes on the main thread
|
||||
self?.handleUpdates(state)
|
||||
runAndClearInitialChangeCallback?()
|
||||
}
|
||||
)
|
||||
|
||||
self.viewModel.onThreadChange = { [weak self] updatedThreadData, changeset in
|
||||
self?.handleThreadUpdates(updatedThreadData, changeset: changeset)
|
||||
runAndClearInitialChangeCallback?()
|
||||
}
|
||||
|
||||
// Note: When returning from the background we could have received notifications but the
|
||||
|
@ -361,7 +368,7 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
|
||||
private func stopObservingChanges() {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
self.dataChangeObservable = nil
|
||||
self.viewModel.onThreadChange = nil
|
||||
}
|
||||
|
||||
|
|
|
@ -208,12 +208,16 @@ public class HomeViewModel {
|
|||
)
|
||||
}
|
||||
)
|
||||
|
||||
self?.hasReceivedInitialThreadData = true
|
||||
}
|
||||
)
|
||||
|
||||
// Run the initial query on the main thread so we prevent the app from leaving the loading screen
|
||||
// until we have data (Note: the `.pageBefore` will query from a `0` offset loading the first page)
|
||||
self.pagedDataObserver?.load(.pageBefore)
|
||||
// Run the initial query on a background thread so we don't block the main thread
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
// The `.pageBefore` will query from a `0` offset loading the first page
|
||||
self?.pagedDataObserver?.load(.pageBefore)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
@ -254,8 +258,10 @@ public class HomeViewModel {
|
|||
let oldState: State = self.state
|
||||
self.state = updatedState
|
||||
|
||||
// If the messageRequest content changed then we need to re-process the thread data
|
||||
// If the messageRequest content changed then we need to re-process the thread data (assuming
|
||||
// we've received the initial thread data)
|
||||
guard
|
||||
self.hasReceivedInitialThreadData,
|
||||
(
|
||||
oldState.hasHiddenMessageRequests != updatedState.hasHiddenMessageRequests ||
|
||||
oldState.unreadMessageRequestThreadCount != updatedState.unreadMessageRequestThreadCount
|
||||
|
@ -272,11 +278,7 @@ public class HomeViewModel {
|
|||
|
||||
PagedData.processAndTriggerUpdates(
|
||||
updatedData: updatedThreadData,
|
||||
currentDataRetriever: { [weak self] in
|
||||
guard self?.hasProcessedInitialThreadData == true else { return nil }
|
||||
|
||||
return (self?.unobservedThreadDataChanges?.0 ?? self?.threadData)
|
||||
},
|
||||
currentDataRetriever: { [weak self] in (self?.unobservedThreadDataChanges?.0 ?? self?.threadData) },
|
||||
onDataChange: onThreadChange,
|
||||
onUnobservedDataChange: { [weak self] updatedData, changeset in
|
||||
self?.unobservedThreadDataChanges = (changeset.isEmpty ?
|
||||
|
@ -289,19 +291,23 @@ public class HomeViewModel {
|
|||
|
||||
// MARK: - Thread Data
|
||||
|
||||
private var hasProcessedInitialThreadData: Bool = false
|
||||
private var hasReceivedInitialThreadData: Bool = false
|
||||
public private(set) var unobservedThreadDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>)?
|
||||
public private(set) var threadData: [SectionModel] = []
|
||||
public private(set) var pagedDataObserver: PagedDatabaseObserver<SessionThread, SessionThreadViewModel>?
|
||||
|
||||
public var onThreadChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? {
|
||||
didSet {
|
||||
self.hasProcessedInitialThreadData = (onThreadChange != nil || hasProcessedInitialThreadData)
|
||||
|
||||
// When starting to observe interaction changes we want to trigger a UI update just in case the
|
||||
// data was changed while we weren't observing
|
||||
if let unobservedThreadDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedThreadDataChanges {
|
||||
onThreadChange?(unobservedThreadDataChanges.0, unobservedThreadDataChanges.1)
|
||||
if let changes: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedThreadDataChanges {
|
||||
let performChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? = onThreadChange
|
||||
|
||||
switch Thread.isMainThread {
|
||||
case true: performChange?(changes.0, changes.1)
|
||||
case false: DispatchQueue.main.async { performChange?(changes.0, changes.1) }
|
||||
}
|
||||
|
||||
self.unobservedThreadDataChanges = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
|
|||
private static let loadingHeaderHeight: CGFloat = 40
|
||||
|
||||
private let viewModel: MessageRequestsViewModel = MessageRequestsViewModel()
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var hasLoadedInitialThreadData: Bool = false
|
||||
private var isLoadingMore: Bool = false
|
||||
private var isAutoLoadingNextPage: Bool = false
|
||||
|
@ -161,8 +160,7 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
|
|||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
|
@ -173,8 +171,7 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
|
|||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
@ -223,6 +220,10 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
|
|||
}
|
||||
}
|
||||
|
||||
private func stopObservingChanges() {
|
||||
self.viewModel.onThreadChange = nil
|
||||
}
|
||||
|
||||
private func handleThreadUpdates(
|
||||
_ updatedData: [MessageRequestsViewModel.SectionModel],
|
||||
changeset: StagedChangeset<[MessageRequestsViewModel.SectionModel]>,
|
||||
|
|
|
@ -129,8 +129,14 @@ public class MessageRequestsViewModel {
|
|||
didSet {
|
||||
// When starting to observe interaction changes we want to trigger a UI update just in case the
|
||||
// data was changed while we weren't observing
|
||||
if let unobservedThreadDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedThreadDataChanges {
|
||||
self.onThreadChange?(unobservedThreadDataChanges.0, unobservedThreadDataChanges.1)
|
||||
if let changes: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedThreadDataChanges {
|
||||
let performChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? = onThreadChange
|
||||
|
||||
switch Thread.isMainThread {
|
||||
case true: performChange?(changes.0, changes.1)
|
||||
case false: DispatchQueue.main.async { performChange?(changes.0, changes.1) }
|
||||
}
|
||||
|
||||
self.unobservedThreadDataChanges = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,8 +203,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
|||
let scale = UIScreen.main.scale
|
||||
let cellSize = collectionViewFlowLayout.itemSize
|
||||
photoMediaSize.thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
|
||||
|
||||
reloadDataAndRestoreSelection()
|
||||
|
||||
if !hasEverAppeared {
|
||||
scrollToBottom(animated: false)
|
||||
}
|
||||
|
@ -291,30 +290,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
|||
}
|
||||
}
|
||||
|
||||
private func reloadDataAndRestoreSelection() {
|
||||
guard let collectionView = collectionView else {
|
||||
owsFailDebug("Missing collectionView.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("delegate was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
collectionView.reloadData()
|
||||
collectionView.layoutIfNeeded()
|
||||
|
||||
let count = photoCollectionContents.assetCount
|
||||
for index in 0..<count {
|
||||
let asset = photoCollectionContents.asset(at: index)
|
||||
if delegate.imagePicker(self, isAssetSelected: asset) {
|
||||
collectionView.selectItem(at: IndexPath(row: index, section: 0),
|
||||
animated: false, scrollPosition: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc
|
||||
|
@ -365,7 +340,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
|||
}
|
||||
|
||||
collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode
|
||||
reloadDataAndRestoreSelection()
|
||||
}
|
||||
|
||||
func clearCollectionViewSelection() {
|
||||
|
@ -402,7 +376,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
|||
|
||||
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
|
||||
photoCollectionContents = photoCollection.contents()
|
||||
reloadDataAndRestoreSelection()
|
||||
}
|
||||
|
||||
// MARK: - PhotoCollectionPicker Presentation
|
||||
|
|
|
@ -48,8 +48,14 @@ public class MediaGalleryViewModel {
|
|||
didSet {
|
||||
// When starting to observe interaction changes we want to trigger a UI update just in case the
|
||||
// data was changed while we weren't observing
|
||||
if let unobservedGalleryDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedGalleryDataChanges {
|
||||
onGalleryChange?(unobservedGalleryDataChanges.0, unobservedGalleryDataChanges.1)
|
||||
if let changes: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedGalleryDataChanges {
|
||||
let performChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? = onGalleryChange
|
||||
|
||||
switch Thread.isMainThread {
|
||||
case true: performChange?(changes.0, changes.1)
|
||||
case false: DispatchQueue.main.async { performChange?(changes.0, changes.1) }
|
||||
}
|
||||
|
||||
self.unobservedGalleryDataChanges = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
fileprivate var mediaInteractiveDismiss: MediaInteractiveDismiss?
|
||||
|
||||
public let viewModel: MediaGalleryViewModel
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var dataChangeObservable: DatabaseCancellable? {
|
||||
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
|
||||
}
|
||||
private var initialPage: MediaDetailViewController
|
||||
private var cachedPages: [Int64: [MediaGalleryViewModel.Item: MediaDetailViewController]] = [:]
|
||||
|
||||
|
@ -40,7 +42,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
)
|
||||
|
||||
// Swap out the database observer
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
viewModel.replaceAlbumObservation(toObservationFor: item.interactionId)
|
||||
startObservingChanges()
|
||||
|
||||
|
@ -238,8 +240,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
|
||||
resignFirstResponder()
|
||||
}
|
||||
|
@ -252,8 +253,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
@ -388,6 +388,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges() {
|
||||
guard dataChangeObservable == nil else { return }
|
||||
|
||||
// Start observing for data changes
|
||||
dataChangeObservable = Storage.shared.start(
|
||||
viewModel.observableAlbumData,
|
||||
|
@ -399,6 +401,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
)
|
||||
}
|
||||
|
||||
private func stopObservingChanges() {
|
||||
dataChangeObservable = nil
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedViewData: [MediaGalleryViewModel.Item]) {
|
||||
// Determine if we swapped albums (if so we don't need to do anything else)
|
||||
guard updatedViewData.contains(where: { $0.interactionId == currentItem.interactionId }) else {
|
||||
|
@ -710,7 +716,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
// Swap out the database observer
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
viewModel.replaceAlbumObservation(toObservationFor: interactionIdAfter)
|
||||
startObservingChanges()
|
||||
|
||||
|
@ -755,7 +761,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
// Swap out the database observer
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
viewModel.replaceAlbumObservation(toObservationFor: interactionIdBefore)
|
||||
startObservingChanges()
|
||||
|
||||
|
|
|
@ -13,23 +13,25 @@ import SignalCoreKit
|
|||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
private static let maxRootViewControllerInitialQueryDuration: TimeInterval = 5
|
||||
|
||||
var window: UIWindow?
|
||||
var backgroundSnapshotBlockerWindow: UIWindow?
|
||||
var appStartupWindow: UIWindow?
|
||||
var hasInitialRootViewController: Bool = false
|
||||
var startTime: CFTimeInterval = 0
|
||||
private var loadingViewController: LoadingViewController?
|
||||
|
||||
enum LifecycleMethod {
|
||||
case finishLaunching
|
||||
case enterForeground
|
||||
}
|
||||
|
||||
/// This needs to be a lazy variable to ensure it doesn't get initialized before it actually needs to be used
|
||||
lazy var poller: CurrentUserPoller = CurrentUserPoller()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Log something immediately to make it easier to track app launches (and crashes during launch)
|
||||
SNLog("Launching \(SessionApp.versionInfo)")
|
||||
startTime = CACurrentMediaTime()
|
||||
|
||||
// These should be the first things we do (the startup process can fail without them)
|
||||
SetCurrentAppContext(MainAppContext())
|
||||
verifyDBKeysAvailableBeforeBackgroundLaunch()
|
||||
|
@ -71,7 +73,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: .finishLaunching, error: error)
|
||||
self?.showFailedStartupAlert(calledFrom: .finishLaunching, error: .migrationError(error))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -147,7 +149,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: .enterForeground, error: error)
|
||||
DispatchQueue.main.async {
|
||||
self?.showFailedStartupAlert(calledFrom: .enterForeground, error: .migrationError(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -187,7 +191,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
UserDefaults.sharedLokiProject?[.isMainAppActive] = true
|
||||
|
||||
ensureRootViewController()
|
||||
ensureRootViewController(calledFrom: .didBecomeActive)
|
||||
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in
|
||||
self?.handleActivation()
|
||||
|
@ -283,122 +287,139 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
Configuration.performMainSetup()
|
||||
JobRunner.add(executor: SyncPushTokensJob.self, for: .syncPushTokens)
|
||||
|
||||
/// Setup the UI
|
||||
///
|
||||
/// **Note:** This **MUST** be run before calling:
|
||||
/// - `AppReadiness.setAppIsReady()`:
|
||||
/// If we are launching the app from a push notification the HomeVC won't be setup yet
|
||||
/// and it won't open the related thread
|
||||
///
|
||||
/// - `JobRunner.appDidFinishLaunching()`:
|
||||
/// The jobs which run on launch (eg. DisappearingMessages job) can impact the interactions
|
||||
/// which get fetched to display on the home screen, if the PagedDatabaseObserver hasn't
|
||||
/// been setup yet then the home screen can show stale (ie. deleted) interactions incorrectly
|
||||
self.ensureRootViewController(isPreAppReadyCall: true)
|
||||
|
||||
// Trigger any launch-specific jobs and start the JobRunner
|
||||
if lifecycleMethod == .finishLaunching {
|
||||
JobRunner.appDidFinishLaunching()
|
||||
}
|
||||
|
||||
// Note that this does much more than set a flag;
|
||||
// it will also run all deferred blocks (including the JobRunner
|
||||
// 'appDidBecomeActive' method)
|
||||
AppReadiness.setAppIsReady()
|
||||
|
||||
DeviceSleepManager.sharedInstance.removeBlock(blockObject: self)
|
||||
AppVersion.sharedInstance().mainAppLaunchDidComplete()
|
||||
Environment.shared?.audioSession.setup()
|
||||
Environment.shared?.reachabilityManager.setup()
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
// Disable the SAE until the main app has successfully completed launch process
|
||||
// at least once in the post-SAE world.
|
||||
db[.isReadyForAppExtensions] = true
|
||||
// Setup the UI and trigger any post-UI setup actions
|
||||
self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self] in
|
||||
/// Trigger any launch-specific jobs and start the JobRunner with `JobRunner.appDidFinishLaunching()` some
|
||||
/// of these jobs (eg. DisappearingMessages job) can impact the interactions which get fetched to display on the home
|
||||
/// screen, if the PagedDatabaseObserver hasn't been setup yet then the home screen can show stale (ie. deleted)
|
||||
/// interactions incorrectly
|
||||
if lifecycleMethod == .finishLaunching {
|
||||
JobRunner.appDidFinishLaunching()
|
||||
}
|
||||
|
||||
if Identity.userCompletedRequiredOnboarding(db) {
|
||||
let appVersion: AppVersion = AppVersion.sharedInstance()
|
||||
/// Flag that the app is ready via `AppReadiness.setAppIsReady()`
|
||||
///
|
||||
/// If we are launching the app from a push notification we need to ensure we wait until after the `HomeVC` is setup
|
||||
/// otherwise it won't open the related thread
|
||||
///
|
||||
/// **Note:** This this does much more than set a flag - it will also run all deferred blocks (including the JobRunner
|
||||
/// `appDidBecomeActive` method hence why it **must** also come after calling
|
||||
/// `JobRunner.appDidFinishLaunching()`)
|
||||
AppReadiness.setAppIsReady()
|
||||
|
||||
/// Remove the sleep blocking once the startup is done (needs to run on the main thread and sleeping while
|
||||
/// doing the startup could suspend the database causing errors/crashes
|
||||
DeviceSleepManager.sharedInstance.removeBlock(blockObject: self)
|
||||
|
||||
/// App launch hasn't really completed until the main screen is loaded so wait until then to register it
|
||||
AppVersion.sharedInstance().mainAppLaunchDidComplete()
|
||||
|
||||
/// App won't be ready for extensions and no need to enqueue a config sync unless we successfully completed startup
|
||||
Storage.shared.writeAsync { db in
|
||||
// Disable the SAE until the main app has successfully completed launch process
|
||||
// at least once in the post-SAE world.
|
||||
db[.isReadyForAppExtensions] = true
|
||||
|
||||
// If the device needs to sync config or the user updated to a new version
|
||||
if
|
||||
needsConfigSync || (
|
||||
(appVersion.lastAppVersion?.count ?? 0) > 0 &&
|
||||
appVersion.lastAppVersion != appVersion.currentAppVersion
|
||||
)
|
||||
{
|
||||
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
|
||||
if Identity.userCompletedRequiredOnboarding(db) {
|
||||
let appVersion: AppVersion = AppVersion.sharedInstance()
|
||||
|
||||
// If the device needs to sync config or the user updated to a new version
|
||||
if
|
||||
needsConfigSync || (
|
||||
(appVersion.lastAppVersion?.count ?? 0) > 0 &&
|
||||
appVersion.lastAppVersion != appVersion.currentAppVersion
|
||||
)
|
||||
{
|
||||
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a log to track the proper startup time of the app so we know whether we need to
|
||||
// improve it in the future from user logs
|
||||
let endTime: CFTimeInterval = CACurrentMediaTime()
|
||||
SNLog("Launch completed in \((self?.startTime).map { ceil((endTime - $0) * 1000) } ?? -1)ms")
|
||||
}
|
||||
|
||||
// May as well run these on the background thread
|
||||
Environment.shared?.audioSession.setup()
|
||||
Environment.shared?.reachabilityManager.setup()
|
||||
}
|
||||
|
||||
private func showFailedMigrationAlert(
|
||||
private func showFailedStartupAlert(
|
||||
calledFrom lifecycleMethod: LifecycleMethod,
|
||||
error: Error?,
|
||||
isRestoreError: Bool = false
|
||||
error: StartupError,
|
||||
animated: Bool = true,
|
||||
presentationCompletion: (() -> ())? = nil
|
||||
) {
|
||||
let alert = UIAlertController(
|
||||
/// This **must** be a standard `UIAlertController` instead of a `ConfirmationModal` because we may not
|
||||
/// have access to the database when displaying this so can't extract theme information for styling purposes
|
||||
let alert: UIAlertController = UIAlertController(
|
||||
title: "Session",
|
||||
message: {
|
||||
switch (isRestoreError, (error ?? StorageError.generic)) {
|
||||
case (true, _): return "DATABASE_RESTORE_FAILED".localized()
|
||||
case (_, StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized()
|
||||
default: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
}
|
||||
}(),
|
||||
message: error.message,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "HELP_REPORT_BUG_ACTION_TITLE".localized(), style: .default) { _ in
|
||||
HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||
// Don't bother showing the "Failed Startup" modal again if we happen to now
|
||||
// have an initial view controller (this most likely means that the startup
|
||||
// completed while the user was sharing logs so we can just let the user use
|
||||
// the app)
|
||||
guard self?.hasInitialRootViewController == false else { return }
|
||||
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: error)
|
||||
}
|
||||
})
|
||||
|
||||
// Only offer the 'Restore' option if the user hasn't already tried to restore
|
||||
if !isRestoreError {
|
||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||
if SUKLegacy.hasLegacyDatabaseFile {
|
||||
// Remove the legacy database and any message hashes that have been migrated to the new DB
|
||||
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
||||
|
||||
Storage.shared.write { db in
|
||||
try SnodeReceivedMessageInfo.deleteAll(db)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If we don't have a legacy database then reset the current database for a clean migration
|
||||
Storage.resetForCleanMigration()
|
||||
}
|
||||
|
||||
// Hide the top banner if there was one
|
||||
TopBannerController.hide()
|
||||
|
||||
// The re-run the migration (should succeed since there is no data)
|
||||
AppSetup.runPostSetupMigrations(
|
||||
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
|
||||
self?.loadingViewController?.updateProgress(
|
||||
progress: progress,
|
||||
minEstimatedTotalTime: minEstimatedTotalTime
|
||||
)
|
||||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error, isRestoreError: true)
|
||||
return
|
||||
}
|
||||
switch error {
|
||||
// Offer the 'Restore' option if it was a migration error
|
||||
case .migrationError:
|
||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||
if SUKLegacy.hasLegacyDatabaseFile {
|
||||
// Remove the legacy database and any message hashes that have been migrated to the new DB
|
||||
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
||||
|
||||
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync)
|
||||
Storage.shared.write { db in
|
||||
try SnodeReceivedMessageInfo.deleteAll(db)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
else {
|
||||
// If we don't have a legacy database then reset the current database for a clean migration
|
||||
Storage.resetForCleanMigration()
|
||||
}
|
||||
|
||||
// Hide the top banner if there was one
|
||||
TopBannerController.hide()
|
||||
|
||||
// The re-run the migration (should succeed since there is no data)
|
||||
AppSetup.runPostSetupMigrations(
|
||||
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
|
||||
self?.loadingViewController?.updateProgress(
|
||||
progress: progress,
|
||||
minEstimatedTotalTime: minEstimatedTotalTime
|
||||
)
|
||||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
switch result {
|
||||
case .failure:
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .failedToRestore)
|
||||
|
||||
case .success:
|
||||
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Close", style: .default) { _ in
|
||||
alert.addAction(UIAlertAction(title: "APP_STARTUP_EXIT".localized(), style: .default) { _ in
|
||||
DDLog.flushLog()
|
||||
exit(0)
|
||||
})
|
||||
|
||||
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
|
||||
self.window?.rootViewController?.present(alert, animated: animated, completion: presentationCompletion)
|
||||
}
|
||||
|
||||
/// The user must unlock the device once after reboot before the database encryption key can be accessed.
|
||||
|
@ -452,36 +473,101 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
}
|
||||
|
||||
private func ensureRootViewController(isPreAppReadyCall: Bool = false) {
|
||||
guard (AppReadiness.isAppReady() || isPreAppReadyCall) && Storage.shared.isValid && !hasInitialRootViewController else {
|
||||
private func ensureRootViewController(
|
||||
calledFrom lifecycleMethod: LifecycleMethod,
|
||||
onComplete: (() -> ())? = nil
|
||||
) {
|
||||
guard (AppReadiness.isAppReady() || lifecycleMethod == .finishLaunching) && Storage.shared.isValid && !hasInitialRootViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
self.hasInitialRootViewController = true
|
||||
self.window?.rootViewController = TopBannerController(
|
||||
child: StyledNavigationController(
|
||||
rootViewController: {
|
||||
guard Identity.userExists() else { return LandingVC() }
|
||||
guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else {
|
||||
// If we have no display name then collect one (this can happen if the
|
||||
// app crashed during onboarding which would leave the user in an invalid
|
||||
// state with no display name)
|
||||
return DisplayNameVC(flow: .register)
|
||||
}
|
||||
|
||||
return HomeVC()
|
||||
}()
|
||||
),
|
||||
cachedWarning: UserDefaults.sharedLokiProject?[.topBannerWarningToShow]
|
||||
.map { rawValue in TopBannerController.Warning(rawValue: rawValue) }
|
||||
)
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
/// Start a timeout for the creation of the rootViewController setup process (if it takes too long then we want to give the user
|
||||
/// the option to export their logs)
|
||||
let populateHomeScreenTimer: Timer = Timer.scheduledTimerOnMainThread(
|
||||
withTimeInterval: AppDelegate.maxRootViewControllerInitialQueryDuration,
|
||||
repeats: false
|
||||
) { [weak self] timer in
|
||||
timer.invalidate()
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout)
|
||||
}
|
||||
|
||||
/// **Note:** There is an annoying case when starting the app by interacting with a push notification where
|
||||
/// the `HomeVC` won't have completed loading it's view which means the `SessionApp.homeViewController`
|
||||
/// won't have been set - we set the value directly here to resolve this edge case
|
||||
if let homeViewController: HomeVC = (self.window?.rootViewController as? UINavigationController)?.viewControllers.first as? HomeVC {
|
||||
SessionApp.homeViewController.mutate { $0 = homeViewController }
|
||||
// All logic which needs to run after the 'rootViewController' is created
|
||||
let rootViewControllerSetupComplete: (UIViewController) -> () = { [weak self] rootViewController in
|
||||
let presentedViewController: UIViewController? = self?.window?.rootViewController?.presentedViewController
|
||||
let targetRootViewController: UIViewController = TopBannerController(
|
||||
child: StyledNavigationController(rootViewController: rootViewController),
|
||||
cachedWarning: UserDefaults.sharedLokiProject?[.topBannerWarningToShow]
|
||||
.map { rawValue in TopBannerController.Warning(rawValue: rawValue) }
|
||||
)
|
||||
|
||||
/// Insert the `targetRootViewController` below the current view and trigger a layout without animation before properly
|
||||
/// swapping the `rootViewController` over so we can avoid any weird initial layout behaviours
|
||||
UIView.performWithoutAnimation {
|
||||
self?.window?.rootViewController = targetRootViewController
|
||||
}
|
||||
|
||||
self?.hasInitialRootViewController = true
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
|
||||
/// **Note:** There is an annoying case when starting the app by interacting with a push notification where
|
||||
/// the `HomeVC` won't have completed loading it's view which means the `SessionApp.homeViewController`
|
||||
/// won't have been set - we set the value directly here to resolve this edge case
|
||||
if let homeViewController: HomeVC = rootViewController as? HomeVC {
|
||||
SessionApp.homeViewController.mutate { $0 = homeViewController }
|
||||
}
|
||||
|
||||
/// If we were previously presenting a viewController but are no longer preseting it then present it again
|
||||
///
|
||||
/// **Note:** Looks like the OS will throw an exception if we try to present a screen which is already (or
|
||||
/// was previously?) presented, even if it's not attached to the screen it seems...
|
||||
switch presentedViewController {
|
||||
case is UIAlertController, is ConfirmationModal:
|
||||
/// If the viewController we were presenting happened to be the "failed startup" modal then we can dismiss it
|
||||
/// automatically (while this seems redundant it's less jarring for the user than just instantly having it disappear)
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout, animated: false) {
|
||||
self?.window?.rootViewController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
case is UIActivityViewController: HelpViewModel.shareLogs(animated: false)
|
||||
default: break
|
||||
}
|
||||
|
||||
// Setup is completed so run any post-setup tasks
|
||||
onComplete?()
|
||||
}
|
||||
|
||||
// Navigate to the approriate screen depending on the onboarding state
|
||||
switch Onboarding.State.current {
|
||||
case .newUser:
|
||||
DispatchQueue.main.async {
|
||||
let viewController: LandingVC = LandingVC()
|
||||
populateHomeScreenTimer.invalidate()
|
||||
rootViewControllerSetupComplete(viewController)
|
||||
}
|
||||
|
||||
case .missingName:
|
||||
DispatchQueue.main.async {
|
||||
let viewController: DisplayNameVC = DisplayNameVC(flow: .register)
|
||||
populateHomeScreenTimer.invalidate()
|
||||
rootViewControllerSetupComplete(viewController)
|
||||
}
|
||||
|
||||
case .completed:
|
||||
DispatchQueue.main.async {
|
||||
let viewController: HomeVC = HomeVC()
|
||||
|
||||
/// We want to start observing the changes for the 'HomeVC' and want to wait until we actually get data back before we
|
||||
/// continue as we don't want to show a blank home screen
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
viewController.startObservingChanges() {
|
||||
populateHomeScreenTimer.invalidate()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
rootViewControllerSetupComplete(viewController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -750,3 +836,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LifecycleMethod
|
||||
|
||||
private enum LifecycleMethod {
|
||||
case finishLaunching
|
||||
case enterForeground
|
||||
case didBecomeActive
|
||||
}
|
||||
|
||||
// MARK: - StartupError
|
||||
|
||||
private enum StartupError: Error {
|
||||
case databaseStartupError
|
||||
case migrationError(Error)
|
||||
case failedToRestore
|
||||
case startupTimeout
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .databaseStartupError: return "DATABASE_STARTUP_FAILED".localized()
|
||||
case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
|
||||
case .migrationError: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,31 @@ import SessionMessagingKit
|
|||
import SignalCoreKit
|
||||
|
||||
public struct SessionApp {
|
||||
// FIXME: Refactor this to be protocol based for unit testing (or even dynamic based on view hierarchy - do want to avoid needing to use the main thread to access them though)
|
||||
static let homeViewController: Atomic<HomeVC?> = Atomic(nil)
|
||||
static let currentlyOpenConversationViewController: Atomic<ConversationVC?> = Atomic(nil)
|
||||
|
||||
static var versionInfo: String {
|
||||
let buildNumber: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String)
|
||||
.map { " (\($0))" }
|
||||
.defaulting(to: "")
|
||||
let appVersion: String? = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
||||
.map { "App: \($0)\(buildNumber)" }
|
||||
#if DEBUG
|
||||
let commitInfo: String? = (Bundle.main.infoDictionary?["GitCommitHash"] as? String).map { "Commit: \($0)" }
|
||||
#else
|
||||
let commitInfo: String? = nil
|
||||
#endif
|
||||
|
||||
let versionInfo: [String] = [
|
||||
"iOS \(UIDevice.current.systemVersion)",
|
||||
appVersion,
|
||||
"libSession: \(SessionUtil.libSessionVersion)",
|
||||
commitInfo
|
||||
].compactMap { $0 }
|
||||
|
||||
return versionInfo.joined(separator: ", ")
|
||||
}
|
||||
|
||||
// MARK: - View Convenience Methods
|
||||
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
|
||||
"LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "هنگام بهینهسازی پایگاه داده خطایی روی داد\n\nشما میتوانید گزارشهای برنامه خود را صادر کنید تا بتوانید برای عیبیابی به اشتراک بگذارید یا میتوانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن دادههای قدیمیتر از دو هفته میشود.";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
|
||||
"LOADING_CONVERSATIONS" = "Chargement des conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -415,6 +415,8 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -99,6 +99,7 @@ protocol NotificationPresenterAdaptee: AnyObject {
|
|||
sound: Preferences.Sound?,
|
||||
threadVariant: SessionThread.Variant,
|
||||
threadName: String,
|
||||
applicationState: UIApplication.State,
|
||||
replacingIdentifier: String?
|
||||
)
|
||||
|
||||
|
@ -116,7 +117,8 @@ extension NotificationPresenterAdaptee {
|
|||
previewType: Preferences.NotificationPreviewType,
|
||||
sound: Preferences.Sound?,
|
||||
threadVariant: SessionThread.Variant,
|
||||
threadName: String
|
||||
threadName: String,
|
||||
applicationState: UIApplication.State
|
||||
) {
|
||||
notify(
|
||||
category: category,
|
||||
|
@ -127,22 +129,16 @@ extension NotificationPresenterAdaptee {
|
|||
sound: sound,
|
||||
threadVariant: threadVariant,
|
||||
threadName: threadName,
|
||||
applicationState: applicationState,
|
||||
replacingIdentifier: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(OWSNotificationPresenter)
|
||||
public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||
|
||||
private let adaptee: NotificationPresenterAdaptee
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
self.adaptee = UserNotificationPresenterAdaptee()
|
||||
|
||||
super.init()
|
||||
public class NotificationPresenter: NotificationsProtocol {
|
||||
private let adaptee: NotificationPresenterAdaptee = UserNotificationPresenterAdaptee()
|
||||
|
||||
public init() {
|
||||
SwiftSingletons.register(self)
|
||||
}
|
||||
|
||||
|
@ -152,7 +148,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return adaptee.registerNotificationSettings()
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(
|
||||
_ db: Database,
|
||||
for interaction: Interaction,
|
||||
in thread: SessionThread,
|
||||
applicationState: UIApplication.State
|
||||
) {
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||
|
||||
// Ensure we should be showing a notification for the thread
|
||||
|
@ -244,34 +245,39 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound: Preferences.Sound? = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound
|
||||
)
|
||||
|
||||
notificationBody = MentionUtilities.highlightMentionsNoAttributes(
|
||||
in: (notificationBody ?? ""),
|
||||
threadVariant: thread.variant,
|
||||
currentUserPublicKey: userPublicKey,
|
||||
currentUserBlindedPublicKey: userBlindedKey
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: groupName,
|
||||
replacingIdentifier: identifier
|
||||
)
|
||||
}
|
||||
let sound: Preferences.Sound? = requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound,
|
||||
applicationState: applicationState
|
||||
)
|
||||
|
||||
notificationBody = MentionUtilities.highlightMentionsNoAttributes(
|
||||
in: (notificationBody ?? ""),
|
||||
threadVariant: thread.variant,
|
||||
currentUserPublicKey: userPublicKey,
|
||||
currentUserBlindedPublicKey: userBlindedKey
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: groupName,
|
||||
applicationState: applicationState,
|
||||
replacingIdentifier: identifier
|
||||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(
|
||||
_ db: Database,
|
||||
forIncomingCall interaction: Interaction,
|
||||
in thread: SessionThread,
|
||||
applicationState: UIApplication.State
|
||||
) {
|
||||
// No call notifications for muted or group threads
|
||||
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||
guard
|
||||
|
@ -320,28 +326,32 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
let sound = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound,
|
||||
applicationState: applicationState
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: senderName,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: (notificationBody ?? ""),
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: senderName,
|
||||
applicationState: applicationState,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) {
|
||||
public func notifyUser(
|
||||
_ db: Database,
|
||||
forReaction reaction: Reaction,
|
||||
in thread: SessionThread,
|
||||
applicationState: UIApplication.State
|
||||
) {
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||
|
||||
// No reaction notifications for muted, group threads or message requests
|
||||
|
@ -380,28 +390,31 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
)
|
||||
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
let sound = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound,
|
||||
applicationState: applicationState
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: category,
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
applicationState: applicationState,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
|
||||
public func notifyForFailedSend(_ db: Database, in thread: SessionThread) {
|
||||
public func notifyForFailedSend(
|
||||
_ db: Database,
|
||||
in thread: SessionThread,
|
||||
applicationState: UIApplication.State
|
||||
) {
|
||||
let notificationTitle: String?
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
.defaulting(to: .defaultPreviewType)
|
||||
|
@ -432,24 +445,23 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
]
|
||||
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let sound: Preferences.Sound? = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: .errorMessage,
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName
|
||||
)
|
||||
}
|
||||
let sound: Preferences.Sound? = self.requestSound(
|
||||
thread: thread,
|
||||
fallbackSound: fallbackSound,
|
||||
applicationState: applicationState
|
||||
)
|
||||
|
||||
self.adaptee.notify(
|
||||
category: .errorMessage,
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
userInfo: userInfo,
|
||||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
applicationState: applicationState
|
||||
)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -471,32 +483,30 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
// MARK: -
|
||||
|
||||
var mostRecentNotifications = TruncatedList<UInt64>(maxLength: kAudioNotificationsThrottleCount)
|
||||
var mostRecentNotifications: Atomic<TruncatedList<UInt64>> = Atomic(TruncatedList<UInt64>(maxLength: kAudioNotificationsThrottleCount))
|
||||
|
||||
private func requestSound(thread: SessionThread, fallbackSound: Preferences.Sound) -> Preferences.Sound? {
|
||||
guard checkIfShouldPlaySound() else {
|
||||
return nil
|
||||
}
|
||||
private func requestSound(
|
||||
thread: SessionThread,
|
||||
fallbackSound: Preferences.Sound,
|
||||
applicationState: UIApplication.State
|
||||
) -> Preferences.Sound? {
|
||||
guard checkIfShouldPlaySound(applicationState: applicationState) else { return nil }
|
||||
|
||||
return (thread.notificationSound ?? fallbackSound)
|
||||
}
|
||||
|
||||
private func checkIfShouldPlaySound() -> Bool {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard UIApplication.shared.applicationState == .active else { return true }
|
||||
private func checkIfShouldPlaySound(applicationState: UIApplication.State) -> Bool {
|
||||
guard applicationState == .active else { return true }
|
||||
guard Storage.shared[.playNotificationSoundInForeground] else { return false }
|
||||
|
||||
let nowMs: UInt64 = UInt64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
let recentThreshold = nowMs - UInt64(kAudioNotificationsThrottleInterval * Double(kSecondInMs))
|
||||
|
||||
let recentNotifications = mostRecentNotifications.filter { $0 > recentThreshold }
|
||||
let recentNotifications = mostRecentNotifications.wrappedValue.filter { $0 > recentThreshold }
|
||||
|
||||
guard recentNotifications.count < kAudioNotificationsThrottleCount else {
|
||||
return false
|
||||
}
|
||||
guard recentNotifications.count < kAudioNotificationsThrottleCount else { return false }
|
||||
|
||||
mostRecentNotifications.append(nowMs)
|
||||
mostRecentNotifications.mutate { $0.append(nowMs) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -527,7 +537,11 @@ class NotificationActionHandler {
|
|||
return markAsRead(threadId: threadId)
|
||||
}
|
||||
|
||||
func reply(userInfo: [AnyHashable: Any], replyText: String) -> AnyPublisher<Void, Error> {
|
||||
func reply(
|
||||
userInfo: [AnyHashable: Any],
|
||||
replyText: String,
|
||||
applicationState: UIApplication.State
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
|
||||
return Fail<Void, Error>(error: NotificationError.failDebug("threadId was unexpectedly nil"))
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -582,7 +596,11 @@ class NotificationActionHandler {
|
|||
case .finished: break
|
||||
case .failure:
|
||||
Storage.shared.read { [weak self] db in
|
||||
self?.notificationPresenter.notifyForFailedSend(db, in: thread)
|
||||
self?.notificationPresenter.notifyForFailedSend(
|
||||
db,
|
||||
in: thread,
|
||||
applicationState: applicationState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class UserNotificationConfig {
|
|||
|
||||
class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelegate {
|
||||
private let notificationCenter: UNUserNotificationCenter
|
||||
private var notifications: [String: UNNotificationRequest] = [:]
|
||||
private var notifications: Atomic<[String: UNNotificationRequest]> = Atomic([:])
|
||||
|
||||
override init() {
|
||||
self.notificationCenter = UNUserNotificationCenter.current()
|
||||
|
@ -105,10 +105,9 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
sound: Preferences.Sound?,
|
||||
threadVariant: SessionThread.Variant,
|
||||
threadName: String,
|
||||
applicationState: UIApplication.State,
|
||||
replacingIdentifier: String?
|
||||
) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String)
|
||||
let content = UNMutableNotificationContent()
|
||||
content.categoryIdentifier = category.identifier
|
||||
|
@ -119,16 +118,21 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
threadVariant == .community &&
|
||||
replacingIdentifier == threadIdentifier
|
||||
)
|
||||
let isAppActive = UIApplication.shared.applicationState == .active
|
||||
if let sound = sound, sound != .none {
|
||||
content.sound = sound.notificationSound(isQuiet: isAppActive)
|
||||
content.sound = sound.notificationSound(isQuiet: (applicationState == .active))
|
||||
}
|
||||
|
||||
let notificationIdentifier: String = (replacingIdentifier ?? UUID().uuidString)
|
||||
let isReplacingNotification: Bool = (notifications[notificationIdentifier] != nil)
|
||||
let isReplacingNotification: Bool = (notifications.wrappedValue[notificationIdentifier] != nil)
|
||||
let shouldPresentNotification: Bool = shouldPresentNotification(
|
||||
category: category,
|
||||
applicationState: applicationState,
|
||||
frontMostViewController: SessionApp.currentlyOpenConversationViewController.wrappedValue,
|
||||
userInfo: userInfo
|
||||
)
|
||||
var trigger: UNNotificationTrigger?
|
||||
|
||||
if shouldPresentNotification(category: category, userInfo: userInfo) {
|
||||
if shouldPresentNotification {
|
||||
if let displayableTitle = title?.filterForDisplay {
|
||||
content.title = displayableTitle
|
||||
}
|
||||
|
@ -142,7 +146,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
repeats: false
|
||||
)
|
||||
|
||||
let numberExistingNotifications: Int? = notifications[notificationIdentifier]?
|
||||
let numberExistingNotifications: Int? = notifications.wrappedValue[notificationIdentifier]?
|
||||
.content
|
||||
.userInfo[AppNotificationUserInfoKey.threadNotificationCounter]
|
||||
.asType(Int.self)
|
||||
|
@ -180,47 +184,48 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) }
|
||||
|
||||
notificationCenter.add(request)
|
||||
notifications[notificationIdentifier] = request
|
||||
notifications.mutate { $0[notificationIdentifier] = request }
|
||||
}
|
||||
|
||||
func cancelNotifications(identifiers: [String]) {
|
||||
AssertIsOnMainThread()
|
||||
identifiers.forEach { notifications.removeValue(forKey: $0) }
|
||||
notifications.mutate { notifications in
|
||||
identifiers.forEach { notifications.removeValue(forKey: $0) }
|
||||
}
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers)
|
||||
}
|
||||
|
||||
func cancelNotification(_ notification: UNNotificationRequest) {
|
||||
AssertIsOnMainThread()
|
||||
cancelNotifications(identifiers: [notification.identifier])
|
||||
}
|
||||
|
||||
func cancelNotifications(threadId: String) {
|
||||
AssertIsOnMainThread()
|
||||
for notification in notifications.values {
|
||||
guard let notificationThreadId = notification.content.userInfo[AppNotificationUserInfoKey.threadId] as? String else {
|
||||
continue
|
||||
let notificationsIdsToCancel: [String] = notifications.wrappedValue
|
||||
.values
|
||||
.compactMap { notification in
|
||||
guard
|
||||
let notificationThreadId: String = notification.content.userInfo[AppNotificationUserInfoKey.threadId] as? String,
|
||||
notificationThreadId == threadId
|
||||
else { return nil }
|
||||
|
||||
return notification.identifier
|
||||
}
|
||||
|
||||
guard notificationThreadId == threadId else {
|
||||
continue
|
||||
}
|
||||
|
||||
cancelNotification(notification)
|
||||
}
|
||||
|
||||
cancelNotifications(identifiers: notificationsIdsToCancel)
|
||||
}
|
||||
|
||||
func clearAllNotifications() {
|
||||
AssertIsOnMainThread()
|
||||
notificationCenter.removeAllPendingNotificationRequests()
|
||||
notificationCenter.removeAllDeliveredNotifications()
|
||||
}
|
||||
|
||||
func shouldPresentNotification(category: AppNotificationCategory, userInfo: [AnyHashable: Any]) -> Bool {
|
||||
AssertIsOnMainThread()
|
||||
guard UIApplication.shared.applicationState == .active else {
|
||||
return true
|
||||
}
|
||||
func shouldPresentNotification(
|
||||
category: AppNotificationCategory,
|
||||
applicationState: UIApplication.State,
|
||||
frontMostViewController: UIViewController?,
|
||||
userInfo: [AnyHashable: Any]
|
||||
) -> Bool {
|
||||
guard applicationState == .active else { return true }
|
||||
|
||||
guard category == .incomingMessage || category == .errorMessage else {
|
||||
return true
|
||||
|
@ -231,7 +236,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
return true
|
||||
}
|
||||
|
||||
guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationVC else {
|
||||
guard let conversationViewController: ConversationVC = frontMostViewController as? ConversationVC else {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -271,7 +276,8 @@ public class UserNotificationActionHandler: NSObject {
|
|||
AssertIsOnMainThread()
|
||||
assert(AppReadiness.isAppReady())
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo
|
||||
let applicationState: UIApplication.State = UIApplication.shared.applicationState
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier:
|
||||
|
@ -307,7 +313,7 @@ public class UserNotificationActionHandler: NSObject {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return actionHandler.reply(userInfo: userInfo, replyText: textInputResponse.userText)
|
||||
return actionHandler.reply(userInfo: userInfo, replyText: textInputResponse.userText, applicationState: applicationState)
|
||||
|
||||
case .showThread:
|
||||
return actionHandler.showThread(userInfo: userInfo)
|
||||
|
|
|
@ -123,6 +123,25 @@ enum Onboarding {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
enum State {
|
||||
case newUser
|
||||
case missingName
|
||||
case completed
|
||||
|
||||
static var current: State {
|
||||
// If we have no identify information then the user needs to register
|
||||
guard Identity.userExists() else { return .newUser }
|
||||
|
||||
// If we have no display name then collect one (this can happen if the
|
||||
// app crashed during onboarding which would leave the user in an invalid
|
||||
// state with no display name)
|
||||
guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else { return .missingName }
|
||||
|
||||
// Otherwise we have enough for a full user and can start the app
|
||||
return .completed
|
||||
}
|
||||
}
|
||||
|
||||
enum Flow {
|
||||
case register, recover, link
|
||||
|
||||
|
|
|
@ -177,23 +177,10 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
public static func shareLogs(
|
||||
viewControllerToDismiss: UIViewController? = nil,
|
||||
targetView: UIView? = nil,
|
||||
animated: Bool = true,
|
||||
onShareComplete: (() -> ())? = nil
|
||||
) {
|
||||
let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
||||
.defaulting(to: "")
|
||||
#if DEBUG
|
||||
let commitInfo: String? = (Bundle.main.infoDictionary?["GitCommitHash"] as? String).map { "Commit: \($0)" }
|
||||
#else
|
||||
let commitInfo: String? = nil
|
||||
#endif
|
||||
|
||||
let versionInfo: [String] = [
|
||||
"iOS \(UIDevice.current.systemVersion)",
|
||||
"App: \(version)",
|
||||
"libSession: \(SessionUtil.libSessionVersion)",
|
||||
commitInfo
|
||||
].compactMap { $0 }
|
||||
OWSLogger.info("[Version] \(versionInfo.joined(separator: ", "))")
|
||||
OWSLogger.info("[Version] \(SessionApp.versionInfo)")
|
||||
DDLog.flushLog()
|
||||
|
||||
let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths
|
||||
|
@ -216,7 +203,7 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view)
|
||||
shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds
|
||||
}
|
||||
viewController.present(shareVC, animated: true, completion: nil)
|
||||
viewController.present(shareVC, animated: animated, completion: nil)
|
||||
}
|
||||
|
||||
guard let viewControllerToDismiss: UIViewController = viewControllerToDismiss else {
|
||||
|
@ -224,7 +211,7 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
return
|
||||
}
|
||||
|
||||
viewControllerToDismiss.dismiss(animated: true) {
|
||||
viewControllerToDismiss.dismiss(animated: animated) {
|
||||
showShareSheet()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,11 @@ import GRDB
|
|||
import SessionSnodeKit
|
||||
|
||||
final class IP2Country {
|
||||
var countryNamesCache: Atomic<[String: String]> = Atomic([:])
|
||||
|
||||
|
||||
private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
|
||||
static var isInitialized = false
|
||||
|
||||
// MARK: Tables
|
||||
var countryNamesCache: Atomic<[String: String]> = Atomic([:])
|
||||
|
||||
// MARK: - Tables
|
||||
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
|
||||
/// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
|
||||
/// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
|
||||
|
@ -58,13 +56,12 @@ final class IP2Country {
|
|||
}
|
||||
|
||||
@objc func populateCacheIfNeededAsync() {
|
||||
// This has to be sync since the `countryNamesCache` dict doesn't like async access
|
||||
IP2Country.workQueue.sync { [weak self] in
|
||||
_ = self?.populateCacheIfNeeded()
|
||||
DispatchQueue.global(qos: .utility).async { [weak self] in
|
||||
self?.populateCacheIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func populateCacheIfNeeded() -> Bool {
|
||||
@discardableResult func populateCacheIfNeeded() -> Bool {
|
||||
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
|
||||
|
||||
countryNamesCache.mutate { [weak self] cache in
|
||||
|
|
|
@ -46,6 +46,10 @@ extension MessageReceiver {
|
|||
private static func handleNewCallMessage(_ db: Database, message: CallMessage) throws {
|
||||
SNLog("[Calls] Received pre-offer message.")
|
||||
|
||||
// Determine whether the app is active based on the prefs rather than the UIApplication state to avoid
|
||||
// requiring main-thread execution
|
||||
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
|
||||
|
||||
// It is enough just ignoring the pre offers, other call messages
|
||||
// for this call would be dropped because of no Session call instance
|
||||
guard
|
||||
|
@ -69,7 +73,8 @@ extension MessageReceiver {
|
|||
.notifyUser(
|
||||
db,
|
||||
forIncomingCall: interaction,
|
||||
in: thread
|
||||
in: thread,
|
||||
applicationState: (isMainAppActive ? .active : .background)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +91,8 @@ extension MessageReceiver {
|
|||
.notifyUser(
|
||||
db,
|
||||
forIncomingCall: interaction,
|
||||
in: thread
|
||||
in: thread,
|
||||
applicationState: (isMainAppActive ? .active : .background)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -331,7 +331,8 @@ extension MessageReceiver {
|
|||
.notifyUser(
|
||||
db,
|
||||
for: interaction,
|
||||
in: thread
|
||||
in: thread,
|
||||
applicationState: (isMainAppActive ? .active : .background)
|
||||
)
|
||||
|
||||
return interactionId
|
||||
|
@ -372,6 +373,9 @@ extension MessageReceiver {
|
|||
|
||||
switch reaction.kind {
|
||||
case .react:
|
||||
// Determine whether the app is active based on the prefs rather than the UIApplication state to avoid
|
||||
// requiring main-thread execution
|
||||
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
|
||||
let timestampMs: Int64 = Int64(messageSentTimestamp * 1000)
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let reaction: Reaction = try Reaction(
|
||||
|
@ -398,7 +402,8 @@ extension MessageReceiver {
|
|||
.notifyUser(
|
||||
db,
|
||||
forReaction: reaction,
|
||||
in: thread
|
||||
in: thread,
|
||||
applicationState: (isMainAppActive ? .active : .background)
|
||||
)
|
||||
}
|
||||
case .remove:
|
||||
|
|
|
@ -4,9 +4,9 @@ import Foundation
|
|||
import GRDB
|
||||
|
||||
public protocol NotificationsProtocol {
|
||||
func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread)
|
||||
func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread)
|
||||
func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread)
|
||||
func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State)
|
||||
func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State)
|
||||
func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread, applicationState: UIApplication.State)
|
||||
func cancelNotifications(identifiers: [String])
|
||||
func clearAllNotifications()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ public class DeviceSleepManager: NSObject {
|
|||
return "SleepBlock(\(String(reflecting: blockObject)))"
|
||||
}
|
||||
|
||||
init(blockObject: NSObject) {
|
||||
init(blockObject: NSObject?) {
|
||||
self.blockObject = blockObject
|
||||
}
|
||||
}
|
||||
|
@ -51,14 +51,14 @@ public class DeviceSleepManager: NSObject {
|
|||
}
|
||||
|
||||
@objc
|
||||
public func addBlock(blockObject: NSObject) {
|
||||
public func addBlock(blockObject: NSObject?) {
|
||||
blocks.append(SleepBlock(blockObject: blockObject))
|
||||
|
||||
ensureSleepBlocking()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func removeBlock(blockObject: NSObject) {
|
||||
public func removeBlock(blockObject: NSObject?) {
|
||||
blocks = blocks.filter {
|
||||
$0.blockObject != nil && $0.blockObject != blockObject
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import SessionMessagingKit
|
|||
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||
private var notifications: [String: UNNotificationRequest] = [:]
|
||||
|
||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||
|
||||
// Ensure we should be showing a notification for the thread
|
||||
|
@ -124,7 +124,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
// No call notifications for muted or group threads
|
||||
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||
guard
|
||||
|
@ -180,7 +180,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
)
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
||||
|
||||
// No reaction notifications for muted, group threads or message requests
|
||||
|
|
|
@ -158,7 +158,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
.notifyUser(
|
||||
db,
|
||||
forIncomingCall: interaction,
|
||||
in: thread
|
||||
in: thread,
|
||||
applicationState: .background
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import SessionMessagingKit
|
|||
|
||||
final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
|
||||
private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel()
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var dataChangeObservable: DatabaseCancellable? {
|
||||
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
|
||||
}
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
|
||||
var shareNavController: ShareNavController?
|
||||
|
@ -79,8 +81,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
|
@ -91,8 +92,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
|
@ -104,6 +104,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges() {
|
||||
guard dataChangeObservable == nil else { return }
|
||||
|
||||
// Start observing for data changes
|
||||
dataChangeObservable = Storage.shared.start(
|
||||
viewModel.observableViewData,
|
||||
|
@ -115,6 +117,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
)
|
||||
}
|
||||
|
||||
private func stopObservingChanges() {
|
||||
dataChangeObservable = nil
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedViewData: [SessionThreadViewModel]) {
|
||||
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
||||
// in from a frame of CGRect.zero)
|
||||
|
|
|
@ -361,10 +361,26 @@ open class Storage {
|
|||
|
||||
// MARK: - Functions
|
||||
|
||||
private static func logIfNeeded(_ error: Error, isWrite: Bool) {
|
||||
switch error {
|
||||
case DatabaseError.SQLITE_ABORT:
|
||||
let message: String = ((error as? DatabaseError)?.message ?? "Unknown")
|
||||
SNLog("[Storage] Database \(isWrite ? "write" : "read") failed due to error: \(message)")
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private static func logIfNeeded<T>(_ error: Error, isWrite: Bool) -> T? {
|
||||
logIfNeeded(error, isWrite: isWrite)
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult public final func write<T>(updates: (Database) throws -> T?) -> T? {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
|
||||
|
||||
return try? dbWriter.write(updates)
|
||||
do { return try dbWriter.write(updates) }
|
||||
catch { return Storage.logIfNeeded(error, isWrite: true) }
|
||||
}
|
||||
|
||||
open func writeAsync<T>(updates: @escaping (Database) throws -> T) {
|
||||
|
@ -377,6 +393,11 @@ open class Storage {
|
|||
dbWriter.asyncWrite(
|
||||
updates,
|
||||
completion: { db, result in
|
||||
switch result {
|
||||
case .failure(let error): Storage.logIfNeeded(error, isWrite: true)
|
||||
default: break
|
||||
}
|
||||
|
||||
try? completion(db, result)
|
||||
}
|
||||
)
|
||||
|
@ -400,7 +421,10 @@ open class Storage {
|
|||
return Deferred {
|
||||
Future { resolver in
|
||||
do { resolver(Result.success(try dbWriter.write(updates))) }
|
||||
catch { resolver(Result.failure(error)) }
|
||||
catch {
|
||||
Storage.logIfNeeded(error, isWrite: true)
|
||||
resolver(Result.failure(error))
|
||||
}
|
||||
}
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -423,7 +447,10 @@ open class Storage {
|
|||
return Deferred {
|
||||
Future { resolver in
|
||||
do { resolver(Result.success(try dbWriter.read(value))) }
|
||||
catch { resolver(Result.failure(error)) }
|
||||
catch {
|
||||
Storage.logIfNeeded(error, isWrite: false)
|
||||
resolver(Result.failure(error))
|
||||
}
|
||||
}
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -431,7 +458,8 @@ open class Storage {
|
|||
@discardableResult public final func read<T>(_ value: (Database) throws -> T?) -> T? {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
|
||||
|
||||
return try? dbWriter.read(value)
|
||||
do { return try dbWriter.read(value) }
|
||||
catch { return Storage.logIfNeeded(error, isWrite: false) }
|
||||
}
|
||||
|
||||
/// Rever to the `ValueObservation.start` method for full documentation
|
||||
|
|
|
@ -90,12 +90,10 @@ public enum AppSetup {
|
|||
// method when calling within a database read/write closure)
|
||||
Storage.shared.read { db in SessionUtil.refreshingUserConfigsEnabled(db) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))
|
||||
|
||||
// The 'if' is only there to prevent the "variable never read" warning from showing
|
||||
if backgroundTask != nil { backgroundTask = nil }
|
||||
}
|
||||
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))
|
||||
|
||||
// The 'if' is only there to prevent the "variable never read" warning from showing
|
||||
if backgroundTask != nil { backgroundTask = nil }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,15 +8,15 @@ import SignalCoreKit
|
|||
public class NoopNotificationsManager: NotificationsProtocol {
|
||||
public init() {}
|
||||
|
||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
owsFailDebug("")
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
owsFailDebug("")
|
||||
}
|
||||
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) {
|
||||
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread, applicationState: UIApplication.State) {
|
||||
owsFailDebug("")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue