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)
This commit is contained in:
parent
6ba9d1df89
commit
f373a989a8
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<NSString, AnyObject> = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
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
|
||||
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")"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -433,7 +433,22 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
),
|
||||
title: "vc_settings_recovery_phrase_button_title".localized(),
|
||||
onTap: {
|
||||
self?.transitionToScreen(SeedModal(), transitionType: .present)
|
||||
let targetViewController: UIViewController = {
|
||||
if let modal: SeedModal = try? SeedModal() {
|
||||
return modal
|
||||
}
|
||||
|
||||
return ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "ALERT_ERROR_TITLE".localized(),
|
||||
body: .text("LOAD_RECOVERY_PASSWORD_ERROR".localized()),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
)
|
||||
}()
|
||||
|
||||
self?.transitionToScreen(targetViewController, transitionType: .present)
|
||||
}
|
||||
),
|
||||
SessionCell.Info(
|
||||
|
|
|
@ -110,7 +110,12 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
|
|||
|
||||
// Set the input device to autoFocus (since we don't have the interaction setup for
|
||||
// doing it manually)
|
||||
maybeDevice?.focusMode = .continuousAutoFocus
|
||||
do {
|
||||
try maybeDevice?.lockForConfiguration()
|
||||
maybeDevice?.focusMode = .continuousAutoFocus
|
||||
maybeDevice?.unlockForConfiguration()
|
||||
}
|
||||
catch {}
|
||||
|
||||
// Device input
|
||||
guard
|
||||
|
|
|
@ -1283,7 +1283,7 @@ public enum OpenGroupAPI {
|
|||
let path: String = url.path
|
||||
.appending(url.query.map { value in "?\(value)" })
|
||||
let method: String = (request.httpMethod ?? "GET")
|
||||
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
|
||||
let timestamp: Int = Int(floor(Date().timeIntervalSince1970))
|
||||
let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
|
||||
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
|
||||
|
||||
|
|
|
@ -36,8 +36,8 @@ public final class OpenGroupManager {
|
|||
return .greatestFiniteMagnitude
|
||||
}
|
||||
|
||||
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen)
|
||||
return dependencies.date.timeIntervalSince(lastOpen)
|
||||
_timeSinceLastOpen = Date().timeIntervalSince(lastOpen)
|
||||
return Date().timeIntervalSince(lastOpen)
|
||||
}
|
||||
|
||||
public var pendingChanges: [OpenGroupAPI.PendingChange] = []
|
||||
|
@ -1129,7 +1129,7 @@ public final class OpenGroupManager {
|
|||
// there is one.
|
||||
let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: server)
|
||||
let lastOpenGroupImageUpdate: Date? = dependencies.standardUserDefaults[.lastOpenGroupImageUpdate]
|
||||
let now: Date = dependencies.date
|
||||
let now: Date = Date()
|
||||
let timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
|
||||
let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
|
||||
let canUseExistingImage: Bool = (
|
||||
|
|
|
@ -167,10 +167,15 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
|
||||
/// This is the users blinded25 key (will only be set for messages within open groups)
|
||||
public let currentUserBlinded25PublicKey: String?
|
||||
|
||||
/// This is a temporary id used before an outgoing message is persisted into the database
|
||||
public let optimisticMessageId: UUID?
|
||||
|
||||
// MARK: - Mutation
|
||||
|
||||
public func with(
|
||||
state: RecipientState.State? = nil, // Optimistic outgoing messages
|
||||
mostRecentFailureText: String? = nil, // Optimistic outgoing messages
|
||||
attachments: [Attachment]? = nil,
|
||||
reactionInfo: [ReactionInfo]? = nil
|
||||
) -> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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..<index(startIndex, offsetBy: Int(length))])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ open class Storage {
|
|||
|
||||
public static let shared: Storage = Storage()
|
||||
public private(set) var isValid: Bool = false
|
||||
|
||||
/// This property gets set when triggering the suspend/resume notifications for the database but `GRDB` will attempt to
|
||||
/// resume the suspention when it attempts to perform a write so it's possible for this to return a **false-positive** so
|
||||
/// this should be taken into consideration when used
|
||||
public private(set) var isSuspendedUnsafe: Bool = false
|
||||
|
||||
public var hasCompletedMigrations: Bool { migrationsCompleted.wrappedValue }
|
||||
public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? {
|
||||
internalCurrentlyRunningMigration.wrappedValue
|
||||
|
@ -353,6 +359,25 @@ open class Storage {
|
|||
|
||||
// MARK: - File Management
|
||||
|
||||
/// In order to avoid the `0xdead10cc` exception when accessing the database while another target is accessing it we call
|
||||
/// the experimental `Database.suspendNotification` notification (and store the current suspended state) to prevent
|
||||
/// `GRDB` from trying to access the locked database file
|
||||
///
|
||||
/// The generally suggested approach is to avoid this entirely by not storing the database in an AppGroup folder and sharing it
|
||||
/// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the
|
||||
/// database and other files into the App folder
|
||||
public static func suspendDatabaseAccess(using dependencies: Dependencies = Dependencies()) {
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
dependencies.storage.isSuspendedUnsafe = true
|
||||
}
|
||||
|
||||
/// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()`
|
||||
/// above for more information
|
||||
public static func resumeDatabaseAccess(using dependencies: Dependencies = Dependencies()) {
|
||||
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
|
||||
dependencies.storage.isSuspendedUnsafe = false
|
||||
}
|
||||
|
||||
public static func resetAllStorage() {
|
||||
// Just in case they haven't been removed for some reason, delete the legacy database & keys
|
||||
SUKLegacy.clearLegacyDatabaseInstance()
|
||||
|
|
Loading…
Reference in New Issue