diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 642093112..ea42d0fbb 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6032,7 +6032,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6057,7 +6057,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6105,7 +6105,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6135,7 +6135,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6171,7 +6171,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6194,7 +6194,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6245,7 +6245,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6273,7 +6273,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -7173,7 +7173,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7212,7 +7212,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7245,7 +7245,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 385; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7284,7 +7284,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 42c150a15..3c1ef4444 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -314,7 +314,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) guard !updatedName.isEmpty else { - return showError(title: "vc_create_closed_group_group_name_missing_error".lowercased()) + return showError(title: "vc_create_closed_group_group_name_missing_error".localized()) } guard updatedName.count < 64 else { return showError(title: "vc_create_closed_group_group_name_too_long_error".localized()) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 8bb622e84..f342c6824 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -54,9 +54,10 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl var scrollDistanceToBottomBeforeUpdate: CGFloat? var baselineKeyboardHeight: CGFloat = 0 - /// This flag is true between `viewDidAppear` and `viewWillDisappear` and is used to prevent keyboard changes - /// from trying to animate (as the animations can cause staggering with push transitions) - var viewIsFocussed = false + /// These flags are true between `viewDid/Will Appear/Disappear` and is used to prevent keyboard changes + /// from trying to animate (as the animations can cause buggy transitions) + var viewIsDisappearing = false + var viewIsAppearing = false // Reaction var currentReactionListSheet: ReactionListSheet? @@ -399,6 +400,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl super.viewWillAppear(animated) startObservingChanges() + + viewIsAppearing = true } override func viewDidAppear(_ animated: Bool) { @@ -407,7 +410,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // Flag that the initial layout has been completed (the flag blocks and unblocks a number // of different behaviours) didFinishInitialLayout = true - viewIsFocussed = true + viewIsAppearing = false if delayFirstResponder || isShowingSearchUI { delayFirstResponder = false @@ -426,7 +429,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - viewIsFocussed = false + viewIsDisappearing = true // Don't set the draft or resign the first responder if we are replacing the thread (want the keyboard // to appear to remain focussed) @@ -442,6 +445,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl mediaCache.removeAllObjects() hasReloadedThreadDataAfterDisappearance = false + viewIsDisappearing = false } @objc func applicationDidBecomeActive(_ notification: Notification) { @@ -1067,7 +1071,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // MARK: - Notifications @objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) { - guard viewIsFocussed || !didFinishInitialLayout else { return } + guard !viewIsDisappearing else { return } // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 // and https://stackoverflow.com/a/25260930 to better understand what we are @@ -1115,7 +1119,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } // Perform the changes (don't animate if the initial layout hasn't been completed) - guard hasDoneLayout && didFinishInitialLayout else { + guard hasDoneLayout && didFinishInitialLayout && !viewIsAppearing else { UIView.performWithoutAnimation { changes() } @@ -1132,8 +1136,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } @objc func handleKeyboardWillHideNotification(_ notification: Notification) { - guard viewIsFocussed else { return } - // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 // and https://stackoverflow.com/a/25260930 to better understand what we are // doing with the UIViewAnimationOptions diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 84e3004ed..442a7e9aa 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -227,6 +227,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Remaining constraints authorLabel.pin(.leading, to: .leading, of: snContentView, withInset: VisibleMessageCell.authorLabelInset) + authorLabel.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) // Under bubble content addSubview(underBubbleStackView) @@ -1052,6 +1053,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) -> TappableLabel { let isOutgoing: Bool = (cellViewModel.variant == .standardOutgoing) let result: TappableLabel = TappableLabel() + result.setContentCompressionResistancePriority(.required, for: .vertical) result.themeBackgroundColor = .clear result.isOpaque = false result.isUserInteractionEnabled = true diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index b2a97dfe4..12a5b6b83 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -214,8 +214,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD BackgroundPoller.isValid = false - // Suspend database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + if CurrentAppContext().isInBackground() { + // Suspend database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } SNLog("Background poll failed due to manual timeout") completionHandler(.failed) @@ -233,8 +235,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD BackgroundPoller.isValid = false - // Suspend database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + if CurrentAppContext().isInBackground() { + // Suspend database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } cancelTimer.invalidate() completionHandler(result) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 4d32c974b..051ba7cc2 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -227,6 +227,7 @@ public enum OpenGroupAPI { public static func capabilities( _ db: Database, server: String, + forceBlinded: Bool = false, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { return OpenGroupAPI @@ -236,6 +237,7 @@ public enum OpenGroupAPI { server: server, endpoint: .capabilities ), + forceBlinded: forceBlinded, using: dependencies ) .decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, using: dependencies) @@ -1260,6 +1262,7 @@ public enum OpenGroupAPI { messageBytes: Bytes, for serverName: String, fallbackSigningType signingType: SessionId.Prefix, + forceBlinded: Bool = false, using dependencies: SMKDependencies = SMKDependencies() ) -> (publicKey: String, signature: Bytes)? { guard @@ -1279,7 +1282,7 @@ public enum OpenGroupAPI { .defaulting(to: []) // If we have no capabilities or if the server supports blinded keys then sign using the blinded key - if capabilities.isEmpty || capabilities.contains(.blind) { + if forceBlinded || capabilities.isEmpty || capabilities.contains(.blind) { guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else { return nil } @@ -1326,6 +1329,7 @@ public enum OpenGroupAPI { request: URLRequest, for serverName: String, with serverPublicKey: String, + forceBlinded: Bool = false, using dependencies: SMKDependencies = SMKDependencies() ) -> URLRequest? { guard let url: URL = request.url else { return nil } @@ -1366,7 +1370,7 @@ public enum OpenGroupAPI { .appending(contentsOf: bodyHash ?? []) /// Sign the above message - guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: messageBytes, for: serverName, fallbackSigningType: .unblinded, using: dependencies) else { + guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: messageBytes, for: serverName, fallbackSigningType: .unblinded, forceBlinded: forceBlinded, using: dependencies) else { return nil } @@ -1386,6 +1390,7 @@ public enum OpenGroupAPI { private static func send( _ db: Database, request: Request, + forceBlinded: Bool = false, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise<(OnionRequestResponseInfoType, Data?)> { let urlRequest: URLRequest @@ -1406,7 +1411,7 @@ public enum OpenGroupAPI { guard let publicKey: String = maybePublicKey else { return Promise(error: OpenGroupAPIError.noPublicKey) } // Attempt to sign the request with the new auth - guard let signedRequest: URLRequest = sign(db, request: urlRequest, for: request.server, with: publicKey, using: dependencies) else { + guard let signedRequest: URLRequest = sign(db, request: urlRequest, for: request.server, with: publicKey, forceBlinded: forceBlinded, using: dependencies) else { return Promise(error: OpenGroupAPIError.signingFailed) } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 0166e026c..48b8c0dff 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -84,7 +84,7 @@ extension OpenGroupAPI { let (promise, seal) = Promise.pending() promise.retainUntilComplete() - Threading.pollerQueue.async { + let pollingLogic: () -> Void = { dependencies.storage .read { db -> Promise<(Int64, PollResponse)> in let failureCount: Int64 = (try? OpenGroup @@ -150,7 +150,7 @@ extension OpenGroupAPI { error: error ) .done(on: OpenGroupAPI.workQueue) { [weak self] didHandleError in - if !didHandleError { + if !didHandleError && isBackgroundPollerValid() { // Increase the failure count let pollFailureCount: Int64 = Storage.shared .read { db in @@ -181,6 +181,14 @@ extension OpenGroupAPI { } } + // If this was run via the background poller then don't run on the pollerQueue + if calledFromBackgroundPoller { + pollingLogic() + } + else { + Threading.pollerQueue.async { pollingLogic() } + } + return promise } @@ -213,11 +221,12 @@ extension OpenGroupAPI { OpenGroupAPI.capabilities( db, server: server, + forceBlinded: true, using: dependencies ) } .then(on: OpenGroupAPI.workQueue) { [weak self] _, responseBody -> Promise in - guard let strongSelf = self else { return Promise.value(()) } + guard let strongSelf = self, isBackgroundPollerValid() else { return Promise.value(()) } // Handle the updated capabilities and re-trigger the poll strongSelf.isPolling = false diff --git a/SessionSnodeKit/Models/OnionRequestAPIError.swift b/SessionSnodeKit/Models/OnionRequestAPIError.swift index 3b75fe124..43e70da73 100644 --- a/SessionSnodeKit/Models/OnionRequestAPIError.swift +++ b/SessionSnodeKit/Models/OnionRequestAPIError.swift @@ -16,6 +16,9 @@ public enum OnionRequestAPIError: LocalizedError { switch self { case .httpRequestFailedAtDestination(let statusCode, let data, let destination): if statusCode == 429 { return "Rate limited." } + if let processedResponseBodyData: Data = OnionRequestAPI.process(bencodedData: data)?.body, let errorResponse: String = String(data: processedResponseBodyData, encoding: .utf8) { + return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." + } if let errorResponse: String = String(data: data, encoding: .utf8) { return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." } diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 5a7e2fd45..d2a71b66c 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -700,72 +700,86 @@ public enum OnionRequestAPI: OnionRequestAPIType { do { let data: Data = try AESGCM.decrypt(responseData, with: destinationSymmetricKey) - // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into - // parts to properly process it - guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let stringParts: [String.SubSequence] = responseString.split(separator: ":") - - guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) - let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) - let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { - return seal.fulfill((responseInfo, nil)) - } - - // Extract the response data as well - let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) - let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") - - guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]), let suffixData: Data = "e".data(using: .utf8) else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let dataBytes: Array = Array(data) - let dataEndIndex: Int = (dataBytes.count - suffixData.count) - let dataStartIndex: Int = (dataEndIndex - finalDataLength) - let finalDataBytes: ArraySlice = dataBytes[dataStartIndex.. (info: ResponseInfo, body: Data?)? { + // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data + // into parts to properly process it + guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { + return nil + } + + let stringParts: [String.SubSequence] = responseString.split(separator: ":") + + guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { + return nil + } + + let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) + let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) + let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { + return (responseInfo, nil) + } + + // Extract the response data as well + let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) + let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") + + guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]), let suffixData: Data = "e".data(using: .utf8) else { + return nil + } + + let dataBytes: Array = Array(data) + let dataEndIndex: Int = (dataBytes.count - suffixData.count) + let dataStartIndex: Int = (dataEndIndex - finalDataLength) + let finalDataBytes: ArraySlice = dataBytes[dataStartIndex..