Merge pull request #868 from mpretty-cyro/fix/user-config-crashes

Fixes for user config crashes
This commit is contained in:
Morgan Pretty 2023-07-24 12:55:06 +10:00 committed by GitHub
commit 8a47adfe79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 403 additions and 186 deletions

View File

@ -6366,7 +6366,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6390,7 +6390,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -6438,7 +6438,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6467,7 +6467,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -6503,7 +6503,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6526,7 +6526,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6577,7 +6577,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6605,7 +6605,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7537,7 +7537,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7575,7 +7575,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7608,7 +7608,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 420; CURRENT_PROJECT_VERSION = 421;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7646,7 +7646,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.3.1; MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

View File

@ -188,7 +188,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
if CurrentAppContext().isInBackground() { if CurrentAppContext().isInBackground() {
// Stop all jobs except for message sending and when completed suspend the database // Stop all jobs except for message sending and when completed suspend the database
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) {
NotificationCenter.default.post(name: Database.suspendNotification, object: self) Storage.suspendDatabaseAccess()
} }
} }
} }

View File

@ -459,15 +459,13 @@ extension ConversationVC:
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can // 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' // use it to determine if the user is creating a new thread and update the 'isApproved'
// flags appropriately // 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 oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
// If this was a message request then approve it // If this was a message request then approve it
approveMessageRequestIfNeeded( approveMessageRequestIfNeeded(
for: threadId, for: self.viewModel.threadData.threadId,
threadVariant: threadVariant, threadVariant: self.viewModel.threadData.threadVariant,
isNewThread: !oldThreadShouldBeVisible, isNewThread: !oldThreadShouldBeVisible,
timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting
) )
@ -482,10 +480,17 @@ extension ConversationVC:
quoteModel: quoteModel 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 { DispatchQueue.global(qos:.userInitiated).async {
// Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as
// this can take up to 0.5s // this can take up to 0.5s
let quoteThumbnailAttachment: Attachment? = quoteModel?.attachment?.cloneAsQuoteThumbnail() let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail()
// Actually send the message // Actually send the message
Storage.shared 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 there is a LinkPreview and it doesn't match an existing one then add it now
if if
let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft, let linkPreviewDraft: LinkPreviewDraft = optimisticData.linkPreviewDraft,
(try? insertedInteraction.linkPreview.isEmpty(db)) == true (try? insertedInteraction.linkPreview.isEmpty(db)) == true
{ {
try LinkPreview( try LinkPreview(
@ -515,7 +520,7 @@ extension ConversationVC:
} }
// If there is a Quote the insert it now // 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( try Quote(
interactionId: interactionId, interactionId: interactionId,
authorId: quoteModel.authorId, authorId: quoteModel.authorId,
@ -541,7 +546,13 @@ extension ConversationVC:
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete( .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() self?.handleMessageSent()
} }
) )
@ -1608,6 +1619,31 @@ extension ConversationVC:
} }
func retry(_ cellViewModel: MessageViewModel) { 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 Storage.shared.writeAsync { [weak self] db in
guard guard
let threadId: String = self?.viewModel.threadData.threadId, let threadId: String = self?.viewModel.threadData.threadId,

View File

@ -96,14 +96,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
return margin <= ConversationVC.scrollToBottomMargin return margin <= ConversationVC.scrollToBottomMargin
} }
lazy var mnemonic: String = { lazy var mnemonic: String = { ((try? SeedVC.mnemonic()) ?? "") }()
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
// FIXME: Would be good to create a Swift-based cache and replace this // FIXME: Would be good to create a Swift-based cache and replace this
lazy var mediaCache: NSCache<NSString, AnyObject> = { lazy var mediaCache: NSCache<NSString, AnyObject> = {

View File

@ -353,7 +353,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
data: updatedData, data: updatedData,
for: updatedPageInfo, for: updatedPageInfo,
optimisticMessages: (self?.optimisticallyInsertedMessages.wrappedValue.values) optimisticMessages: (self?.optimisticallyInsertedMessages.wrappedValue.values)
.map { Array($0) }, .map { $0.map { $0.messageViewModel } },
initialUnreadInteractionId: self?.initialUnreadInteractionId initialUnreadInteractionId: self?.initialUnreadInteractionId
), ),
currentDataRetriever: { self?.interactionData }, currentDataRetriever: { self?.interactionData },
@ -377,8 +377,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
) -> [SectionModel] { ) -> [SectionModel] {
let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true })
let sortedData: [MessageViewModel] = data let sortedData: [MessageViewModel] = data
.appending(contentsOf: (optimisticMessages ?? [])) .filter { $0.id != MessageViewModel.optimisticUpdateId } // Remove old optimistic updates
.filter { !$0.cellType.isPostProcessed } .appending(contentsOf: (optimisticMessages ?? [])) // Insert latest optimistic updates
.filter { !$0.cellType.isPostProcessed } // Remove headers and other
.sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs } .sorted { lhs, rhs -> Bool in lhs.timestampMs < rhs.timestampMs }
// We load messages from newest to oldest so having a pageOffset larger than zero means // 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 = ( public typealias OptimisticMessageData = (
id: UUID, id: UUID,
messageViewModel: MessageViewModel,
interaction: Interaction, interaction: Interaction,
attachmentData: Attachment.PreparedData?, 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([:]) private var optimisticMessageAssociatedInteractionIds: Atomic<[Int64: UUID]> = Atomic([:])
public func optimisticallyAppendOutgoingMessage( public func optimisticallyAppendOutgoingMessage(
@ -507,15 +511,10 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
mimeType: OWSMimeTypeImageJpeg mimeType: OWSMimeTypeImageJpeg
) )
} }
let optimisticData: OptimisticMessageData = (
optimisticMessageId,
interaction,
optimisticAttachments,
linkPreviewAttachment
)
// Generate the actual 'MessageViewModel' // Generate the actual 'MessageViewModel'
let messageViewModel: MessageViewModel = MessageViewModel( let messageViewModel: MessageViewModel = MessageViewModel(
optimisticMessageId: optimisticMessageId,
threadId: threadData.threadId, threadId: threadData.threadId,
threadVariant: threadData.threadVariant, threadVariant: threadData.threadVariant,
threadHasDisappearingMessagesEnabled: (threadData.disappearingMessagesConfiguration?.isEnabled ?? false), threadHasDisappearingMessagesEnabled: (threadData.disappearingMessagesConfiguration?.isEnabled ?? false),
@ -556,14 +555,67 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
linkPreviewAttachment: linkPreviewAttachment, linkPreviewAttachment: linkPreviewAttachment,
attachments: optimisticAttachments?.attachments 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) return optimisticData
guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else { }
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 /// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above
let currentData: [SectionModel] = (unobservedInteractionDataChanges?.0 ?? interactionData) let currentData: [SectionModel] = (unobservedInteractionDataChanges?.0 ?? interactionData)
@ -571,7 +623,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
updatedData: process( updatedData: process(
data: (currentData.first(where: { $0.model == .messages })?.elements ?? []), data: (currentData.first(where: { $0.model == .messages })?.elements ?? []),
for: currentPageInfo, for: currentPageInfo,
optimisticMessages: Array(optimisticallyInsertedMessages.wrappedValue.values), optimisticMessages: optimisticallyInsertedMessages.wrappedValue.values.map { $0.messageViewModel },
initialUnreadInteractionId: initialUnreadInteractionId initialUnreadInteractionId: initialUnreadInteractionId
), ),
currentDataRetriever: { [weak self] in self?.interactionData }, 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 // MARK: - Mentions

View File

@ -752,9 +752,22 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
// MARK: - Interaction // MARK: - Interaction
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) { func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
let seedVC = SeedVC() let targetViewController: UIViewController = {
let navigationController = StyledNavigationController(rootViewController: seedVC) if let seedVC: SeedVC = try? SeedVC() {
present(navigationController, animated: true, completion: nil) 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( func show(

View File

@ -17,6 +17,7 @@ protocol ImagePickerGridControllerDelegate: AnyObject {
var isInBatchSelectMode: Bool { get } var isInBatchSelectMode: Bool { get }
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool
func imagePicker(_ imagePicker: ImagePickerGridController, failedToRetrieveAssetAt index: Int, forCount count: Int)
} }
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate { class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate {
@ -127,31 +128,33 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
switch selectionPanGesture.state { switch selectionPanGesture.state {
case .possible: case .possible: break
break case .began:
case .began: collectionView.isUserInteractionEnabled = false
collectionView.isUserInteractionEnabled = false collectionView.isScrollEnabled = false
collectionView.isScrollEnabled = false
let location = selectionPanGesture.location(in: collectionView) let location = selectionPanGesture.location(in: collectionView)
guard let indexPath = collectionView.indexPathForItem(at: location) else { guard
return let indexPath: IndexPath = collectionView.indexPathForItem(at: location),
} let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item)
let asset = photoCollectionContents.asset(at: indexPath.item) else { return }
if delegate.imagePicker(self, isAssetSelected: asset) {
selectionPanGestureMode = .deselect if delegate.imagePicker(self, isAssetSelected: asset) {
} else { selectionPanGestureMode = .deselect
selectionPanGestureMode = .select }
} else {
case .changed: selectionPanGestureMode = .select
let location = selectionPanGesture.location(in: collectionView) }
guard let indexPath = collectionView.indexPathForItem(at: location) else {
return case .changed:
} let location = selectionPanGesture.location(in: collectionView)
tryToToggleBatchSelect(at: indexPath) guard let indexPath = collectionView.indexPathForItem(at: location) else { return }
case .cancelled, .ended, .failed:
collectionView.isUserInteractionEnabled = true tryToToggleBatchSelect(at: indexPath)
collectionView.isScrollEnabled = true
case .cancelled, .ended, .failed:
collectionView.isUserInteractionEnabled = true
collectionView.isScrollEnabled = true
} }
} }
@ -171,7 +174,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return return
} }
let asset = photoCollectionContents.asset(at: indexPath.item) guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else { return }
switch selectionPanGestureMode { switch selectionPanGestureMode {
case .select: case .select:
guard delegate.imagePickerCanSelectAdditionalItems(self) else { guard delegate.imagePickerCanSelectAdditionalItems(self) else {
@ -469,7 +473,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return 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( delegate.imagePicker(
self, self,
didSelectAsset: asset, didSelectAsset: asset,
@ -490,7 +499,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return 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) delegate.imagePicker(self, didDeselectAsset: asset)
} }
@ -504,7 +518,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath) 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.configure(item: assetItem)
cell.isAccessibilityElement = true cell.isAccessibilityElement = true
cell.accessibilityIdentifier = "\(assetItem.asset.modificationDate.map { "\($0)" } ?? "Unknown Date")" cell.accessibilityIdentifier = "\(assetItem.asset.modificationDate.map { "\($0)" } ?? "Unknown Date")"

View File

@ -351,27 +351,23 @@ extension PhotoCapture: CaptureButtonDelegate {
Logger.verbose("") Logger.verbose("")
Just(()) sessionQueue.async { [weak self] in // Must run this on a specific queue to prevent crashes
.subscribe(on: sessionQueue) // Must run this on a specific queue to prevent crashes guard let strongSelf = self else { return }
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
guard let strongSelf = self else { return }
do { do {
try strongSelf.startAudioCapture() try strongSelf.startAudioCapture()
strongSelf.captureOutput.beginVideo(delegate: strongSelf) strongSelf.captureOutput.beginVideo(delegate: strongSelf)
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.delegate?.photoCaptureDidBeginVideo(strongSelf) strongSelf.delegate?.photoCaptureDidBeginVideo(strongSelf)
}
}
catch {
DispatchQueue.main.async {
strongSelf.delegate?.photoCapture(strongSelf, processingDidError: error)
}
}
} }
) }
catch {
DispatchQueue.main.async {
strongSelf.delegate?.photoCapture(strongSelf, processingDidError: error)
}
}
}
} }
func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) {

View File

@ -105,28 +105,29 @@ class PhotoCollectionContents {
return asset(at: 0) 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) return fetchResult.object(at: index)
} }
// MARK: - AssetItem Accessors // MARK: - AssetItem Accessors
func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem { func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? {
let mediaAsset = asset(at: index) guard let mediaAsset: PHAsset = asset(at: index) else { return nil }
return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize)
} }
func firstAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? { func firstAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? {
guard let mediaAsset = firstAsset else { guard let mediaAsset = firstAsset else { return nil }
return nil
}
return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize)
} }
func lastAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? { func lastAssetItem(photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem? {
guard let mediaAsset = lastAsset else { guard let mediaAsset = lastAsset else { return nil }
return nil
}
return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize) return PhotoPickerAssetItem(asset: mediaAsset, photoCollectionContents: self, photoMediaSize: photoMediaSize)
} }

View File

@ -395,6 +395,18 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool { func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool {
return attachmentDraftCollection.count <= SignalAttachment.maxAttachmentsAllowed 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 { extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate {

View File

@ -134,8 +134,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// Apple's documentation on the matter) /// Apple's documentation on the matter)
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
// Resume database Storage.resumeDatabaseAccess()
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
// Reset the 'startTime' (since it would be invalid from the last launch) // Reset the 'startTime' (since it would be invalid from the last launch)
startTime = CACurrentMediaTime() startTime = CACurrentMediaTime()
@ -197,7 +196,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Stop all jobs except for message sending and when completed suspend the database // Stop all jobs except for message sending and when completed suspend the database
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) {
if !self.hasCallOngoing() { 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 // MARK: - Background Fetching
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Resume database Storage.resumeDatabaseAccess()
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
// Background tasks only last for a certain amount of time (which can result in a crash and a // 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 // 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 BackgroundPoller.isValid = false
if CurrentAppContext().isInBackground() { if CurrentAppContext().isInBackground() {
// Suspend database Storage.suspendDatabaseAccess()
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
} }
SNLog("Background poll failed due to manual timeout") SNLog("Background poll failed due to manual timeout")
@ -303,8 +300,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
BackgroundPoller.isValid = false BackgroundPoller.isValid = false
if CurrentAppContext().isInBackground() { if CurrentAppContext().isInBackground() {
// Suspend database Storage.suspendDatabaseAccess()
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
} }
cancelTimer.invalidate() cancelTimer.invalidate()

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -646,3 +646,5 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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."; "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.";

View File

@ -286,8 +286,7 @@ public enum PushRegistrationError: Error {
return return
} }
// Resume database Storage.resumeDatabaseAccess()
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
let maybeCall: SessionCall? = Storage.shared.write { db in let maybeCall: SessionCall? = Storage.shared.write { db in
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(

View File

@ -312,8 +312,9 @@ private final class RecoveryPhraseVC: UIViewController {
) )
self.present(modal, animated: true) self.present(modal, animated: true)
} }
let mnemonic = mnemonicTextView.text!.lowercased()
do { do {
let mnemonic = (mnemonicTextView.text ?? "").lowercased()
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
let seed = Data(hex: hexEncodedSeed) let seed = Data(hex: hexEncodedSeed)
mnemonicTextView.resignFirstResponder() mnemonicTextView.resignFirstResponder()

View File

@ -205,7 +205,7 @@ final class RestoreVC: BaseVC {
let keyPairs: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) let keyPairs: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)
do { do {
let mnemonic: String = mnemonicTextView.text!.lowercased() let mnemonic: String = (mnemonicTextView.text ?? "").lowercased()
let hexEncodedSeed: String = try Mnemonic.decode(mnemonic: mnemonic) let hexEncodedSeed: String = try Mnemonic.decode(mnemonic: mnemonic)
seed = Data(hex: hexEncodedSeed) seed = Data(hex: hexEncodedSeed)
keyPairs = try Identity.generate(from: seed) keyPairs = try Identity.generate(from: seed)

View File

@ -6,14 +6,35 @@ import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
final class SeedVC: BaseVC { 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() { if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed) 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 // Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString()) return Mnemonic.encode(hexEncodedString: legacyPrivateKey)
}() }
private let mnemonic: String
private lazy var redactedMnemonic: String = { private lazy var redactedMnemonic: String = {
if isIPhone5OrSmaller { if isIPhone5OrSmaller {
@ -23,6 +44,18 @@ final class SeedVC: BaseVC {
return "▆▆▆▆ ▆▆▆▆▆▆ ▆▆▆ ▆▆▆▆▆▆▆ ▆▆ ▆▆▆▆ ▆▆▆ ▆▆▆▆▆ ▆▆▆ ▆ ▆▆▆▆ ▆▆ ▆▆▆▆▆▆▆ ▆▆▆▆▆ ▆▆▆▆▆▆▆▆ ▆▆ ▆▆▆ ▆▆▆▆▆▆▆" 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 // MARK: - Components
private lazy var seedReminderView: SeedReminderView = { private lazy var seedReminderView: SeedReminderView = {

View File

@ -5,19 +5,14 @@ import SessionUIKit
import SessionUtilitiesKit import SessionUtilitiesKit
final class SeedModal: Modal { final class SeedModal: Modal {
private let mnemonic: String = { private let mnemonic: String
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
// MARK: - Initialization // MARK: - Initialization
override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { init() throws {
super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.mnemonic = try SeedVC.mnemonic()
super.init(targetView: nil, dismissType: .recursive, afterClosed: nil)
self.modalPresentationStyle = .overFullScreen self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve self.modalTransitionStyle = .crossDissolve

View File

@ -433,7 +433,22 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
), ),
title: "vc_settings_recovery_phrase_button_title".localized(), title: "vc_settings_recovery_phrase_button_title".localized(),
onTap: { 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( SessionCell.Info(

View File

@ -1283,7 +1283,7 @@ public enum OpenGroupAPI {
let path: String = url.path let path: String = url.path
.appending(url.query.map { value in "?\(value)" }) .appending(url.query.map { value in "?\(value)" })
let method: String = (request.httpMethod ?? "GET") 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 nonce: Data = Data(dependencies.nonceGenerator16.nonce())
let serverPublicKeyData: Data = Data(hex: serverPublicKey) let serverPublicKeyData: Data = Data(hex: serverPublicKey)

View File

@ -36,8 +36,8 @@ public final class OpenGroupManager {
return .greatestFiniteMagnitude return .greatestFiniteMagnitude
} }
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen) _timeSinceLastOpen = Date().timeIntervalSince(lastOpen)
return dependencies.date.timeIntervalSince(lastOpen) return Date().timeIntervalSince(lastOpen)
} }
public var pendingChanges: [OpenGroupAPI.PendingChange] = [] public var pendingChanges: [OpenGroupAPI.PendingChange] = []
@ -1129,7 +1129,7 @@ public final class OpenGroupManager {
// there is one. // there is one.
let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: server) let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: server)
let lastOpenGroupImageUpdate: Date? = dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] 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 timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
let updateInterval: TimeInterval = (7 * 24 * 60 * 60) let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
let canUseExistingImage: Bool = ( let canUseExistingImage: Bool = (

View File

@ -168,9 +168,14 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
/// This is the users blinded25 key (will only be set for messages within open groups) /// This is the users blinded25 key (will only be set for messages within open groups)
public let currentUserBlinded25PublicKey: String? 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 // MARK: - Mutation
public func with( public func with(
state: RecipientState.State? = nil, // Optimistic outgoing messages
mostRecentFailureText: String? = nil, // Optimistic outgoing messages
attachments: [Attachment]? = nil, attachments: [Attachment]? = nil,
reactionInfo: [ReactionInfo]? = nil reactionInfo: [ReactionInfo]? = nil
) -> MessageViewModel { ) -> MessageViewModel {
@ -194,9 +199,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
rawBody: self.rawBody, rawBody: self.rawBody,
expiresStartedAtMs: self.expiresStartedAtMs, expiresStartedAtMs: self.expiresStartedAtMs,
expiresInSeconds: self.expiresInSeconds, expiresInSeconds: self.expiresInSeconds,
state: self.state, state: (state ?? self.state),
hasAtLeastOneReadReceipt: self.hasAtLeastOneReadReceipt, hasAtLeastOneReadReceipt: self.hasAtLeastOneReadReceipt,
mostRecentFailureText: self.mostRecentFailureText, mostRecentFailureText: (mostRecentFailureText ?? self.mostRecentFailureText),
isSenderOpenGroupModerator: self.isSenderOpenGroupModerator, isSenderOpenGroupModerator: self.isSenderOpenGroupModerator,
isTypingIndicator: self.isTypingIndicator, isTypingIndicator: self.isTypingIndicator,
profile: self.profile, profile: self.profile,
@ -221,7 +226,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
isLast: self.isLast, isLast: self.isLast,
isLastOutgoing: self.isLastOutgoing, isLastOutgoing: self.isLastOutgoing,
currentUserBlinded15PublicKey: self.currentUserBlinded15PublicKey, currentUserBlinded15PublicKey: self.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: self.currentUserBlinded25PublicKey currentUserBlinded25PublicKey: self.currentUserBlinded25PublicKey,
optimisticMessageId: self.optimisticMessageId
) )
} }
@ -447,7 +453,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
isLast: isLast, isLast: isLast,
isLastOutgoing: isLastOutgoing, isLastOutgoing: isLastOutgoing,
currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, currentUserBlinded15PublicKey: currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: currentUserBlinded25PublicKey currentUserBlinded25PublicKey: currentUserBlinded25PublicKey,
optimisticMessageId: self.optimisticMessageId
) )
} }
} }
@ -607,10 +614,12 @@ public extension MessageViewModel {
self.isLastOutgoing = isLastOutgoing self.isLastOutgoing = isLastOutgoing
self.currentUserBlinded15PublicKey = nil self.currentUserBlinded15PublicKey = nil
self.currentUserBlinded25PublicKey = nil self.currentUserBlinded25PublicKey = nil
self.optimisticMessageId = nil
} }
/// This init method is only used for optimistic outgoing messages /// This init method is only used for optimistic outgoing messages
init( init(
optimisticMessageId: UUID,
threadId: String, threadId: String,
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
threadHasDisappearingMessagesEnabled: Bool, threadHasDisappearingMessagesEnabled: Bool,
@ -686,6 +695,7 @@ public extension MessageViewModel {
self.isLastOutgoing = false self.isLastOutgoing = false
self.currentUserBlinded15PublicKey = nil self.currentUserBlinded15PublicKey = nil
self.currentUserBlinded25PublicKey = nil self.currentUserBlinded25PublicKey = nil
self.optimisticMessageId = optimisticMessageId
} }
} }

View File

@ -25,8 +25,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
self.contentHandler = contentHandler self.contentHandler = contentHandler
self.request = request self.request = request
// Resume database Storage.resumeDatabaseAccess()
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return self.completeSilenty() return self.completeSilenty()
@ -289,8 +288,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private func completeSilenty() { private func completeSilenty() {
SNLog("Complete silenty") SNLog("Complete silenty")
// Suspend the database Storage.suspendDatabaseAccess()
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
self.contentHandler!(.init()) self.contentHandler!(.init())
} }
@ -354,8 +352,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
} }
private func handleFailure(for content: UNMutableNotificationContent) { private func handleFailure(for content: UNMutableNotificationContent) {
// Suspend the database Storage.suspendDatabaseAccess()
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
content.body = "You've got a new message" content.body = "You've got a new message"
content.title = "Session" content.title = "Session"

View File

@ -195,8 +195,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
shareNavController?.dismiss(animated: true, completion: nil) shareNavController?.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
// Resume database Storage.resumeDatabaseAccess()
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
Storage.shared Storage.shared
.writePublisher { db -> MessageSender.PreparedSendData in .writePublisher { db -> MessageSender.PreparedSendData in
@ -272,8 +271,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in
// Suspend the database Storage.suspendDatabaseAccess()
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { } activityIndicator.dismiss { }
switch result { switch result {

View File

@ -23,7 +23,7 @@ public enum Mnemonic {
public struct Language: Hashable { public struct Language: Hashable {
fileprivate let filename: String fileprivate let filename: String
fileprivate let prefixLength: UInt fileprivate let prefixLength: Int
public static let english = Language(filename: "english", prefixLength: 3) public static let english = Language(filename: "english", prefixLength: 3)
public static let japanese = Language(filename: "japanese", 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 wordSetCache: [Language: [String]] = [:]
private static var truncatedWordSetCache: [Language: [String]] = [:] private static var truncatedWordSetCache: [Language: [String]] = [:]
private init(filename: String, prefixLength: UInt) { private init(filename: String, prefixLength: Int) {
self.filename = filename self.filename = filename
self.prefixLength = prefixLength self.prefixLength = prefixLength
} }
@ -56,7 +56,7 @@ public enum Mnemonic {
return cachedResult return cachedResult
} }
let result = loadWordSet().map { $0.prefix(length: prefixLength) } let result = loadWordSet().map { String($0.prefix(prefixLength)) }
Language.truncatedWordSetCache[self] = result Language.truncatedWordSetCache[self] = result
return result return result
@ -116,9 +116,9 @@ public enum Mnemonic {
} }
public static func decode(mnemonic: String, language: Language = .english) throws -> String { public static func decode(mnemonic: String, language: Language = .english) throws -> String {
var words = mnemonic.split(separator: " ").map { String($0) } var words: [String] = mnemonic.split(separator: " ").map { String($0) }
let truncatedWordSet = language.loadTruncatedWordSet() let truncatedWordSet: [String] = language.loadTruncatedWordSet()
let prefixLength = language.prefixLength let prefixLength: Int = language.prefixLength
var result = "" var result = ""
let n = truncatedWordSet.count let n = truncatedWordSet.count
@ -131,9 +131,12 @@ public enum Mnemonic {
// Decode // Decode
for chunkStartIndex in stride(from: 0, to: words.count, by: 3) { for chunkStartIndex in stride(from: 0, to: words.count, by: 3) {
guard let w1 = truncatedWordSet.firstIndex(of: words[chunkStartIndex].prefix(length: prefixLength)), guard
let w2 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 1].prefix(length: prefixLength)), let w1 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex].prefix(prefixLength))),
let w3 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 2].prefix(length: prefixLength)) else { throw DecodingError.invalidWord } 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) let x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n)
guard x % n == w1 else { throw DecodingError.generic } guard x % n == w1 else { throw DecodingError.generic }
let string = "0000000" + String(x, radix: 16) let string = "0000000" + String(x, radix: 16)
@ -143,7 +146,10 @@ public enum Mnemonic {
// Verify checksum // Verify checksum
let checksumIndex = determineChecksumIndex(for: words, prefixLength: prefixLength) let checksumIndex = determineChecksumIndex(for: words, prefixLength: prefixLength)
let expectedChecksumWord = words[checksumIndex] 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
return result return result
@ -162,15 +168,9 @@ public enum Mnemonic {
return String(p1 + p2 + p3 + p4) return String(p1 + p2 + p3 + p4)
} }
private static func determineChecksumIndex(for x: [String], prefixLength: UInt) -> Int { private static func determineChecksumIndex(for x: [String], prefixLength: Int) -> Int {
let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8)) let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(prefixLength) }.joined().utf8))
return Int(checksum) % x.count return Int(checksum) % x.count
} }
} }
private extension String {
func prefix(length: UInt) -> String {
return String(self[startIndex..<index(startIndex, offsetBy: Int(length))])
}
}

View File

@ -33,6 +33,12 @@ open class Storage {
public static let shared: Storage = Storage() public static let shared: Storage = Storage()
public private(set) var isValid: Bool = false 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 hasCompletedMigrations: Bool { migrationsCompleted.wrappedValue }
public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? { public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? {
internalCurrentlyRunningMigration.wrappedValue internalCurrentlyRunningMigration.wrappedValue
@ -353,6 +359,25 @@ open class Storage {
// MARK: - File Management // 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() { public static func resetAllStorage() {
// Just in case they haven't been removed for some reason, delete the legacy database & keys // Just in case they haven't been removed for some reason, delete the legacy database & keys
SUKLegacy.clearLegacyDatabaseInstance() SUKLegacy.clearLegacyDatabaseInstance()