From f373a989a8849efb1f13d8f131326d7773009875 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 20 Jul 2023 12:29:56 +1000 Subject: [PATCH 1/2] Fixes for user config crashes Added a method to determine if the database is suspended Updated the code to show the "Failed" state if sending a message fails due to a suspended database Prevented a crash which could occur in rare cases when accessing the Seed via the home screen prompt (direct user to share logs with session - db locked or device in an invalid state) Prevented a crash which could occur when trying to send a message due to failing to retrieve the mnemonic (db locked or device in an invalid state) Fixed a bug where optimistic messages could end up appearing multiple times Fixed a crash with the QRCode scanner Fixed a crash when trying to take a video attachment Fixed a crash where the image picker grid could go out of bounds when selecting elements Fixed a crash which could occur when a user provides a recovery password with a word that contains less than 3 characters Fixed a potential issue where the dependency injection could result in a stale date being used in some places (removing the DI here, proper fix requires larger changes in another branch) --- Session.xcodeproj/project.pbxproj | 24 ++--- .../Call Management/SessionCallManager.swift | 2 +- .../ConversationVC+Interaction.swift | 52 +++++++-- Session/Conversations/ConversationVC.swift | 9 +- .../Conversations/ConversationViewModel.swift | 102 ++++++++++++------ Session/Home/HomeVC.swift | 19 +++- .../ImagePickerController.swift | 77 ++++++++----- .../PhotoCapture.swift | 31 +++--- .../PhotoLibrary.swift | 19 ++-- .../SendMediaNavigationController.swift | 12 +++ Session/Meta/AppDelegate.swift | 14 +-- .../Translations/de.lproj/Localizable.strings | 2 + .../Translations/en.lproj/Localizable.strings | 2 + .../Translations/es.lproj/Localizable.strings | 2 + .../Translations/fa.lproj/Localizable.strings | 2 + .../Translations/fi.lproj/Localizable.strings | 2 + .../Translations/fr.lproj/Localizable.strings | 2 + .../Translations/hi.lproj/Localizable.strings | 2 + .../Translations/hr.lproj/Localizable.strings | 2 + .../id-ID.lproj/Localizable.strings | 2 + .../Translations/it.lproj/Localizable.strings | 2 + .../Translations/ja.lproj/Localizable.strings | 2 + .../Translations/nl.lproj/Localizable.strings | 2 + .../Translations/pl.lproj/Localizable.strings | 2 + .../pt_BR.lproj/Localizable.strings | 2 + .../Translations/ru.lproj/Localizable.strings | 2 + .../Translations/si.lproj/Localizable.strings | 2 + .../Translations/sk.lproj/Localizable.strings | 2 + .../Translations/sv.lproj/Localizable.strings | 2 + .../Translations/th.lproj/Localizable.strings | 2 + .../vi-VN.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + .../zh_CN.lproj/Localizable.strings | 2 + .../PushRegistrationManager.swift | 3 +- Session/Onboarding/LinkDeviceVC.swift | 3 +- Session/Onboarding/RestoreVC.swift | 2 +- Session/Onboarding/SeedVC.swift | 39 ++++++- Session/Settings/SeedModal.swift | 15 +-- Session/Settings/SettingsViewModel.swift | 17 ++- .../Shared/QRCodeScanningViewController.swift | 7 +- .../Open Groups/OpenGroupAPI.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 6 +- .../Shared Models/MessageViewModel.swift | 18 +++- .../NotificationServiceExtension.swift | 9 +- SessionShareExtension/ThreadPickerVC.swift | 6 +- SessionUtilitiesKit/Crypto/Mnemonic.swift | 36 +++---- SessionUtilitiesKit/Database/Storage.swift | 25 +++++ 47 files changed, 410 insertions(+), 183 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8dd26fc33..979c65d60 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6366,7 +6366,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6390,7 +6390,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6438,7 +6438,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6467,7 +6467,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6503,7 +6503,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6526,7 +6526,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6577,7 +6577,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6605,7 +6605,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -7537,7 +7537,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7575,7 +7575,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7608,7 +7608,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 419; + CURRENT_PROJECT_VERSION = 420; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7646,7 +7646,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 18fa498da..c89cd5e23 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -188,7 +188,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { if CurrentAppContext().isInBackground() { // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() } } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index f597cc418..e7a3889a8 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -459,15 +459,13 @@ extension ConversationVC: // Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can // use it to determine if the user is creating a new thread and update the 'isApproved' // flags appropriately - let threadId: String = self.viewModel.threadData.threadId - let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() // If this was a message request then approve it approveMessageRequestIfNeeded( - for: threadId, - threadVariant: threadVariant, + for: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, isNewThread: !oldThreadShouldBeVisible, timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting ) @@ -482,10 +480,17 @@ extension ConversationVC: quoteModel: quoteModel ) + sendMessage(optimisticData: optimisticData) + } + + private func sendMessage(optimisticData: ConversationViewModel.OptimisticMessageData) { + let threadId: String = self.viewModel.threadData.threadId + let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant + DispatchQueue.global(qos:.userInitiated).async { // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as // this can take up to 0.5s - let quoteThumbnailAttachment: Attachment? = quoteModel?.attachment?.cloneAsQuoteThumbnail() + let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail() // Actually send the message Storage.shared @@ -504,7 +509,7 @@ extension ConversationVC: // If there is a LinkPreview and it doesn't match an existing one then add it now if - let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft, + let linkPreviewDraft: LinkPreviewDraft = optimisticData.linkPreviewDraft, (try? insertedInteraction.linkPreview.isEmpty(db)) == true { try LinkPreview( @@ -515,7 +520,7 @@ extension ConversationVC: } // If there is a Quote the insert it now - if let interactionId: Int64 = insertedInteraction.id, let quoteModel: QuotedReplyModel = quoteModel { + if let interactionId: Int64 = insertedInteraction.id, let quoteModel: QuotedReplyModel = optimisticData.quoteModel { try Quote( interactionId: interactionId, authorId: quoteModel.authorId, @@ -541,7 +546,13 @@ extension ConversationVC: } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .sinkUntilComplete( - receiveCompletion: { [weak self] _ in + receiveCompletion: { [weak self] result in + switch result { + case .finished: break + case .failure(let error): + self?.viewModel.failedToStoreOptimisticOutgoingMessage(id: optimisticData.id, error: error) + } + self?.handleMessageSent() } ) @@ -1608,6 +1619,31 @@ extension ConversationVC: } func retry(_ cellViewModel: MessageViewModel) { + // If the failed message is an optimistic update then we need to do things differently + guard cellViewModel.id != MessageViewModel.optimisticUpdateId else { + guard + let optimisticMessageId: UUID = cellViewModel.optimisticMessageId, + let optimisticMessageData: ConversationViewModel.OptimisticMessageData = self.viewModel.optimisticMessageData(for: optimisticMessageId) + else { + // Show an error for the retry + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "ALERT_ERROR_TITLE".localized(), + body: .text("FAILED_TO_STORE_OUTGOING_MESSAGE".localized()), + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .alert_text + ) + ) + + self.present(modal, animated: true, completion: nil) + return + } + + // Try to send the optimistic message again + self.sendMessage(optimisticData: optimisticMessageData) + return + } + Storage.shared.writeAsync { [weak self] db in guard let threadId: String = self?.viewModel.threadData.threadId, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 895c6a9c7..fa5656fa1 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -96,14 +96,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers return margin <= ConversationVC.scrollToBottomMargin } - lazy var mnemonic: String = { - if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() { - return Mnemonic.encode(hexEncodedString: hexEncodedSeed) - } - - // Legacy account - return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString()) - }() + lazy var mnemonic: String = { ((try? SeedVC.mnemonic()) ?? "") }() // FIXME: Would be good to create a Swift-based cache and replace this lazy var mediaCache: NSCache = { diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 425db2210..0bfef4254 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -353,7 +353,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { data: updatedData, for: updatedPageInfo, optimisticMessages: (self?.optimisticallyInsertedMessages.wrappedValue.values) - .map { Array($0) }, + .map { $0.map { $0.messageViewModel } }, initialUnreadInteractionId: self?.initialUnreadInteractionId ), currentDataRetriever: { self?.interactionData }, @@ -377,8 +377,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { ) -> [SectionModel] { let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let sortedData: [MessageViewModel] = data - .appending(contentsOf: (optimisticMessages ?? [])) - .filter { !$0.cellType.isPostProcessed } + .filter { $0.id != MessageViewModel.optimisticUpdateId } // Remove old optimistic updates + .appending(contentsOf: (optimisticMessages ?? [])) // Insert latest optimistic updates + .filter { !$0.cellType.isPostProcessed } // Remove headers and other .sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs } // We load messages from newest to oldest so having a pageOffset larger than zero means @@ -459,12 +460,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { public typealias OptimisticMessageData = ( id: UUID, + messageViewModel: MessageViewModel, interaction: Interaction, attachmentData: Attachment.PreparedData?, - linkPreviewAttachment: Attachment? + linkPreviewDraft: LinkPreviewDraft?, + linkPreviewAttachment: Attachment?, + quoteModel: QuotedReplyModel? ) - private var optimisticallyInsertedMessages: Atomic<[UUID: MessageViewModel]> = Atomic([:]) + private var optimisticallyInsertedMessages: Atomic<[UUID: OptimisticMessageData]> = Atomic([:]) private var optimisticMessageAssociatedInteractionIds: Atomic<[Int64: UUID]> = Atomic([:]) public func optimisticallyAppendOutgoingMessage( @@ -507,15 +511,10 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { mimeType: OWSMimeTypeImageJpeg ) } - let optimisticData: OptimisticMessageData = ( - optimisticMessageId, - interaction, - optimisticAttachments, - linkPreviewAttachment - ) // Generate the actual 'MessageViewModel' let messageViewModel: MessageViewModel = MessageViewModel( + optimisticMessageId: optimisticMessageId, threadId: threadData.threadId, threadVariant: threadData.threadVariant, threadHasDisappearingMessagesEnabled: (threadData.disappearingMessagesConfiguration?.isEnabled ?? false), @@ -556,14 +555,67 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { linkPreviewAttachment: linkPreviewAttachment, attachments: optimisticAttachments?.attachments ) + let optimisticData: OptimisticMessageData = ( + optimisticMessageId, + messageViewModel, + interaction, + optimisticAttachments, + linkPreviewDraft, + linkPreviewAttachment, + quoteModel + ) - optimisticallyInsertedMessages.mutate { $0[optimisticMessageId] = messageViewModel } + optimisticallyInsertedMessages.mutate { $0[optimisticMessageId] = optimisticData } + forceUpdateDataIfPossible() - // If we can't get the current page data then don't bother trying to update (it's not going to work) - guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else { - return optimisticData + return optimisticData + } + + public func failedToStoreOptimisticOutgoingMessage(id: UUID, error: Error) { + optimisticallyInsertedMessages.mutate { + $0[id] = $0[id].map { + ( + $0.id, + $0.messageViewModel.with( + state: .failed, + mostRecentFailureText: "FAILED_TO_STORE_OUTGOING_MESSAGE".localized() + ), + $0.interaction, + $0.attachmentData, + $0.linkPreviewDraft, + $0.linkPreviewAttachment, + $0.quoteModel + ) + } } + forceUpdateDataIfPossible() + } + + /// Record an association between an `optimisticMessageId` and a specific `interactionId` + public func associate(optimisticMessageId: UUID, to interactionId: Int64?) { + guard let interactionId: Int64 = interactionId else { return } + + optimisticMessageAssociatedInteractionIds.mutate { $0[interactionId] = optimisticMessageId } + } + + public func optimisticMessageData(for optimisticMessageId: UUID) -> OptimisticMessageData? { + return optimisticallyInsertedMessages.wrappedValue[optimisticMessageId] + } + + /// Remove any optimisticUpdate entries which have an associated interactionId in the provided data + private func resolveOptimisticUpdates(with data: [MessageViewModel]) { + let interactionIds: [Int64] = data.map { $0.id } + let idsToRemove: [UUID] = optimisticMessageAssociatedInteractionIds + .mutate { associatedIds in interactionIds.compactMap { associatedIds.removeValue(forKey: $0) } } + + optimisticallyInsertedMessages.mutate { messages in idsToRemove.forEach { messages.removeValue(forKey: $0) } } + } + + private func forceUpdateDataIfPossible() { + // If we can't get the current page data then don't bother trying to update (it's not going to work) + guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else { return } + /// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above let currentData: [SectionModel] = (unobservedInteractionDataChanges?.0 ?? interactionData) @@ -571,7 +623,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { updatedData: process( data: (currentData.first(where: { $0.model == .messages })?.elements ?? []), for: currentPageInfo, - optimisticMessages: Array(optimisticallyInsertedMessages.wrappedValue.values), + optimisticMessages: optimisticallyInsertedMessages.wrappedValue.values.map { $0.messageViewModel }, initialUnreadInteractionId: initialUnreadInteractionId ), currentDataRetriever: { [weak self] in self?.interactionData }, @@ -583,24 +635,6 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { ) } ) - - return optimisticData - } - - /// Record an association between an `optimisticMessageId` and a specific `interactionId` - public func associate(optimisticMessageId: UUID, to interactionId: Int64?) { - guard let interactionId: Int64 = interactionId else { return } - - optimisticMessageAssociatedInteractionIds.mutate { $0[interactionId] = optimisticMessageId } - } - - /// Remove any optimisticUpdate entries which have an associated interactionId in the provided data - private func resolveOptimisticUpdates(with data: [MessageViewModel]) { - let interactionIds: [Int64] = data.map { $0.id } - let idsToRemove: [UUID] = optimisticMessageAssociatedInteractionIds - .mutate { associatedIds in interactionIds.compactMap { associatedIds.removeValue(forKey: $0) } } - - optimisticallyInsertedMessages.mutate { messages in idsToRemove.forEach { messages.removeValue(forKey: $0) } } } // MARK: - Mentions diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index c8d774f2e..50a7b6151 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -752,9 +752,22 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData // MARK: - Interaction func handleContinueButtonTapped(from seedReminderView: SeedReminderView) { - let seedVC = SeedVC() - let navigationController = StyledNavigationController(rootViewController: seedVC) - present(navigationController, animated: true, completion: nil) + let targetViewController: UIViewController = { + if let seedVC: SeedVC = try? SeedVC() { + return StyledNavigationController(rootViewController: seedVC) + } + + return ConfirmationModal( + info: ConfirmationModal.Info( + title: "ALERT_ERROR_TITLE".localized(), + body: .text("LOAD_RECOVERY_PASSWORD_ERROR".localized()), + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .alert_text + ) + ) + }() + + present(targetViewController, animated: true, completion: nil) } func show( diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index e7837f15a..37fb7f4a8 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -17,6 +17,7 @@ protocol ImagePickerGridControllerDelegate: AnyObject { var isInBatchSelectMode: Bool { get } func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool + func imagePicker(_ imagePicker: ImagePickerGridController, failedToRetrieveAssetAt index: Int, forCount count: Int) } class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate { @@ -127,31 +128,33 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } switch selectionPanGesture.state { - case .possible: - break - case .began: - collectionView.isUserInteractionEnabled = false - collectionView.isScrollEnabled = false - - let location = selectionPanGesture.location(in: collectionView) - guard let indexPath = collectionView.indexPathForItem(at: location) else { - return - } - let asset = photoCollectionContents.asset(at: indexPath.item) - if delegate.imagePicker(self, isAssetSelected: asset) { - selectionPanGestureMode = .deselect - } else { - selectionPanGestureMode = .select - } - case .changed: - let location = selectionPanGesture.location(in: collectionView) - guard let indexPath = collectionView.indexPathForItem(at: location) else { - return - } - tryToToggleBatchSelect(at: indexPath) - case .cancelled, .ended, .failed: - collectionView.isUserInteractionEnabled = true - collectionView.isScrollEnabled = true + case .possible: break + case .began: + collectionView.isUserInteractionEnabled = false + collectionView.isScrollEnabled = false + + let location = selectionPanGesture.location(in: collectionView) + guard + let indexPath: IndexPath = collectionView.indexPathForItem(at: location), + let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) + else { return } + + if delegate.imagePicker(self, isAssetSelected: asset) { + selectionPanGestureMode = .deselect + } + else { + selectionPanGestureMode = .select + } + + case .changed: + let location = selectionPanGesture.location(in: collectionView) + guard let indexPath = collectionView.indexPathForItem(at: location) else { return } + + tryToToggleBatchSelect(at: indexPath) + + case .cancelled, .ended, .failed: + collectionView.isUserInteractionEnabled = true + collectionView.isScrollEnabled = true } } @@ -171,7 +174,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - let asset = photoCollectionContents.asset(at: indexPath.item) + guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else { return } + switch selectionPanGestureMode { case .select: guard delegate.imagePickerCanSelectAdditionalItems(self) else { @@ -469,7 +473,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) + guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else { + SNLog("Failed to select cell for asset at \(indexPath.item)") + delegate.imagePicker(self, failedToRetrieveAssetAt: indexPath.item, forCount: photoCollectionContents.assetCount) + return + } + delegate.imagePicker( self, didSelectAsset: asset, @@ -490,7 +499,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - let asset = photoCollectionContents.asset(at: indexPath.item) + guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else { + SNLog("Failed to deselect cell for asset at \(indexPath.item)") + delegate.imagePicker(self, failedToRetrieveAssetAt: indexPath.item, forCount: photoCollectionContents.assetCount) + return + } + delegate.imagePicker(self, didDeselectAsset: asset) } @@ -504,7 +518,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath) - let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) + + guard let assetItem: PhotoPickerAssetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) else { + SNLog("Failed to style cell for asset at \(indexPath.item)") + return cell + } + cell.configure(item: assetItem) cell.isAccessibilityElement = true cell.accessibilityIdentifier = "\(assetItem.asset.modificationDate.map { "\($0)" } ?? "Unknown Date")" diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index 6f4687e85..a961aa339 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -351,22 +351,23 @@ extension PhotoCapture: CaptureButtonDelegate { Logger.verbose("") - Just(()) - .subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes - .sinkUntilComplete( - receiveCompletion: { [weak self] _ in - guard let strongSelf = self else { return } - - do { - try strongSelf.startAudioCapture() - strongSelf.captureOutput.beginVideo(delegate: strongSelf) - strongSelf.delegate?.photoCaptureDidBeginVideo(strongSelf) - } - catch { - strongSelf.delegate?.photoCapture(strongSelf, processingDidError: error) - } + sessionQueue.async { [weak self] in // Must run this on a specific queue to prevent crashes + guard let strongSelf = self else { return } + + do { + try strongSelf.startAudioCapture() + strongSelf.captureOutput.beginVideo(delegate: strongSelf) + + DispatchQueue.main.async { + strongSelf.delegate?.photoCaptureDidBeginVideo(strongSelf) } - ) + } + catch { + DispatchQueue.main.async { + strongSelf.delegate?.photoCapture(strongSelf, processingDidError: error) + } + } + } } func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index 84fd28be2..5bc29f7e5 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -105,28 +105,29 @@ class PhotoCollectionContents { return asset(at: 0) } - func asset(at index: Int) -> PHAsset { + func asset(at index: Int) -> PHAsset? { + guard index >= 0 && index < fetchResult.count else { return nil } + return fetchResult.object(at: index) } // MARK: - AssetItem Accessors - func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem { - let mediaAsset = asset(at: index) + func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? { + guard let mediaAsset: PHAsset = asset(at: index) else { return nil } + return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) } func firstAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? { - guard let mediaAsset = firstAsset else { - return nil - } + guard let mediaAsset = firstAsset else { return nil } + return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) } func lastAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? { - guard let mediaAsset = lastAsset else { - return nil - } + guard let mediaAsset = lastAsset else { return nil } + return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) } diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index 63bf1fde4..a9f5780d1 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -395,6 +395,18 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool { return attachmentDraftCollection.count <= SignalAttachment.maxAttachmentsAllowed } + + func imagePicker(_ imagePicker: ImagePickerGridController, failedToRetrieveAssetAt index: Int, forCount count: Int) { + let modal: ConfirmationModal = ConfirmationModal( + targetView: self.view, + info: ConfirmationModal.Info( + title: "IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS".localized(), + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .alert_text + ) + ) + self.present(modal, animated: true) + } } extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index e2afa3890..efb70ee58 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -134,8 +134,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// Apple's documentation on the matter) UNUserNotificationCenter.current().delegate = self - // Resume database - NotificationCenter.default.post(name: Database.resumeNotification, object: self) + Storage.resumeDatabaseAccess() // Reset the 'startTime' (since it would be invalid from the last launch) startTime = CACurrentMediaTime() @@ -197,7 +196,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { if !self.hasCallOngoing() { - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() } } } @@ -258,8 +257,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Background Fetching func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // Resume database - NotificationCenter.default.post(name: Database.resumeNotification, object: self) + Storage.resumeDatabaseAccess() // Background tasks only last for a certain amount of time (which can result in a crash and a // prompt appearing for the user), we want to avoid this and need to make sure to suspend the @@ -276,8 +274,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD BackgroundPoller.isValid = false if CurrentAppContext().isInBackground() { - // Suspend database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() } SNLog("Background poll failed due to manual timeout") @@ -303,8 +300,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD BackgroundPoller.isValid = false if CurrentAppContext().isInBackground() { - // Suspend database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() } cancelTimer.invalidate() diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 36e57b643..c3fda3b34 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index fb8c06328..d40fa50bd 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 26cd6fcc7..8e83718e1 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 591ed7146..7f48d7e42 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index d583693df..9167e07b6 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 562da051b..6b78436ee 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index c5837e958..4ceb134f9 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 60b5da861..1e9412ce3 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 8a45a00b0..12013921b 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 547d49c04..f301b4c65 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index e380f3ea7..e141682fa 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 8aff0e69e..6dbbda2b3 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index bc5252c48..7613d5d0c 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 16a6b845d..0dc850760 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index a5968d207..33d5a4288 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index d0f8738c0..8804247ad 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 8df3501ac..69eefbe68 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 1d3d86dff..c7d37b567 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 581fc3e1c..75612d387 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 07311c2ab..bc19cad24 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 8fd5c3e27..e80f39d42 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 7f63ffbf9..cb780247e 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -646,3 +646,5 @@ "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; +"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index d6eb260be..04d4a4e0b 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -286,8 +286,7 @@ public enum PushRegistrationError: Error { return } - // Resume database - NotificationCenter.default.post(name: Database.resumeNotification, object: self) + Storage.resumeDatabaseAccess() let maybeCall: SessionCall? = Storage.shared.write { db in let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( diff --git a/Session/Onboarding/LinkDeviceVC.swift b/Session/Onboarding/LinkDeviceVC.swift index bef5d2460..15d7b4a3a 100644 --- a/Session/Onboarding/LinkDeviceVC.swift +++ b/Session/Onboarding/LinkDeviceVC.swift @@ -312,8 +312,9 @@ private final class RecoveryPhraseVC: UIViewController { ) self.present(modal, animated: true) } - let mnemonic = mnemonicTextView.text!.lowercased() + do { + let mnemonic = (mnemonicTextView.text ?? "").lowercased() let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) let seed = Data(hex: hexEncodedSeed) mnemonicTextView.resignFirstResponder() diff --git a/Session/Onboarding/RestoreVC.swift b/Session/Onboarding/RestoreVC.swift index e196c5f06..3a272280d 100644 --- a/Session/Onboarding/RestoreVC.swift +++ b/Session/Onboarding/RestoreVC.swift @@ -205,7 +205,7 @@ final class RestoreVC: BaseVC { let keyPairs: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) do { - let mnemonic: String = mnemonicTextView.text!.lowercased() + let mnemonic: String = (mnemonicTextView.text ?? "").lowercased() let hexEncodedSeed: String = try Mnemonic.decode(mnemonic: mnemonic) seed = Data(hex: hexEncodedSeed) keyPairs = try Identity.generate(from: seed) diff --git a/Session/Onboarding/SeedVC.swift b/Session/Onboarding/SeedVC.swift index c31a42d43..45b530e7d 100644 --- a/Session/Onboarding/SeedVC.swift +++ b/Session/Onboarding/SeedVC.swift @@ -6,14 +6,35 @@ import SessionUtilitiesKit import SignalUtilitiesKit final class SeedVC: BaseVC { - private let mnemonic: String = { + public static func mnemonic() throws -> String { + let dbIsValid: Bool = Storage.shared.isValid + let dbIsSuspendedUnsafe: Bool = Storage.shared.isSuspendedUnsafe + if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() { return Mnemonic.encode(hexEncodedString: hexEncodedSeed) } + guard let legacyPrivateKey: String = Identity.fetchUserPrivateKey()?.toHexString() else { + let hasStoredPublicKey: Bool = (Identity.fetchUserPublicKey() != nil) + let hasStoredEdKeyPair: Bool = (Identity.fetchUserEd25519KeyPair() != nil) + let dbStates: [String] = [ + "dbIsValid: \(dbIsValid)", + "dbIsSuspendedUnsafe: \(dbIsSuspendedUnsafe)", + "storedSeed: false", + "userPublicKey: \(hasStoredPublicKey)", + "userPrivateKey: false", + "userEdKeyPair: \(hasStoredEdKeyPair)" + ] + + SNLog("Failed to retrieve keys for mnemonic generation (\(dbStates.joined(separator: ", ")))") + throw StorageError.objectNotFound + } + // Legacy account - return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString()) - }() + return Mnemonic.encode(hexEncodedString: legacyPrivateKey) + } + + private let mnemonic: String private lazy var redactedMnemonic: String = { if isIPhone5OrSmaller { @@ -23,6 +44,18 @@ final class SeedVC: BaseVC { return "▆▆▆▆ ▆▆▆▆▆▆ ▆▆▆ ▆▆▆▆▆▆▆ ▆▆ ▆▆▆▆ ▆▆▆ ▆▆▆▆▆ ▆▆▆ ▆ ▆▆▆▆ ▆▆ ▆▆▆▆▆▆▆ ▆▆▆▆▆ ▆▆▆▆▆▆▆▆ ▆▆ ▆▆▆ ▆▆▆▆▆▆▆" }() + // MARK: - Initialization + + init(info: String? = nil) throws { + self.mnemonic = try SeedVC.mnemonic() + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Components private lazy var seedReminderView: SeedReminderView = { diff --git a/Session/Settings/SeedModal.swift b/Session/Settings/SeedModal.swift index 7cd95456e..ce0bf4165 100644 --- a/Session/Settings/SeedModal.swift +++ b/Session/Settings/SeedModal.swift @@ -5,19 +5,14 @@ import SessionUIKit import SessionUtilitiesKit final class SeedModal: Modal { - private let mnemonic: String = { - if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() { - return Mnemonic.encode(hexEncodedString: hexEncodedSeed) - } - - // Legacy account - return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString()) - }() + private let mnemonic: String // MARK: - Initialization - override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { - super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) + init() throws { + self.mnemonic = try SeedVC.mnemonic() + + super.init(targetView: nil, dismissType: .recursive, afterClosed: nil) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index cbf08f836..d1ecec4f0 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -433,7 +433,22 @@ class SettingsViewModel: SessionTableViewModel MessageViewModel { @@ -194,9 +199,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, rawBody: self.rawBody, expiresStartedAtMs: self.expiresStartedAtMs, expiresInSeconds: self.expiresInSeconds, - state: self.state, + state: (state ?? self.state), hasAtLeastOneReadReceipt: self.hasAtLeastOneReadReceipt, - mostRecentFailureText: self.mostRecentFailureText, + mostRecentFailureText: (mostRecentFailureText ?? self.mostRecentFailureText), isSenderOpenGroupModerator: self.isSenderOpenGroupModerator, isTypingIndicator: self.isTypingIndicator, profile: self.profile, @@ -221,7 +226,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, isLast: self.isLast, isLastOutgoing: self.isLastOutgoing, currentUserBlinded15PublicKey: self.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: self.currentUserBlinded25PublicKey + currentUserBlinded25PublicKey: self.currentUserBlinded25PublicKey, + optimisticMessageId: self.optimisticMessageId ) } @@ -447,7 +453,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, isLast: isLast, isLastOutgoing: isLastOutgoing, currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: currentUserBlinded25PublicKey + currentUserBlinded25PublicKey: currentUserBlinded25PublicKey, + optimisticMessageId: self.optimisticMessageId ) } } @@ -607,10 +614,12 @@ public extension MessageViewModel { self.isLastOutgoing = isLastOutgoing self.currentUserBlinded15PublicKey = nil self.currentUserBlinded25PublicKey = nil + self.optimisticMessageId = nil } /// This init method is only used for optimistic outgoing messages init( + optimisticMessageId: UUID, threadId: String, threadVariant: SessionThread.Variant, threadHasDisappearingMessagesEnabled: Bool, @@ -686,6 +695,7 @@ public extension MessageViewModel { self.isLastOutgoing = false self.currentUserBlinded15PublicKey = nil self.currentUserBlinded25PublicKey = nil + self.optimisticMessageId = optimisticMessageId } } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 2db3e27ba..6237975be 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -25,8 +25,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request - // Resume database - NotificationCenter.default.post(name: Database.resumeNotification, object: self) + Storage.resumeDatabaseAccess() guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return self.completeSilenty() @@ -289,8 +288,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func completeSilenty() { SNLog("Complete silenty") - // Suspend the database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() self.contentHandler!(.init()) } @@ -354,8 +352,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } private func handleFailure(for content: UNMutableNotificationContent) { - // Suspend the database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() content.body = "You've got a new message" content.title = "Session" diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 4728da4d3..09a58d228 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -195,8 +195,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareNavController?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in - // Resume database - NotificationCenter.default.post(name: Database.resumeNotification, object: self) + Storage.resumeDatabaseAccess() Storage.shared .writePublisher { db -> MessageSender.PreparedSendData in @@ -272,8 +271,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .receive(on: DispatchQueue.main) .sinkUntilComplete( receiveCompletion: { [weak self] result in - // Suspend the database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + Storage.suspendDatabaseAccess() activityIndicator.dismiss { } switch result { diff --git a/SessionUtilitiesKit/Crypto/Mnemonic.swift b/SessionUtilitiesKit/Crypto/Mnemonic.swift index a02c31940..5cabefc7e 100644 --- a/SessionUtilitiesKit/Crypto/Mnemonic.swift +++ b/SessionUtilitiesKit/Crypto/Mnemonic.swift @@ -23,7 +23,7 @@ public enum Mnemonic { public struct Language: Hashable { fileprivate let filename: String - fileprivate let prefixLength: UInt + fileprivate let prefixLength: Int public static let english = Language(filename: "english", prefixLength: 3) public static let japanese = Language(filename: "japanese", prefixLength: 3) @@ -33,7 +33,7 @@ public enum Mnemonic { private static var wordSetCache: [Language: [String]] = [:] private static var truncatedWordSetCache: [Language: [String]] = [:] - private init(filename: String, prefixLength: UInt) { + private init(filename: String, prefixLength: Int) { self.filename = filename self.prefixLength = prefixLength } @@ -56,7 +56,7 @@ public enum Mnemonic { return cachedResult } - let result = loadWordSet().map { $0.prefix(length: prefixLength) } + let result = loadWordSet().map { String($0.prefix(prefixLength)) } Language.truncatedWordSetCache[self] = result return result @@ -116,9 +116,9 @@ public enum Mnemonic { } public static func decode(mnemonic: String, language: Language = .english) throws -> String { - var words = mnemonic.split(separator: " ").map { String($0) } - let truncatedWordSet = language.loadTruncatedWordSet() - let prefixLength = language.prefixLength + var words: [String] = mnemonic.split(separator: " ").map { String($0) } + let truncatedWordSet: [String] = language.loadTruncatedWordSet() + let prefixLength: Int = language.prefixLength var result = "" let n = truncatedWordSet.count @@ -131,9 +131,12 @@ public enum Mnemonic { // Decode for chunkStartIndex in stride(from: 0, to: words.count, by: 3) { - guard let w1 = truncatedWordSet.firstIndex(of: words[chunkStartIndex].prefix(length: prefixLength)), - let w2 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 1].prefix(length: prefixLength)), - let w3 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 2].prefix(length: prefixLength)) else { throw DecodingError.invalidWord } + guard + let w1 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex].prefix(prefixLength))), + let w2 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex + 1].prefix(prefixLength))), + let w3 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex + 2].prefix(prefixLength))) + else { throw DecodingError.invalidWord } + let x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n) guard x % n == w1 else { throw DecodingError.generic } let string = "0000000" + String(x, radix: 16) @@ -143,7 +146,10 @@ public enum Mnemonic { // Verify checksum let checksumIndex = determineChecksumIndex(for: words, prefixLength: prefixLength) let expectedChecksumWord = words[checksumIndex] - guard expectedChecksumWord.prefix(length: prefixLength) == checksumWord.prefix(length: prefixLength) else { throw DecodingError.verificationFailed } + + guard expectedChecksumWord.prefix(prefixLength) == checksumWord.prefix(prefixLength) else { + throw DecodingError.verificationFailed + } // Return return result @@ -162,15 +168,9 @@ public enum Mnemonic { return String(p1 + p2 + p3 + p4) } - private static func determineChecksumIndex(for x: [String], prefixLength: UInt) -> Int { - let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8)) + private static func determineChecksumIndex(for x: [String], prefixLength: Int) -> Int { + let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(prefixLength) }.joined().utf8)) return Int(checksum) % x.count } } - -private extension String { - func prefix(length: UInt) -> String { - return String(self[startIndex.. Date: Mon, 24 Jul 2023 12:30:30 +1000 Subject: [PATCH 2/2] Updated build and version numbers --- Session.xcodeproj/project.pbxproj | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 979c65d60..12341133d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6366,7 +6366,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6390,7 +6390,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6438,7 +6438,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6467,7 +6467,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6503,7 +6503,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6526,7 +6526,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6577,7 +6577,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6605,7 +6605,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -7537,7 +7537,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7575,7 +7575,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7608,7 +7608,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 420; + CURRENT_PROJECT_VERSION = 421; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7646,7 +7646,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session;