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:
Morgan Pretty 2023-07-20 12:29:56 +10:00
parent 6ba9d1df89
commit f373a989a8
47 changed files with 409 additions and 182 deletions

View File

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

View File

@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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))])
}
}

View File

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