From dce05376d13285970b48d5db7ad8f37b5470907a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 31 Jan 2025 17:31:27 +1100 Subject: [PATCH] Fixed additional QA issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the "Delete for Everyone" behaviour to show a blocking loading indicator if a network request is required • Removed some duplicate code • Fixed an issue where mentions wouldn't filter correctly for groups in some cases • Fixed an issue where legacy group members could appear in the members list multiple times when navigating there from the conversation title • Fixed an issue where the message deletion toasts could appear behind blurred modal backgrounds • Fixed an issue where pasting text which is too large into an empty input field would result in the text having the default OS styling instead of the text styling we have set --- Session.xcodeproj/project.pbxproj | 16 +-- .../ConversationVC+Interaction.swift | 81 +++++-------- .../Input View/InputTextView.swift | 31 ++++- .../Settings/ThreadSettingsViewModel.swift | 96 ++++++++------- Session/Shared/Types/NavigatableState.swift | 34 +++++- .../Shared Models/MentionInfo.swift | 22 ++-- .../MessageViewModel+DeletionActions.swift | 13 ++ SessionSnodeKit/SnodeAPI/SnodeAPI.swift | 112 +++++++++--------- ...ModalActivityIndicatorViewController.swift | 29 +++++ 9 files changed, 265 insertions(+), 169 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b836fb10a..9493b20ad 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -182,7 +182,6 @@ 947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; - 94C5DCB02BE88170003AA8C5 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; @@ -1454,7 +1453,6 @@ 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; - 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -3721,11 +3719,6 @@ isa = PBXGroup; children = ( 94C58AC82D2E036E00609195 /* Permissions.swift */, - FD6A39422C2AD81600762359 /* BackgroundTaskManager.swift */, - FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, - FD6A39162C2A99A000762359 /* BencodeDecoder.swift */, - FD6A39182C2A99AB00762359 /* BencodeEncoder.swift */, - FD6A391A2C2A99B600762359 /* BencodeResponse.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */, FD7443462D07CA9F00862443 /* CGPoint+Utilities.swift */, @@ -7598,7 +7591,6 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; @@ -7927,7 +7919,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 530; + CURRENT_PROJECT_VERSION = 531; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8003,7 +7995,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 530; + CURRENT_PROJECT_VERSION = 531; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8066,7 +8058,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 528; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -8104,7 +8095,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.7; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -8134,7 +8124,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 528; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -8172,7 +8161,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.7; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 760ac3c5d..c765c28d9 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -46,40 +46,19 @@ extension ConversationVC: navigationController?.pushViewController(viewController, animated: true) case (.userCount, .group, true, _), (.userCount, .legacyGroup, true, _): - let viewController: SessionTableViewController = SessionTableViewController( - viewModel: UserListViewModel( - title: "groupMembers".localized(), - showProfileIcons: true, - request: GroupMember - .filter(GroupMember.Columns.groupId == self.viewModel.threadData.threadId), - onTap: .callback { [weak self, dependencies = viewModel.dependencies] _, memberInfo in - dependencies[singleton: .storage].write { db in - try SessionThread.upsert( - db, - id: memberInfo.profileId, - variant: .contact, - values: SessionThread.TargetValues( - creationDateTimestamp: .useExistingOrSetTo( - (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) - ), - shouldBeVisible: .useExisting, - isDraft: .useExistingOrSetTo(true) - ), - using: dependencies - ) - } - - self?.navigationController?.pushViewController( - ConversationVC( - threadId: memberInfo.profileId, - threadVariant: .contact, - using: dependencies - ), - animated: true - ) - }, - using: self.viewModel.dependencies - ) + let viewController: UIViewController = ThreadSettingsViewModel.createMemberListViewController( + threadId: self.viewModel.threadData.threadId, + transitionToConversation: { [weak self, dependencies = viewModel.dependencies] selectedMemberId in + self?.navigationController?.pushViewController( + ConversationVC( + threadId: selectedMemberId, + threadVariant: .contact, + using: dependencies + ), + animated: true + ) + }, + using: viewModel.dependencies ) navigationController?.pushViewController(viewController, animated: true) @@ -2088,23 +2067,27 @@ extension ConversationVC: /// Trigger the deletion behaviours deletionBehaviours .publisherForAction(at: selectedIndex, using: dependencies) + .showingBlockingLoading( + in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ? + self?.viewModel.navigatableState : + nil + ) .sinkUntilComplete( receiveCompletion: { result in - switch result { - case .finished: - DispatchQueue.main.async { - self?.viewModel.showToast( - text: "deleteMessageDeleted" - .putNumber(messagesToDelete.count) - .localized(), - backgroundColor: .backgroundSecondary, - inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing - ) + DispatchQueue.main.async { + switch result { + case .finished: + modal.dismiss(animated: true) { + self?.viewModel.showToast( + text: "deleteMessageDeleted" + .putNumber(messagesToDelete.count) + .localized(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } - modal.close() - } - case .failure: - DispatchQueue.main.async { + case .failure: self?.viewModel.showToast( text: "deleteMessageFailed" .putNumber(messagesToDelete.count) @@ -2112,7 +2095,7 @@ extension ConversationVC: backgroundColor: .backgroundSecondary, inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing ) - } + } } } ) diff --git a/Session/Conversations/Input View/InputTextView.swift b/Session/Conversations/Input View/InputTextView.swift index c2c080c4a..28e011b56 100644 --- a/Session/Conversations/Input View/InputTextView.swift +++ b/Session/Conversations/Input View/InputTextView.swift @@ -5,6 +5,8 @@ import SessionUIKit import SessionUtilitiesKit public final class InputTextView: UITextView, UITextViewDelegate { + private static let defaultFont: UIFont = .systemFont(ofSize: Values.mediumFontSize) + private static let defaultThemeTextColor: ThemeValue = .textPrimary private weak var snDelegate: InputTextViewDelegate? private let maxWidth: CGFloat private lazy var heightConstraint = self.set(.height, to: minHeight) @@ -69,9 +71,11 @@ public final class InputTextView: UITextView, UITextViewDelegate { showsHorizontalScrollIndicator = false showsVerticalScrollIndicator = false - font = .systemFont(ofSize: Values.mediumFontSize) + /// **Note:** If we add any additional attributes here then we will need to update the logic in + /// `textView(_:,shouldChangeTextIn:replacementText:)` to match + font = InputTextView.defaultFont themeBackgroundColor = .clear - themeTextColor = .textPrimary + themeTextColor = InputTextView.defaultThemeTextColor themeTintColor = .primary heightConstraint.isActive = true @@ -116,7 +120,28 @@ public final class InputTextView: UITextView, UITextViewDelegate { /// Truncate text based on character count (use `textStorage.replaceCharacters` for built in `undo` support) let truncatedText: String = String(text.prefix(remainingSpace)) let offset: Int = range.location + truncatedText.count - textView.textStorage.replaceCharacters(in: range, with: truncatedText) + + /// Pasting a value that is too large into the input will result in some odd default OS styling being applied to the text which is very + /// different from our desired text style, in order to avoid this we need to detect this case and explicitly set the value as an attributed + /// string with our explicit styling + /// + /// **Note:** If we add any additional attributes these will need to be updated to match + if currentText.isEmpty { + textView.textStorage.setAttributedString( + NSAttributedString( + string: truncatedText, + attributes: [ + .font: textView.font ?? InputTextView.defaultFont, + .foregroundColor: textView.textColor ?? ThemeManager.currentTheme.color( + for: InputTextView.defaultThemeTextColor + ) as Any + ] + ) + ) + } + else { + textView.textStorage.replaceCharacters(in: range, with: truncatedText) + } /// Position cursor after inserted text /// diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 590f79b99..5b0319c0d 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -929,50 +929,64 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob ) } - private func viewMembers() { - self.transitionToScreen( - SessionTableViewController( - viewModel: UserListViewModel( - title: "groupMembers".localized(), - showProfileIcons: true, - request: GroupMember - .select( - GroupMember.Columns.groupId, - GroupMember.Columns.profileId, - max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name), - GroupMember.Columns.roleStatus, - GroupMember.Columns.isHidden - ) - .filter(GroupMember.Columns.groupId == threadId) - .group(GroupMember.Columns.profileId), - onTap: .callback { [weak self, dependencies] _, memberInfo in - dependencies[singleton: .storage].write { db in - try SessionThread.upsert( - db, - id: memberInfo.profileId, - variant: .contact, - values: SessionThread.TargetValues( - creationDateTimestamp: .useExistingOrSetTo( - dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 - ), - shouldBeVisible: .useExisting, - isDraft: .useExistingOrSetTo(true) + public static func createMemberListViewController( + threadId: String, + transitionToConversation: @escaping (String) -> Void, + using dependencies: Dependencies + ) -> UIViewController { + return SessionTableViewController( + viewModel: UserListViewModel( + title: "groupMembers".localized(), + showProfileIcons: true, + request: GroupMember + .select( + GroupMember.Columns.groupId, + GroupMember.Columns.profileId, + max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name), + GroupMember.Columns.roleStatus, + GroupMember.Columns.isHidden + ) + .filter(GroupMember.Columns.groupId == threadId) + .group(GroupMember.Columns.profileId), + onTap: .callback { _, memberInfo in + dependencies[singleton: .storage].write { db in + try SessionThread.upsert( + db, + id: memberInfo.profileId, + variant: .contact, + values: SessionThread.TargetValues( + creationDateTimestamp: .useExistingOrSetTo( + dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 ), - using: dependencies - ) - } - - self?.transitionToScreen( - ConversationVC( - threadId: memberInfo.profileId, - threadVariant: .contact, - using: dependencies + shouldBeVisible: .useExisting, + isDraft: .useExistingOrSetTo(true) ), - transitionType: .push + using: dependencies ) - }, - using: dependencies - ) + } + + transitionToConversation(memberInfo.profileId) + }, + using: dependencies + ) + ) + } + + private func viewMembers() { + self.transitionToScreen( + ThreadSettingsViewModel.createMemberListViewController( + threadId: threadId, + transitionToConversation: { [weak self, dependencies] selectedMemberId in + self?.transitionToScreen( + ConversationVC( + threadId: selectedMemberId, + threadVariant: .contact, + using: dependencies + ), + transitionType: .push + ) + }, + using: dependencies ) ) } diff --git a/Session/Shared/Types/NavigatableState.swift b/Session/Shared/Types/NavigatableState.swift index 6d7be0641..ab0a4a2fb 100644 --- a/Session/Shared/Types/NavigatableState.swift +++ b/Session/Shared/Types/NavigatableState.swift @@ -60,10 +60,12 @@ public struct NavigatableState { self.showToast .receive(on: DispatchQueue.main) .sink { [weak viewController] text, color, inset in - guard let view: UIView = viewController?.view else { return } + guard let presenter: UIViewController = (viewController?.presentedViewController ?? viewController) else { + return + } let toastController: ToastController = ToastController(text: text, background: color) - toastController.presentToastView(fromBottomOfView: view, inset: inset) + toastController.presentToastView(fromBottomOfView: presenter.view, inset: inset) } .store(in: &disposables) @@ -113,3 +115,31 @@ public struct NavigatableState { .store(in: &disposables) } } + +public extension Publisher { + func showingBlockingLoading(in navigatableState: NavigatableState?) -> AnyPublisher { + guard let navigatableState: NavigatableState = navigatableState else { + return self.eraseToAnyPublisher() + } + + let modalActivityIndicator: ModalActivityIndicatorViewController = ModalActivityIndicatorViewController(onAppear: { _ in }) + + return self + .handleEvents( + receiveSubscription: { _ in + navigatableState._transitionToScreen.send((modalActivityIndicator, .present)) + } + ) + .asResult() + .flatMap { result -> AnyPublisher in + Deferred { + Future { resolver in + modalActivityIndicator.dismiss(completion: { + resolver(result) + }) + } + }.eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} diff --git a/SessionMessagingKit/Shared Models/MentionInfo.swift b/SessionMessagingKit/Shared Models/MentionInfo.swift index a304d976a..49525e465 100644 --- a/SessionMessagingKit/Shared Models/MentionInfo.swift +++ b/SessionMessagingKit/Shared Models/MentionInfo.swift @@ -35,9 +35,16 @@ public extension MentionInfo { let interaction: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() - let prefixesLiteral: SQLExpression = targetPrefixes - .map { SQL("\(profile[.id]) LIKE '\(SQL(stringLiteral: "\($0.rawValue)%"))'") } + .map { prefix in + SQL( + """ + ( + \(profile[.id]) > '\(SQL(stringLiteral: "\(prefix.rawValue)"))' AND + \(profile[.id]) < '\(SQL(stringLiteral: "\(prefix.endOfRangeString)"))' + ) + """) + } .joined(operator: .or) let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) let currentUserIds: Set = [ @@ -59,9 +66,10 @@ public extension MentionInfo { return """ FROM \(profileFullTextSearch) JOIN \(Profile.self) ON ( - \(Profile.self).rowid = \(profileFullTextSearch).rowid AND - \(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR - \(prefixesLiteral) + \(Profile.self).rowid = \(profileFullTextSearch).rowid AND ( + \(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR + \(prefixesLiteral) + ) ) """ }() @@ -118,10 +126,10 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), - \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) + \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken), + MAX(\(interaction[.timestampMs])) -- Want the newest interaction (for sorting) \(targetJoin) JOIN \(Interaction.self) ON ( diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index ea2a7ca0b..abe6f0179 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -46,6 +46,19 @@ public extension MessageViewModel { public let body: String public let actions: [NamedAction] + public func requiresNetworkRequestForAction(at index: Int) -> Bool { + guard index >= 0, index < actions.count else { + return false + } + + return actions[index].behaviours.contains { behaviour in + switch behaviour { + case .preparedRequest: return true + case .cancelPendingSendJobs, .deleteFromDatabase, .markAsDeleted: return false + } + } + } + /// Collect the actions and construct a publisher which triggers each action before returning the result public func publisherForAction(at index: Int, using dependencies: Dependencies) -> AnyPublisher { guard index >= 0, index < actions.count else { diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI/SnodeAPI.swift index 99ba0581c..d2202a84f 100644 --- a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI/SnodeAPI.swift @@ -31,22 +31,18 @@ public final class SnodeAPI { // If we have any config hashes to refresh TTLs then add those requests first if !refreshingConfigHashes.isEmpty { + let updatedExpiryMS: Int64 = ( + dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + (30 * 24 * 60 * 60 * 1000) // 30 days + ) requests.append( - try SnodeAPI.prepareRequest( - request: Request( - endpoint: .expire, - swarmPublicKey: authMethod.swarmPublicKey, - body: UpdateExpiryRequest( - messageHashes: refreshingConfigHashes, - expiryMs: UInt64( - dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + - (30 * 24 * 60 * 60 * 1000) // 30 days - ), - extend: true, - authMethod: authMethod - ) - ), - responseType: UpdateExpiryResponse.self, + try SnodeAPI.preparedUpdateExpiry( + serverHashes: refreshingConfigHashes, + updatedExpiryMs: updatedExpiryMS, + extendOnly: true, + ignoreValidationFailure: true, + explicitTargetNode: snode, + authMethod: authMethod, using: dependencies ) ) @@ -78,39 +74,6 @@ public final class SnodeAPI { let messageResponses: [Network.BatchSubResponse] = batchResponse .compactMap { $0 as? Network.BatchSubResponse } - /// Since we have extended the TTL for a number of messages we need to make sure we update the local - /// `SnodeReceivedMessageInfo.expirationDateMs` values so we don't end up deleting them - /// incorrectly before they actually expire on the swarm - if - !refreshingConfigHashes.isEmpty, - let refreshTTLSubReponse: Network.BatchSubResponse = batchResponse - .first(where: { $0 is Network.BatchSubResponse }) - .asType(Network.BatchSubResponse.self), - let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body, - let validResults: [String: UpdateExpiryResponseResult] = try? refreshTTLResponse.validResultMap( - swarmPublicKey: authMethod.swarmPublicKey, - validationData: refreshingConfigHashes, - using: dependencies - ), - let targetResult: UpdateExpiryResponseResult = validResults[snode.ed25519PubkeyHex], - let groupedExpiryResult: [UInt64: [String]] = targetResult.changed - .updated(with: targetResult.unchanged) - .groupedByValue() - .nullIfEmpty() - { - dependencies[singleton: .storage].writeAsync { db in - try groupedExpiryResult.forEach { updatedExpiry, hashes in - try SnodeReceivedMessageInfo - .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash)) - .updateAll( - db, - SnodeReceivedMessageInfo.Columns.expirationDateMs - .set(to: updatedExpiry) - ) - } - } - } - return zip(namespaces, messageResponses) .reduce(into: [:]) { result, next in guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return } @@ -404,6 +367,8 @@ public final class SnodeAPI { updatedExpiryMs: Int64, shortenOnly: Bool? = nil, extendOnly: Bool? = nil, + ignoreValidationFailure: Bool = false, + explicitTargetNode: LibSession.Snode? = nil, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> { @@ -427,12 +392,53 @@ public final class SnodeAPI { using: dependencies ) .tryMap { _, response -> [String: UpdateExpiryResponseResult] in - try response.validResultMap( - swarmPublicKey: authMethod.swarmPublicKey, - validationData: serverHashes, - using: dependencies - ) + do { + return try response.validResultMap( + swarmPublicKey: authMethod.swarmPublicKey, + validationData: serverHashes, + using: dependencies + ) + } + catch { + guard ignoreValidationFailure else { throw error } + + return [:] + } } + .handleEvents( + receiveOutput: { _, result in + /// Since we have updated the TTL we need to make sure we also update the local + /// `SnodeReceivedMessageInfo.expirationDateMs` values so they match the updated swarm, if + /// we had a specific `snode` we we're sending the request to then we should use those values, otherwise + /// we can just grab the first value from the response and use that + let maybeTargetResult: UpdateExpiryResponseResult? = { + guard let snode: LibSession.Snode = explicitTargetNode else { + return result.first?.value + } + + return result[snode.ed25519PubkeyHex] + }() + guard + let targetResult: UpdateExpiryResponseResult = maybeTargetResult, + let groupedExpiryResult: [UInt64: [String]] = targetResult.changed + .updated(with: targetResult.unchanged) + .groupedByValue() + .nullIfEmpty() + else { return } + + dependencies[singleton: .storage].writeAsync { db in + try groupedExpiryResult.forEach { updatedExpiry, hashes in + try SnodeReceivedMessageInfo + .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash)) + .updateAll( + db, + SnodeReceivedMessageInfo.Columns.expirationDateMs + .set(to: updatedExpiry) + ) + } + } + } + ) } public static func preparedRevokeSubaccounts( diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index 54ba079c0..2bb313832 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import Combine import MediaPlayer import SessionUIKit import NVActivityIndicatorView @@ -202,3 +203,31 @@ public class ModalActivityIndicatorViewController: OWSViewController { messageLabel.isHidden = (message == nil) } } + +public extension Publisher { + func showingBlockingLoading(in viewController: UIViewController?) -> AnyPublisher { + guard let viewController: UIViewController = viewController else { + return self.eraseToAnyPublisher() + } + + let modalActivityIndicator: ModalActivityIndicatorViewController = ModalActivityIndicatorViewController(onAppear: { _ in }) + + return self + .handleEvents( + receiveSubscription: { [weak viewController] _ in + viewController?.present(modalActivityIndicator, animated: true) + } + ) + .asResult() + .flatMap { result -> AnyPublisher in + Deferred { + Future { resolver in + modalActivityIndicator.dismiss(completion: { + resolver(result) + }) + } + }.eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +}