Merge remote-tracking branch 'upstream/dev' into feature/job-runner-unit-tests

# Conflicts:
#	Session/Conversations/ConversationVC+Interaction.swift
#	SessionMessagingKit/Open Groups/OpenGroupAPI.swift
#	SessionMessagingKit/Open Groups/OpenGroupManager.swift
This commit is contained in:
Morgan Pretty 2023-08-01 14:39:00 +10:00
commit b471a32209
53 changed files with 456 additions and 203 deletions

View File

@ -6414,7 +6414,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 419;
CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6438,7 +6438,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6486,7 +6486,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 419;
CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6515,7 +6515,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6551,7 +6551,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 419;
CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6574,7 +6574,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6625,7 +6625,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 419;
CURRENT_PROJECT_VERSION = 421;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6653,7 +6653,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7585,7 +7585,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 = 421;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7623,7 +7623,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7656,7 +7656,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 = 421;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7694,7 +7694,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.3.0;
MARKETING_VERSION = 2.3.2;
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

@ -149,8 +149,14 @@ extension ConversationVC:
dismiss(animated: true, completion: nil)
}
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendMessage(text: (messageText ?? ""), attachments: attachments)
func sendMediaNav(
_ sendMediaNavigationController: SendMediaNavigationController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
messageText: String?,
using dependencies: Dependencies
) {
sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies)
resetMentions()
dismiss(animated: true) { [weak self] in
@ -460,15 +466,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
)
@ -483,10 +487,20 @@ extension ConversationVC:
quoteModel: quoteModel
)
DispatchQueue.global(qos:.userInitiated).async {
sendMessage(optimisticData: optimisticData, using: dependencies)
}
private func sendMessage(
optimisticData: ConversationViewModel.OptimisticMessageData,
using dependencies: Dependencies
) {
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
DispatchQueue.global(qos:.userInitiated).async(using: dependencies) {
// 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
dependencies.storage
@ -505,7 +519,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(
@ -516,7 +530,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,
@ -543,7 +557,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()
}
)
@ -1617,6 +1637,30 @@ extension ConversationVC:
}
func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
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
sendMessage(optimisticData: optimisticMessageData, using: dependencies)
return
}
dependencies.storage.writeAsync { [weak self] db in
guard
let threadId: String = self?.viewModel.threadData.threadId,
@ -2168,7 +2212,7 @@ extension ConversationVC:
// MARK: - VoiceMessageRecordingViewDelegate
func startVoiceMessageRecording() {
func startVoiceMessageRecording(using dependencies: Dependencies) {
// Request permission if needed
Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in
DispatchQueue.main.async {
@ -2217,7 +2261,7 @@ extension ConversationVC:
// Limit voice messages to a minute
audioTimer = Timer.scheduledTimer(withTimeInterval: 180, repeats: false, block: { [weak self] _ in
self?.snInputView.hideVoiceMessageUI()
self?.endVoiceMessageRecording()
self?.endVoiceMessageRecording(using: dependencies)
})
// Prepare audio recorder
@ -2233,7 +2277,7 @@ extension ConversationVC:
}
}
func endVoiceMessageRecording() {
func endVoiceMessageRecording(using dependencies: Dependencies) {
UIApplication.shared.isIdleTimerDisabled = true
// Hide the UI
@ -2285,7 +2329,7 @@ extension ConversationVC:
}
// Send attachment
sendMessage(text: "", attachments: [attachment])
sendMessage(text: "", attachments: [attachment], using: dependencies)
}
func cancelVoiceMessageRecording() {

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

@ -416,14 +416,14 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
if inputViewButton == sendButton { delegate?.handleSendButtonTapped() }
}
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) {
guard inputViewButton == voiceMessageButton else { return }
// Note: The 'showVoiceMessageUI' call MUST come before triggering 'startVoiceMessageRecording'
// because if something goes wrong it'll trigger `hideVoiceMessageUI` and we don't want it to
// end up in a state with the input content hidden
showVoiceMessageUI()
delegate?.startVoiceMessageRecording()
delegate?.startVoiceMessageRecording(using: dependencies)
}
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) {

View File

@ -137,7 +137,9 @@ final class InputViewButton: UIView {
// We want to detect both taps and long presses
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { onTouchesBegan() }
private func onTouchesBegan(using dependencies: Dependencies = Dependencies()) {
guard isUserInteractionEnabled else { return }
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
@ -145,7 +147,7 @@ final class InputViewButton: UIView {
invalidateLongPressIfNeeded()
longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in
self?.isLongPress = true
self?.delegate?.handleInputViewButtonLongPressBegan(self)
self?.delegate?.handleInputViewButtonLongPressBegan(self, using: dependencies)
})
}
@ -185,13 +187,13 @@ final class InputViewButton: UIView {
protocol InputViewButtonDelegate: AnyObject {
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton)
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?)
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies)
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?)
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?)
}
extension InputViewButtonDelegate {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { }
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) { }
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
}

View File

@ -310,12 +310,12 @@ final class VoiceMessageRecordingView: UIView {
}
}
func handleLongPressEnded(at location: CGPoint) {
func handleLongPressEnded(at location: CGPoint, using dependencies: Dependencies = Dependencies()) {
if pulseView.frame.contains(location) {
delegate?.endVoiceMessageRecording()
delegate?.endVoiceMessageRecording(using: dependencies)
}
else if isValidLockViewLocation(location) {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCircleViewTap))
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onCircleViewTap))
circleView.addGestureRecognizer(tapGestureRecognizer)
UIView.animate(withDuration: 0.25, delay: 0, options: .transitionCrossDissolve, animations: {
@ -332,8 +332,10 @@ final class VoiceMessageRecordingView: UIView {
}
}
@objc private func handleCircleViewTap() {
delegate?.endVoiceMessageRecording()
@objc private func onCircleViewTap() { handleCircleViewTap() }
private func handleCircleViewTap(using dependencies: Dependencies = Dependencies()) {
delegate?.endVoiceMessageRecording(using: dependencies)
}
@objc private func handleCancelButtonTapped() {
@ -474,7 +476,7 @@ extension VoiceMessageRecordingView {
// MARK: - Delegate
protocol VoiceMessageRecordingViewDelegate: AnyObject {
func startVoiceMessageRecording()
func endVoiceMessageRecording()
func startVoiceMessageRecording(using dependencies: Dependencies)
func endVoiceMessageRecording(using dependencies: Dependencies)
func cancelVoiceMessageRecording()
}

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

@ -46,6 +46,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
// MARK: - UI
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown
}

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

@ -44,6 +44,10 @@ class MediaGalleryNavigationController: UINavigationController {
// MARK: - Orientation
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown
}

View File

@ -54,6 +54,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
// MARK: - UI
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown
}

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 {
@ -419,7 +431,7 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText)
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText, using: dependencies)
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -752,7 +764,7 @@ private class DoneButton: UIView {
protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies)
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)

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()
}
}
}
@ -249,7 +248,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .allButUpsideDown
return .all
}
return .portrait
@ -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

@ -140,6 +140,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>

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

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

@ -201,8 +201,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()
dependencies.storage
.writePublisher { db -> MessageSender.PreparedSendData in
@ -279,8 +278,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
@ -351,6 +357,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()

View File

@ -8,17 +8,18 @@
#import <PureLayout/PureLayout.h>
#import <SessionUIKit/SessionUIKit.h>
#import <SignalCoreKit/OWSAsserts.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
NS_ASSUME_NONNULL_BEGIN
BOOL IsLandscapeOrientationEnabled(void)
{
return NO;
return UIDevice.currentDevice.isIPad;
}
UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void)
{
return (IsLandscapeOrientationEnabled() ? UIInterfaceOrientationMaskAllButUpsideDown
return (IsLandscapeOrientationEnabled() ? UIInterfaceOrientationMaskAll
: UIInterfaceOrientationMaskPortrait);
}