From 4dddeebabc076b1e703e8bb45523864c661b5751 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 6 Feb 2025 11:48:02 +1100 Subject: [PATCH 01/24] fix highlight background and add paddings to the trailing of @You --- Session/Utilities/MentionUtilities.swift | 1 + .../HighlightMentionBackgroundView.swift | 31 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Session/Utilities/MentionUtilities.swift b/Session/Utilities/MentionUtilities.swift index e7beb820c..2cd90cc61 100644 --- a/Session/Utilities/MentionUtilities.swift +++ b/Session/Utilities/MentionUtilities.swift @@ -110,6 +110,7 @@ public enum MentionUtilities { result.addAttribute(.currentUserMentionBackgroundCornerRadius, value: (8 * sizeDiff), range: mention.range) result.addAttribute(.currentUserMentionBackgroundPadding, value: (3 * sizeDiff), range: mention.range) result.addAttribute(.currentUserMentionBackgroundColor, value: primaryColor.color, range: mention.range) + result.addAttribute(.kern, value: (3 * sizeDiff), range: NSRange(location: mention.range.upperBound, length: 1)) } switch (location, mention.isCurrentUser, theme.interfaceStyle) { diff --git a/SessionUIKit/Components/HighlightMentionBackgroundView.swift b/SessionUIKit/Components/HighlightMentionBackgroundView.swift index 289787c21..0eba66101 100644 --- a/SessionUIKit/Components/HighlightMentionBackgroundView.swift +++ b/SessionUIKit/Components/HighlightMentionBackgroundView.swift @@ -85,6 +85,9 @@ public class HighlightMentionBackgroundView: UIView { var origins = [CGPoint](repeating: .zero, count: lines.count) CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) + + var currentMentionBounds: CGRect? = nil // Store mention bounding box + var lastMentionBackgroundColor: UIColor = .clear for lineIndex in 0.. Date: Thu, 13 Feb 2025 09:51:58 +1100 Subject: [PATCH 02/24] fix highlight mention background for RTL --- .../HighlightMentionBackgroundView.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SessionUIKit/Components/HighlightMentionBackgroundView.swift b/SessionUIKit/Components/HighlightMentionBackgroundView.swift index 0eba66101..88076382a 100644 --- a/SessionUIKit/Components/HighlightMentionBackgroundView.swift +++ b/SessionUIKit/Components/HighlightMentionBackgroundView.swift @@ -88,6 +88,7 @@ public class HighlightMentionBackgroundView: UIView { var currentMentionBounds: CGRect? = nil // Store mention bounding box var lastMentionBackgroundColor: UIColor = .clear + var lastMentionBackgroundCornerRadius: CGFloat = 0 for lineIndex in 0.. Date: Thu, 13 Feb 2025 14:56:06 +1100 Subject: [PATCH 03/24] make ons with emoji work by punycode --- Session.xcodeproj/project.pbxproj | 16 +++++++++++ .../xcshareddata/swiftpm/Package.resolved | 11 +++++++- .../Settings.bundle/ThirdPartyLicenses.plist | 27 +++++++++++++++++++ SessionSnodeKit/Networking/SnodeAPI.swift | 3 ++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index fdad4025a..c43ac8bba 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -189,6 +189,7 @@ 94367C452C6C828500814252 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 94367C422C6C828500814252 /* Localizable.xcstrings */; }; 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; }; 943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D832B86B5F1004ACE64 /* Localization.swift */; }; + 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; @@ -2205,6 +2206,7 @@ buildActionMask = 2147483647; files = ( C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */, + 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */, FD7F74602BAAA4C7006DDFD8 /* libSessionUtil.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5092,6 +5094,7 @@ FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */, FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */, FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */, + 946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */, ); productRefGroup = D221A08A169C9E5E00537ABF /* Products */; projectDirPath = ""; @@ -8607,6 +8610,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gumob/PunycodeSwift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CocoaLumberjack/CocoaLumberjack.git"; @@ -8706,6 +8717,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 946F5A722D5DA3AC00A5ADCE /* Punycode */ = { + isa = XCSwiftPackageProductDependency; + package = 946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */; + productName = Punycode; + }; FD22866E2C38D42300BC06F7 /* DifferenceKit */ = { isa = XCSwiftPackageProductDependency; package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a42376cd9..596de415f 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4c95b434de06c87c75c3ef96fa055ec67e885a4a4ad78caafd7925b131995b07", + "originHash" : "c367966592f0222dd9924e5d3ce8474e48169d444725d2da11654a281f8f2cef", "pins" : [ { "identity" : "cocoalumberjack", @@ -73,6 +73,15 @@ "version" : "5.2.0" } }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", + "version" : "3.0.0" + } + }, { "identity" : "quick", "kind" : "remoteSourceControl", diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 1dcc5edf1..4886d53a6 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -567,6 +567,33 @@ SOFTWARE. Title NVActivityIndicatorView + + License + MIT License + +Copyright (c) 2018 Gumob + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + PunycodeSwift + License Apache License diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index c2ceb8f0f..22bcc18c1 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -5,6 +5,7 @@ import Foundation import Combine import GRDB +import Punycode import SessionUtilitiesKit public extension Network.RequestType { @@ -344,7 +345,7 @@ public final class SnodeAPI { let validationCount = 3 // The name must be lowercased - let onsName = onsName.lowercased() + let onsName = onsName.lowercased().idnaEncoded ?? onsName.lowercased() // Hash the ONS name using BLAKE2b guard From 6788bb3cb1bcdf6173a4f88284cb67cdd7731d27 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 27 Feb 2025 11:51:51 +1100 Subject: [PATCH 04/24] add accessibility id and label to cells in share to Session screen --- SessionShareExtension/SimplifiedConversationCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 3a561c433..fddcd2126 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -94,5 +94,9 @@ final class SimplifiedConversationCell: UITableViewCell { additionalProfile: cellViewModel.additionalProfile ) displayNameLabel.text = cellViewModel.displayName + + self.isAccessibilityElement = true + self.accessibilityIdentifier = "Contact" + self.accessibilityLabel = cellViewModel.displayName } } From 4b5eb7755c4dd1b74c782351a44ff2c22565eeae Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 5 Mar 2025 16:04:26 +1100 Subject: [PATCH 05/24] Sort group member change control messages deterministically --- .../Database/Models/ClosedGroup.swift | 22 +++++ .../MessageReceiver+Groups.swift | 2 +- .../MessageSender+Groups.swift | 85 ++++++++++--------- .../MessageSenderGroupsSpec.swift | 47 ++++++++++ 4 files changed, 113 insertions(+), 43 deletions(-) diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 52e7c2c9f..d83162556 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -625,6 +625,28 @@ public extension ClosedGroup { } } +public extension Collection where Element == (String, Profile?) { + func sortedById(userSessionId: SessionId) -> [Element] { + return sorted { lhs, rhs in + guard lhs.0 != userSessionId.hexString else { return true } + guard rhs.0 != userSessionId.hexString else { return false } + + return (lhs.0 < rhs.0) + } + } +} + +public extension Collection where Element == String { + func sortedById(userSessionId: SessionId) -> [Element] { + return sorted { lhs, rhs in + guard lhs != userSessionId.hexString else { return true } + guard rhs != userSessionId.hexString else { return false } + + return (lhs < rhs) + } + } +} + public extension [ClosedGroup.RemovableGroupData] { static var allData: [ClosedGroup.RemovableGroupData] { ClosedGroup.RemovableGroupData.allCases } static var noData: [ClosedGroup.RemovableGroupData] { [] } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 7cc50f820..6dc53554c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -433,7 +433,7 @@ extension MessageReceiver { .defaulting(to: []) .reduce(into: [:]) { result, next in result[next.id] = next } let names: [String] = message.memberSessionIds - .sorted { lhs, rhs in lhs == userSessionId.hexString } + .sortedById(userSessionId: userSessionId) .map { id in profiles[id]?.displayName(for: .group) ?? Profile.truncated(id: id, truncating: .middle) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 3fcc48aec..5e15c5757 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -23,6 +23,11 @@ extension MessageSender { members: [(String, Profile?)], using dependencies: Dependencies ) -> AnyPublisher { + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let sortedOtherMembers: [(String, Profile?)] = members + .filter { id, _ in id != userSessionId.hexString } + .sortedById(userSessionId: userSessionId) + return Just(()) .setFailureType(to: Error.self) .flatMap { _ -> AnyPublisher in @@ -40,8 +45,6 @@ extension MessageSender { } .flatMap { (displayPictureInfo: DisplayPictureManager.UploadResult?) -> AnyPublisher in dependencies[singleton: .storage].writePublisher { db -> PreparedGroupData in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - /// Create and cache the libSession entries let createdInfo: LibSession.CreatedGroupInfo = try LibSession.createGroup( db, @@ -77,12 +80,10 @@ extension MessageSender { body: ClosedGroup.MessageInfo .addedUsers( hasCurrentUser: false, - names: members - .filter { id, _ in id != userSessionId.hexString } - .map { id, profile in - profile?.displayName(for: .group) ?? - Profile.truncated(id: id, truncating: .middle) - }, + names: sortedOtherMembers.map { id, profile in + profile?.displayName(for: .group) ?? + Profile.truncated(id: id, truncating: .middle) + }, historyShared: false ) .infoString(using: dependencies), @@ -101,9 +102,7 @@ extension MessageSender { destination: .closedGroup(groupPublicKey: createdInfo.group.id), message: GroupUpdateMemberChangeMessage( changeType: .added, - memberSessionIds: members - .filter { id, _ -> Bool in id != userSessionId.hexString } - .map { id, _ in id }, + memberSessionIds: sortedOtherMembers.map { id, _ in id }, historyShared: false, sentTimestampMs: UInt64(createdInfo.group.formationTimestamp * 1000), authMethod: Authentication.groupAdmin( @@ -553,7 +552,7 @@ extension MessageSender { public static func addGroupMembers( groupSessionId: String, - members: [(id: String, profile: Profile?)], + members: [(String, Profile?)], allowAccessToHistoricMessages: Bool, using dependencies: Dependencies ) -> AnyPublisher { @@ -568,6 +567,10 @@ extension MessageSender { subaccountToken: [UInt8] ) + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let sortedMembers: [(String, Profile?)] = members + .sortedById(userSessionId: userSessionId) + return dependencies[singleton: .storage] .writePublisher { db -> ([MemberJobData], Network.PreparedRequest, Network.PreparedRequest?) in guard @@ -578,7 +581,6 @@ extension MessageSender { .fetchOne(db) else { throw MessageSenderError.invalidClosedGroupUpdate } - let userSessionId: SessionId = dependencies[cache: .general].sessionId let changeTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() var maybeSupplementalKeyRequest: Network.PreparedRequest? @@ -601,7 +603,7 @@ extension MessageSender { let supplementData: Data = try LibSession.keySupplement( db, groupSessionId: sessionId, - memberIds: members.map { $0.id }.asSet(), + memberIds: members.map { id, _ in id }.asSet(), using: dependencies ) @@ -708,13 +710,11 @@ extension MessageSender { variant: .infoGroupMembersUpdated, body: ClosedGroup.MessageInfo .addedUsers( - hasCurrentUser: members.map { $0.id }.contains(userSessionId.hexString), - names: members - .sorted { lhs, rhs in lhs.id == userSessionId.hexString } - .map { id, profile in - profile?.displayName(for: .group) ?? - Profile.truncated(id: id, truncating: .middle) - }, + hasCurrentUser: members.contains { id, _ in id == userSessionId.hexString }, + names: sortedMembers.map { id, profile in + profile?.displayName(for: .group) ?? + Profile.truncated(id: id, truncating: .middle) + }, historyShared: allowAccessToHistoricMessages ) .infoString(using: dependencies), @@ -737,7 +737,7 @@ extension MessageSender { destination: .closedGroup(groupPublicKey: sessionId.hexString), message: GroupUpdateMemberChangeMessage( changeType: .added, - memberSessionIds: members.map { $0.id }, + memberSessionIds: sortedMembers.map { id, _ in id }, historyShared: allowAccessToHistoricMessages, sentTimestampMs: UInt64(changeTimestampMs), authMethod: Authentication.groupAdmin( @@ -979,6 +979,9 @@ extension MessageSender { dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ) + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let sortedMemberIds: [String] = memberIds.sortedById(userSessionId: userSessionId) + return dependencies[singleton: .storage] .writePublisher { db in guard @@ -1028,7 +1031,6 @@ extension MessageSender { /// Send the member changed message if desired if sendMemberChangedMessage { - let userSessionId: SessionId = dependencies[cache: .general].sessionId let removedMemberProfiles: [String: Profile] = (try? Profile .filter(ids: memberIds) .fetchAll(db)) @@ -1045,12 +1047,10 @@ extension MessageSender { body: ClosedGroup.MessageInfo .removedUsers( hasCurrentUser: memberIds.contains(userSessionId.hexString), - names: memberIds - .sorted { lhs, rhs in lhs == userSessionId.hexString } - .map { id in - removedMemberProfiles[id]?.displayName(for: .group) ?? - Profile.truncated(id: id, truncating: .middle) - } + names: sortedMemberIds.map { id in + removedMemberProfiles[id]?.displayName(for: .group) ?? + Profile.truncated(id: id, truncating: .middle) + } ) .infoString(using: dependencies), timestampMs: targetChangeTimestampMs, @@ -1072,7 +1072,7 @@ extension MessageSender { destination: .closedGroup(groupPublicKey: sessionId.hexString), message: GroupUpdateMemberChangeMessage( changeType: .removed, - memberSessionIds: Array(memberIds), + memberSessionIds: sortedMemberIds, historyShared: false, sentTimestampMs: UInt64(targetChangeTimestampMs), authMethod: Authentication.groupAdmin( @@ -1098,10 +1098,12 @@ extension MessageSender { public static func promoteGroupMembers( groupSessionId: SessionId, - members: [(id: String, profile: Profile?)], + members: [(String, Profile?)], isResend: Bool, using dependencies: Dependencies ) -> AnyPublisher { + let userSessionId: SessionId = dependencies[cache: .general].sessionId + return dependencies[singleton: .storage] .writePublisher { db -> Set in guard @@ -1113,7 +1115,7 @@ extension MessageSender { else { throw MessageSenderError.invalidClosedGroupUpdate } /// Determine which members actually need to be promoted (rather than just resent promotions) - let memberIds: Set = Set(members.map(\.id)) + let memberIds: Set = Set(members.map { id, _ in id }) let memberIdsRequiringPromotions: Set = try GroupMember .select(.profileId) .filter(GroupMember.Columns.groupId == groupSessionId.hexString) @@ -1121,8 +1123,10 @@ extension MessageSender { .filter(GroupMember.Columns.role == GroupMember.Role.standard) .asRequest(of: String.self) .fetchSet(db) - let membersReceivingPromotions: [(id: String, profile: Profile?)] = members + let membersReceivingPromotions: [(String, Profile?)] = members .filter { id, _ in memberIdsRequiringPromotions.contains(id) } + let sortedMembersReceivingPromotions: [(String, Profile?)] = membersReceivingPromotions + .sortedById(userSessionId: userSessionId) /// Perform the config changes without triggering a config sync (we will do so manually after the process completes) try dependencies.mutate(cache: .libSession) { cache in @@ -1173,7 +1177,6 @@ extension MessageSender { /// that are getting promotions re-sent to them - we only want to send an admin changed message if there /// is a newly promoted member if !isResend && !membersReceivingPromotions.isEmpty { - let userSessionId: SessionId = dependencies[cache: .general].sessionId let changeTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let disappearingConfig: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration.fetchOne(db, id: groupSessionId.hexString) @@ -1185,14 +1188,12 @@ extension MessageSender { body: ClosedGroup.MessageInfo .promotedUsers( hasCurrentUser: membersReceivingPromotions - .map { $0.id } + .map { id, _ in id } .contains(userSessionId.hexString), - names: membersReceivingPromotions - .sorted { lhs, rhs in lhs.id == userSessionId.hexString } - .map { id, profile in - profile?.displayName(for: .group) ?? - Profile.truncated(id: id, truncating: .middle) - } + names: sortedMembersReceivingPromotions.map { id, profile in + profile?.displayName(for: .group) ?? + Profile.truncated(id: id, truncating: .middle) + } ) .infoString(using: dependencies), timestampMs: changeTimestampMs, @@ -1214,7 +1215,7 @@ extension MessageSender { destination: .closedGroup(groupPublicKey: groupSessionId.hexString), message: GroupUpdateMemberChangeMessage( changeType: .promoted, - memberSessionIds: membersReceivingPromotions.map { $0.id }, + memberSessionIds: sortedMembersReceivingPromotions.map { id, _ in id }, historyShared: false, sentTimestampMs: UInt64(changeTimestampMs), authMethod: Authentication.groupAdmin( diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index c75259a25..dcd4a68ab 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -1433,6 +1433,53 @@ class MessageSenderGroupsSpec: QuickSpec { ) }) } + + // MARK: ---- sorts the members in the control message deterministically + it("sorts the members in the control message deterministically") { + MessageSender.addGroupMembers( + groupSessionId: groupId.hexString, + members: [ + ("051234111111111111111111111111111111111111111111111111111111111112", nil), + ("051111111111111111111111111111111111111111111111111111111111111112", nil), + ("05\(TestConstants.publicKey)", nil) + ], + allowAccessToHistoricMessages: false, + using: dependencies + ).sinkUntilComplete() + + expect(mockJobRunner) + .to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in + jobRunner.add( + .any, + job: Job( + variant: .messageSend, + behaviour: .runOnceAfterConfigSyncIgnoringPermanentFailure, + threadId: groupId.hexString, + details: MessageSendJob.Details( + destination: .closedGroup(groupPublicKey: groupId.hexString), + message: try GroupUpdateMemberChangeMessage( + changeType: .added, + memberSessionIds: [ + "05\(TestConstants.publicKey)", + "051111111111111111111111111111111111111111111111111111111111111112", + "051234111111111111111111111111111111111111111111111111111111111112" + ], + historyShared: false, + sentTimestampMs: UInt64(1234567890000), + authMethod: Authentication.groupAdmin( + groupSessionId: SessionId(.group, hex: groupId.hexString), + ed25519SecretKey: [1, 2, 3] + ), + using: dependencies + ), + requiredConfigSyncVariant: .groupMembers + ) + ), + dependantJob: nil, + canStartJob: false + ) + }) + } } } } From 8649dc084589058bcb66505ab2a5d0e78ad41a51 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 5 Mar 2025 17:20:29 +1100 Subject: [PATCH 06/24] Fixed an issue where the network status indicator wouldn't update --- Session/Path/PathStatusView.swift | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index c9d249839..40d0f6348 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -39,8 +39,8 @@ final class PathStatusView: UIView { super.init(frame: .zero) - setStatus(to: .unknown) // Default to the unknown status setUpViewHierarchy() + setStatus(to: .unknown) // Default to the unknown status registerObservers() } @@ -51,10 +51,23 @@ final class PathStatusView: UIView { // MARK: - Layout private func setUpViewHierarchy() { - layer.cornerRadius = (self.size.pointSize / 2) - layer.masksToBounds = false self.set(.width, to: self.size.pointSize) self.set(.height, to: self.size.pointSize) + + layer.cornerRadius = (self.size.pointSize / 2) + layer.masksToBounds = false + layer.shadowOffset = CGSize(width: 0, height: 0.8) + layer.shadowPath = UIBezierPath( + ovalIn: CGRect( + origin: CGPoint.zero, + size: CGSize(width: self.size.pointSize, height: self.size.pointSize) + ) + ).cgPath + + ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in + self?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1) + self?.layer.shadowRadius = (self?.size.offset(for: theme.interfaceStyle) ?? 0) + } } // MARK: - Functions @@ -62,7 +75,6 @@ final class PathStatusView: UIView { private func registerObservers() { /// Register for status updates (will be called immediately with current status) dependencies[cache: .libSessionNetwork].networkStatus - .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) .receive(on: DispatchQueue.main, using: dependencies) .sink( receiveCompletion: { [weak self] _ in @@ -80,18 +92,6 @@ final class PathStatusView: UIView { private func setStatus(to status: NetworkStatus) { themeBackgroundColor = status.themeColor layer.themeShadowColor = status.themeColor - layer.shadowOffset = CGSize(width: 0, height: 0.8) - layer.shadowPath = UIBezierPath( - ovalIn: CGRect( - origin: CGPoint.zero, - size: CGSize(width: self.size.pointSize, height: self.size.pointSize) - ) - ).cgPath - - ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in - self?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1) - self?.layer.shadowRadius = (self?.size.offset(for: theme.interfaceStyle) ?? 0) - } } } From f67c754088ad4a628fbf7116ff361219eb360376 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 6 Mar 2025 08:59:10 +1100 Subject: [PATCH 07/24] Fixed an issue where the read receipt wouldn't show --- .../Database/Models/Interaction.swift | 39 ++++++++++++++----- .../Types/PagedDatabaseObserver.swift | 3 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index c1d097449..83ad8df77 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -657,23 +657,44 @@ public extension Interaction { ) throws -> Set { guard db[.areReadReceiptsEnabled] == true else { return [] } + struct InterationRowState: Codable, FetchableRecord { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey { + case rowId + case state + } + + var rowId: Int64 + var state: Interaction.State + } + // Get the row ids for the interactions which should be updated - let rowIds: [Int64] = try Interaction - .select(Column.rowID) + let interactionInfo: [InterationRowState] = try Interaction + .select(Column.rowID.forKey(InterationRowState.Columns.rowId), Interaction.Columns.state) .filter(Interaction.Columns.threadId == threadId) .filter(timestampMsValues.contains(Columns.timestampMs)) .filter(Variant.variantsWhichSupportReadReceipts.contains(Columns.variant)) - .asRequest(of: Int64.self) + .asRequest(of: InterationRowState.self) .fetchAll(db) - // If there were no 'rowIds' then no need to run the below queries, all of the timestamps - // and for pending read receipts - guard !rowIds.isEmpty else { return timestampMsValues.asSet() } + // If there were no 'interactionInfo' then no need to run the below queries, all of the + // timestamps are for pending read receipts + guard !interactionInfo.isEmpty else { return timestampMsValues.asSet() } + + let allRowIds: Set = Set(interactionInfo.map { $0.rowId }) + let sentInteractionIds: Set = interactionInfo + .filter { $0.state != .sending } + .map { $0.rowId } + .asSet() + let sendingInteractionInfo: Set = interactionInfo + .filter { $0.state == .sending } + .map { $0.rowId } + .asSet() // Update the 'recipientReadTimestampMs' if it doesn't match (need to do this to prevent // the UI update from being triggered for a redundant update) try Interaction - .filter(rowIds.contains(Column.rowID)) + .filter(sentInteractionIds.contains(Column.rowID)) .filter(Interaction.Columns.recipientReadTimestampMs == nil) .updateAll( db, @@ -683,7 +704,7 @@ public extension Interaction { // If the message still appeared to be sending then mark it as sent (can also remove the // failure text as it's redundant if the message is in the sent state) try Interaction - .filter(rowIds.contains(Column.rowID)) + .filter(sendingInteractionInfo.contains(Column.rowID)) .filter(Interaction.Columns.state == Interaction.State.sending) .updateAll( db, @@ -694,7 +715,7 @@ public extension Interaction { // Retrieve the set of timestamps which were updated let timestampsUpdated: Set = try Interaction .select(Columns.timestampMs) - .filter(rowIds.contains(Column.rowID)) + .filter(allRowIds.contains(Column.rowID)) .filter(timestampMsValues.contains(Columns.timestampMs)) .filter(Variant.variantsWhichSupportReadReceipts.contains(Columns.variant)) .asRequest(of: Int64.self) diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index f0dd0ff29..2b00cea0c 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -99,7 +99,8 @@ public class PagedDatabaseObserver: TransactionObserver where .reduce(into: [:]) { (prev: inout [String: Set], next: PagedData.ObservedChanges) in guard !next.columns.isEmpty else { return } - prev[next.databaseTableName] = next.columns.asSet() + prev[next.databaseTableName] = (prev[next.databaseTableName] ?? []) + .inserting(contentsOf: next.columns.asSet()) } self.observedDeletes = allObservedChanges .filter { $0.events.contains(.delete) } From 4b54d242fe14e6b38f0b1a2808b22f0f1a78b262 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Mar 2025 10:50:00 +1100 Subject: [PATCH 08/24] Updated the placeholder icon cache key to include the 'initials' --- SessionUIKit/Components/PlaceholderIcon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionUIKit/Components/PlaceholderIcon.swift b/SessionUIKit/Components/PlaceholderIcon.swift index dfa3089ff..c9eaaed06 100644 --- a/SessionUIKit/Components/PlaceholderIcon.swift +++ b/SessionUIKit/Components/PlaceholderIcon.swift @@ -60,7 +60,7 @@ public class PlaceholderIcon { .compactMap { word in word.first.map { String($0) } } .joined() - return SNUIKit.placeholderIconCacher(cacheKey: "\(seed)-\(Int(floor(size)))") { + return SNUIKit.placeholderIconCacher(cacheKey: "\(seed)-\(initials)-\(Int(floor(size)))") { let layer = icon.generateLayer( with: size, text: (initials.count >= 2 ? From cb528b0616a22aee05c9840f93ba436609d61744 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Mar 2025 11:16:17 +1100 Subject: [PATCH 09/24] Refactored the 'findFrontMostViewController' function --- .../Utilities/UIViewController+OWS.swift | 67 +++++++++---------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift index 00b246e35..4b6ea451a 100644 --- a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift @@ -7,54 +7,49 @@ import SessionUtilitiesKit public extension UIViewController { func findFrontMostViewController(ignoringAlerts: Bool) -> UIViewController { var visitedViewControllers: [UIViewController] = [] - var viewController: UIViewController = self while true { visitedViewControllers.append(viewController) - var nextViewController: UIViewController? = viewController.presentedViewController - - if - let topBannerController: TopBannerController = nextViewController as? TopBannerController, - !topBannerController.children.isEmpty - { - nextViewController = ( - topBannerController.children[0].presentedViewController ?? - topBannerController.children[0] - ) + func shouldSkipController(_ controller: UIViewController) -> Bool { + return ignoringAlerts && controller is UIAlertController } - if let nextViewController: UIViewController = nextViewController { - if !ignoringAlerts || !(nextViewController is UIAlertController) { - if visitedViewControllers.contains(nextViewController) { - // Cycle detected - return viewController - } - - viewController = nextViewController - continue - } + func tryAdvance(to next: UIViewController) -> Bool { + guard !shouldSkipController(next) else { return false } + guard !visitedViewControllers.contains(next) else { return false } // Loop prevention + + viewController = next + return true } - if let navController: UINavigationController = viewController as? UINavigationController { - nextViewController = navController.topViewController - - if let nextViewController: UIViewController = nextViewController { - if !ignoringAlerts || !(nextViewController is UIAlertController) { - if visitedViewControllers.contains(nextViewController) { - // Cycle detected - return viewController - } - - viewController = nextViewController - continue - } - } + // Check if current viewController is an alert we should ignore + guard !shouldSkipController(viewController) else { break } + + // Handle TopBannerController + if let topBanner: TopBannerController = viewController as? TopBannerController, !topBanner.children.isEmpty { + let child: UIViewController = topBanner.children[0] + let next: UIViewController = (child.presentedViewController ?? child) - break + guard tryAdvance(to: next) else { break } + continue + } + + // Handle presented view controller + if let presented: UIViewController = viewController.presentedViewController { + guard tryAdvance(to: presented) else { break } + continue + } + + // Handle navigation controller + if let navController = viewController as? UINavigationController, + let topViewController = navController.topViewController { + guard tryAdvance(to: topViewController) else { break } + continue } + // No more view controllers to traverse break } From 4435240d2bbe1104739d67efa300c679f0c665b4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Mar 2025 12:00:45 +1100 Subject: [PATCH 10/24] Fixed a number of background processing and polling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Tweaked some background poller logs • Refactored the `BackgroundFetch` handling to use a `DispatchSourceTimer` instead of `NSTimer` and use a specific `DispatchQueue` to avoid race conditions • Refactored the BackgroundTaskManager to use `DispatchQueue` and `DispatchSourceTimer` and removed unused code (was seeing background tasks incorrectly running too long so wanted to clean it up) • Fixed an issue where pollers would incorrectly be released during background polling • Fixed an issue where the background poller wouldn't update the app notification badge count • Fixed an issue where the community pollers 'timeSinceLastPoll' was incorrectly being given both an epoch timestamp as well as a duration since the last poll (resulting in always just refetching recent messages) • Fixed an issue where the community poller wasn't updating the last poll timestamp (also renamed some functions to make them clearer) • Fixed an issue where pollers could incorrectly be started in the background (eg. when receiving a PN) --- Session/Home/HomeVC.swift | 2 +- Session/Meta/AppDelegate.swift | 71 +++-- .../PushRegistrationManager.swift | 2 +- Session/Utilities/BackgroundPoller.swift | 63 ++-- .../Open Groups/OpenGroupManager.swift | 25 +- .../Pollers/CommunityPoller.swift | 14 +- .../Pollers/PollerType.swift | 11 +- .../Open Groups/OpenGroupManagerSpec.swift | 43 ++- .../Pollers/CommunityPollerSpec.swift | 11 +- .../_TestUtilities/MockOGMCache.swift | 8 +- .../Types/BackgroundTaskManager.swift | 296 ++++++++---------- 11 files changed, 293 insertions(+), 253 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 5419046d5..f4eb61731 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -353,7 +353,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi if Identity.userExists(using: viewModel.dependencies), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, - !viewModel.dependencies[singleton: .appContext].isNotInForeground + viewModel.dependencies[singleton: .appContext].isMainAppAndActive { appDelegate.startPollersIfNeeded() } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index e39048605..c2df5b9bb 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -318,26 +318,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Background Fetching func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + /// It seems like it's possible for this function to be called with an invalid `backgroundTimeRemaining` value + /// (`TimeInterval.greatestFiniteMagnitude`) in which case we just want to mark it as a failure + /// + /// Additionally we want to ensure that our timeout timer has enough time to run so make sure we have at least `5 seconds` + /// of background execution (if we don't then the process could incorrectly run longer than it should) + guard + application.backgroundTimeRemaining < TimeInterval.greatestFiniteMagnitude && + application.backgroundTimeRemaining > 5 + else { return completionHandler(.failed) } + Log.appResumedExecution() Log.info(.backgroundPoller, "Starting background fetch.") dependencies[singleton: .storage].resumeDatabaseAccess() dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } - let queue: DispatchQueue = .global(qos: .userInitiated) + let queue: DispatchQueue = DispatchQueue(label: "com.session.backgroundPoll") let poller: BackgroundPoller = BackgroundPoller() var cancellable: AnyCancellable? - // 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 - // database again before the background task ends so we start a timer that expires 1 second - // before the background task is due to expire in order to do so - let cancelTimer: Timer = Timer.scheduledTimerOnMainThread( - withTimeInterval: (application.backgroundTimeRemaining - 5), - repeats: false, - using: dependencies - ) { [poller, dependencies] timer in - timer.invalidate() - + /// 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 database again before the background task ends so we start + /// a timer that expires before the background task is due to expire in order to do so + /// + /// **Note:** We **MUST** capture both `poller` and `cancellable` strongly in the event handler to ensure neither + /// go out of scope until we want them to (we essentually want a retain cycle in this case) + let durationRemainingMs: Int = max(1, Int((application.backgroundTimeRemaining - 5) * 1000)) + let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(queue: queue) + timer.schedule(deadline: .now() + .milliseconds(durationRemainingMs)) + timer.setEventHandler { [poller, dependencies] in guard cancellable != nil else { return } Log.info(.backgroundPoller, "Background poll failed due to manual timeout.") @@ -352,32 +361,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD _ = poller // Capture poller to ensure it doesn't go out of scope completionHandler(.failed) } + timer.resume() dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies, poller] in - // If the 'AppReadiness' process takes too long then it's possible for the user to open - // the app after this closure is registered but before it's actually triggered - this can - // result in the `BackgroundPoller` incorrectly getting called in the foreground, this check - // is here to prevent that + /// If the 'AppReadiness' process takes too long then it's possible for the user to open the app after this closure is registered + /// but before it's actually triggered - this can result in the `BackgroundPoller` incorrectly getting called in the foreground, + /// this check is here to prevent that guard dependencies[singleton: .appContext].isInBackground else { return } + /// Kick off the `BackgroundPoller` + /// + /// **Note:** We **MUST** capture both `poller` and `timer` strongly in the completion handler to ensure neither + /// go out of scope until we want them to (we essentually want a retain cycle in this case) cancellable = poller .poll(using: dependencies) .subscribe(on: queue, using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) + .receive(on: queue, using: dependencies) .sink( - receiveCompletion: { [poller] result in + receiveCompletion: { [timer, poller] result in // Ensure we haven't timed out yet - guard cancelTimer.isValid else { return } + guard timer.isCancelled == false else { return } + + // Immediately cancel the timer to prevent the timeout being triggered + timer.cancel() + // Update the unread count badge + let unreadCount: Int = dependencies[singleton: .storage] + .read { db in try Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) } + .defaulting(to: 0) + + DispatchQueue.main.async(using: dependencies) { + UIApplication.shared.applicationIconBadgeNumber = unreadCount + } + + // If we are still running in the background then suspend the network & database if dependencies[singleton: .appContext].isInBackground { dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } dependencies[singleton: .storage].suspendDatabaseAccess() Log.flush() } - cancelTimer.invalidate() _ = poller // Capture poller to ensure it doesn't go out of scope + // Complete the background task switch result { case .failure: completionHandler(.failed) case .finished: completionHandler(.newData) @@ -850,16 +876,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Polling - public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) { + public func startPollersIfNeeded() { guard dependencies[cache: .onboarding].state == .completed else { return } /// Start the pollers on a background thread so that any database queries they need to run don't /// block the main thread DispatchQueue.global(qos: .background).async { [dependencies] in dependencies[singleton: .currentUserPoller].startIfNeeded() - - guard shouldStartGroupPollers else { return } - dependencies.mutate(cache: .groupPollers) { $0.startAllPollers() } dependencies.mutate(cache: .communityPollers) { $0.startAllPollers() } } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index a8bcbd240..96f5d850b 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -315,7 +315,7 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { dependencies[singleton: .jobRunner].appDidBecomeActive() // NOTE: Just start 1-1 poller so that it won't wait for polling group messages - (UIApplication.shared.delegate as? AppDelegate)?.startPollersIfNeeded(shouldStartGroupPollers: false) + dependencies[singleton: .currentUserPoller].startIfNeeded(forceStartInBackground: true) call.reportIncomingCallIfNeeded { error in if let error = error { diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 9def82740..bb30cb259 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -18,11 +18,17 @@ public extension Log.Category { // MARK: - BackgroundPoller public final class BackgroundPoller { + typealias Pollers = ( + currentUser: CurrentUserPoller, + groups: [GroupPoller], + communities: [CommunityPoller] + ) + public func poll(using dependencies: Dependencies) -> AnyPublisher { let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 return dependencies[singleton: .storage] - .readPublisher { db -> (Set, Set) in + .readPublisher { db -> (Set, Set, [String]) in ( try ClosedGroup .select(.threadId) @@ -46,16 +52,36 @@ public final class BackgroundPoller { ) .distinct() .asRequest(of: String.self) - .fetchSet(db) + .fetchSet(db), + try OpenGroup + .select(.roomToken) + .filter( + OpenGroup.Columns.roomToken != "" && + OpenGroup.Columns.isActive && + OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll + ) + .distinct() + .asRequest(of: String.self) + .fetchAll(db) ) } - .catch { _ in Just(([], [])).eraseToAnyPublisher() } + .catch { _ in Just(([], [], [])).eraseToAnyPublisher() } .handleEvents( - receiveOutput: { groupIds, servers in - Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count).") + receiveOutput: { groupIds, servers, rooms in + Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count) (\(rooms.count) room(s)).") } ) - .map { groupIds, servers -> ([GroupPoller], [CommunityPoller]) in + .map { groupIds, servers, _ -> Pollers in + let currentUserPoller: CurrentUserPoller = CurrentUserPoller( + pollerName: "Background Main Poller", + pollerQueue: DispatchQueue.main, + pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString), + pollerDrainBehaviour: .limitedReuse(count: 6), + namespaces: CurrentUserPoller.namespaces, + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) let groupPollers: [GroupPoller] = groupIds.map { groupId in GroupPoller( pollerName: "Background Group poller for: \(groupId)", // stringlint:ignore @@ -80,16 +106,18 @@ public final class BackgroundPoller { ) } - return (groupPollers, communityPollers) + return (currentUserPoller, groupPollers, communityPollers) } - .flatMap { groupPollers, communityPollers in + .flatMap { currentUserPoller, groupPollers, communityPollers in + /// Need to map back to the pollers to ensure they don't get released until after the polling finishes Publishers.MergeMany( - [BackgroundPoller.pollUserMessages(using: dependencies)] + [BackgroundPoller.pollUserMessages(poller: currentUserPoller, using: dependencies)] .appending(contentsOf: BackgroundPoller.poll(pollers: groupPollers, using: dependencies)) .appending(contentsOf: BackgroundPoller.poll(pollerInfo: communityPollers, using: dependencies)) ) + .collect() + .map { _ in (currentUserPoller, groupPollers, communityPollers) } } - .collect() .map { _ in () } .handleEvents( receiveOutput: { _ in @@ -102,18 +130,9 @@ public final class BackgroundPoller { } private static func pollUserMessages( + poller: CurrentUserPoller, using dependencies: Dependencies ) -> AnyPublisher { - let poller: CurrentUserPoller = CurrentUserPoller( - pollerName: "Background Main Poller", - pollerQueue: DispatchQueue.main, - pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString), - pollerDrainBehaviour: .limitedReuse(count: 6), - namespaces: CurrentUserPoller.namespaces, - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies - ) let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 return poller @@ -182,10 +201,10 @@ public final class BackgroundPoller { return poller .pollFromBackground() .handleEvents( - receiveOutput: { [pollerName = poller.pollerName] _ in + receiveOutput: { [pollerName = poller.pollerName] _, _, rawMessageCount, _ in let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info(.backgroundPoller, "\(pollerName) succeeded after \(duration, unit: .s).") + Log.info(.backgroundPoller, "\(pollerName) received \(rawMessageCount) message(s) succeeded after \(duration, unit: .s).") }, receiveCompletion: { [pollerName = poller.pollerName] result in switch result { diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 2a3486a62..41f2a4b2c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -935,7 +935,7 @@ public extension OpenGroupManager { class Cache: OGMCacheType { private let dependencies: Dependencies private let defaultRoomsSubject: CurrentValueSubject<[DefaultRoomInfo], Error> = CurrentValueSubject([]) - private var _timeSinceLastOpen: TimeInterval? + private var _lastSuccessfulCommunityPollTimestamp: TimeInterval? public var pendingChanges: [OpenGroupAPI.PendingChange] = [] public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error> { @@ -961,18 +961,22 @@ public extension OpenGroupManager { // MARK: - Functions - public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { - if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen { - return storedTimeSinceLastOpen + public func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval { + if let storedTime: TimeInterval = _lastSuccessfulCommunityPollTimestamp { + return storedTime } - guard let lastOpen: Date = dependencies[defaults: .standard, key: .lastOpen] else { - _timeSinceLastOpen = .greatestFiniteMagnitude - return .greatestFiniteMagnitude + guard let lastPoll: Date = dependencies[defaults: .standard, key: .lastOpen] else { + return 0 } - _timeSinceLastOpen = dependencies.dateNow.timeIntervalSince(lastOpen) - return dependencies.dateNow.timeIntervalSince(lastOpen) + _lastSuccessfulCommunityPollTimestamp = lastPoll.timeIntervalSince1970 + return lastPoll.timeIntervalSince1970 + } + + public func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) { + dependencies[defaults: .standard, key: .lastOpen] = Date(timeIntervalSince1970: timestamp) + _lastSuccessfulCommunityPollTimestamp = timestamp } public func setDefaultRoomInfo(_ info: [DefaultRoomInfo]) { @@ -995,6 +999,7 @@ public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType { var pendingChanges: [OpenGroupAPI.PendingChange] { get set } - func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval + func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval + func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo]) } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index b78d19f38..ccfbad27a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -247,10 +247,10 @@ public final class CommunityPoller: CommunityPollerType & PollerType { /// **Note:** The returned messages will have already been processed by the `Poller`, they are only returned /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) public func poll(forceSynchronousProcessing: Bool = false) -> AnyPublisher { - let timeSinceLastPoll: TimeInterval = (self.lastPollStart > 0 ? + let lastSuccessfulPollTimestamp: TimeInterval = (self.lastPollStart > 0 ? lastPollStart : dependencies.mutate(cache: .openGroupManager) { cache in - cache.getTimeSinceLastOpen(using: dependencies) + cache.getLastSuccessfulCommunityPollTimestamp() } ) @@ -260,7 +260,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { db, server: pollerDestination.target, hasPerformedInitialPoll: (pollCount > 0), - timeSinceLastPoll: timeSinceLastPoll, + timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp), using: dependencies ) } @@ -274,8 +274,14 @@ public final class CommunityPoller: CommunityPollerType & PollerType { ) } .handleEvents( - receiveOutput: { [weak self] _ in + receiveOutput: { [weak self, dependencies] _ in self?.pollCount += 1 + + dependencies.mutate(cache: .openGroupManager) { cache in + cache.setLastSuccessfulCommunityPollTimestamp( + dependencies.dateNow.timeIntervalSince1970 + ) + } } ) .eraseToAnyPublisher() diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 12f8ea1a9..581f03fab 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -72,7 +72,7 @@ public protocol PollerType: AnyObject { using dependencies: Dependencies ) - func startIfNeeded() + func startIfNeeded(forceStartInBackground: Bool) func stop() func pollerDidStart() @@ -84,7 +84,14 @@ public protocol PollerType: AnyObject { // MARK: - Default Implementations public extension PollerType { - func startIfNeeded() { + func startIfNeeded() { startIfNeeded(forceStartInBackground: false) } + + func startIfNeeded(forceStartInBackground: Bool) { + guard + forceStartInBackground || + dependencies[singleton: .appContext].isMainAppAndActive + else { return Log.info(.poller, "Ignoring call to start \(pollerName) due to not being active.") } + pollerQueue.async(using: dependencies) { [weak self, pollerName] in guard self?.isPolling != true else { return } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 6e8349a17..9edde7635 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -183,6 +183,7 @@ class OpenGroupManagerSpec: QuickSpec { @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.integer(forKey: .any) }.thenReturn(0) + defaults.when { $0.set(.any, forKey: .any) }.thenReturn(()) } ) @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults( @@ -199,7 +200,7 @@ class OpenGroupManagerSpec: QuickSpec { initialSetup: { cache in cache.when { $0.pendingChanges }.thenReturn([]) cache.when { $0.pendingChanges = .any }.thenReturn(()) - cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0) + cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) } ) @@ -237,20 +238,19 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: -- cache data context("cache data") { - // MARK: ---- defaults the time since last open to greatestFiniteMagnitude - it("defaults the time since last open to greatestFiniteMagnitude") { + // MARK: ---- defaults the time since last open to zero + it("defaults the time since last open to zero") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(nil) - expect(cache.getTimeSinceLastOpen(using: dependencies)) - .to(beCloseTo(.greatestFiniteMagnitude)) + expect(cache.getLastSuccessfulCommunityPollTimestamp()).to(equal(0)) } - // MARK: ---- returns the time since the last open - it("returns the time since the last open") { + // MARK: ---- returns the time since the last poll + it("returns the time since the last poll") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) @@ -258,12 +258,12 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567880)) dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) - expect(cache.getTimeSinceLastOpen(using: dependencies)) - .to(beCloseTo(10)) + expect(cache.getLastSuccessfulCommunityPollTimestamp()) + .to(equal(1234567880)) } - // MARK: ---- caches the time since the last open - it("caches the time since the last open") { + // MARK: ---- caches the time since the last poll in memory + it("caches the time since the last poll in memory") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) @@ -271,8 +271,8 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567770)) dependencies.dateNow = Date(timeIntervalSince1970: 1234567780) - expect(cache.getTimeSinceLastOpen(using: dependencies)) - .to(beCloseTo(10)) + expect(cache.getLastSuccessfulCommunityPollTimestamp()) + .to(equal(1234567770)) mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -281,8 +281,21 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567890)) // Cached value shouldn't have been updated - expect(cache.getTimeSinceLastOpen(using: dependencies)) - .to(beCloseTo(10)) + expect(cache.getLastSuccessfulCommunityPollTimestamp()) + .to(equal(1234567770)) + } + + // MARK: ---- updates the time since the last poll in user defaults + it("updates the time since the last poll in user defaults") { + cache.setLastSuccessfulCommunityPollTimestamp(12345) + + expect(mockUserDefaults) + .to(call(matchingParameters: .all) { + $0.set( + Date(timeIntervalSince1970: 12345), + forKey: UserDefaults.DateKey.lastOpen.rawValue + ) + }) } } diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index d66a07ad8..ebbbaae29 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -69,6 +69,11 @@ class CommunityPollerSpec: QuickSpec { ) } ) + @TestState(singleton: .appContext, in: dependencies) var mockAppContext: MockAppContext! = MockAppContext( + initialSetup: { context in + context.when { $0.isMainAppAndActive }.thenReturn(false) + } + ) @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults() @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( initialSetup: { cache in @@ -78,7 +83,7 @@ class CommunityPollerSpec: QuickSpec { @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache( initialSetup: { cache in cache.when { $0.pendingChanges }.thenReturn([]) - cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0) + cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) } ) @TestState var cache: CommunityPoller.Cache! = CommunityPoller.Cache(using: dependencies) @@ -87,6 +92,10 @@ class CommunityPollerSpec: QuickSpec { describe("a CommunityPollerCache") { // MARK: -- when starting polling context("when starting polling") { + beforeEach { + mockAppContext.when { $0.isMainAppAndActive }.thenReturn(true) + } + // MARK: ---- creates pollers for all of the communities it("creates pollers for all of the communities") { cache.startAllPollers() diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index 103b644ad..a7db4da5d 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -16,8 +16,12 @@ class MockOGMCache: Mock, OGMCacheType { set { mockNoReturn(args: [newValue]) } } - func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { - return mock(args: [dependencies]) + func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval { + return mock() + } + + func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) { + mockNoReturn(args: [timestamp]) } func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo]) { diff --git a/SessionUtilitiesKit/Types/BackgroundTaskManager.swift b/SessionUtilitiesKit/Types/BackgroundTaskManager.swift index 43252b6fa..bf099a6b7 100644 --- a/SessionUtilitiesKit/Types/BackgroundTaskManager.swift +++ b/SessionUtilitiesKit/Types/BackgroundTaskManager.swift @@ -23,7 +23,11 @@ public enum SessionBackgroundTaskState { // MARK: - SessionBackgroundTaskManager public class SessionBackgroundTaskManager { + /// Maximum duration to extend background tasks + private static let maxBackgroundTime: TimeInterval = 180 + private let dependencies: Dependencies + private let queue = DispatchQueue(label: "com.session.backgroundTaskManager") /// This property should only be accessed while synchronized on this instance. private var backgroundTaskId: UIBackgroundTaskIdentifier = .invalid @@ -43,11 +47,11 @@ public class SessionBackgroundTaskManager { /// begins, we use a single uninterrupted background that spans their lifetimes. /// /// This property should only be accessed while synchronized on this instance. - private var continuityTimer: Timer? + private var continuityTimer: DispatchSourceTimer? /// In order to ensure we have sufficient time to clean up before background tasks expire (without having to kick off additional tasks) /// we track the remaining background execution time and end tasks 5 seconds early (same as the AppDelegate background fetch) - private var expirationTimeObserver: Timer? + private var expirationTimeObserver: DispatchSourceTimer? private var hasGottenValidBackgroundTimeRemaining: Bool = false fileprivate init(using dependencies: Dependencies) { @@ -61,13 +65,6 @@ public class SessionBackgroundTaskManager { // MARK: - Functions - @discardableResult private static func synced(_ lock: Any, closure: () -> T) -> T { - objc_sync_enter(lock) - let result: T = closure() - objc_sync_exit(lock) - return result - } - public func startObservingNotifications() { guard dependencies[singleton: .appContext].isMainApp else { return } @@ -86,14 +83,14 @@ public class SessionBackgroundTaskManager { } @objc private func applicationDidBecomeActive() { - SessionBackgroundTaskManager.synced(self) { [weak self] in + queue.sync { [weak self] in self?.isAppActive = true self?.ensureBackgroundTaskState() } } @objc private func applicationWillResignActive() { - SessionBackgroundTaskManager.synced(self) { [weak self] in + queue.sync { [weak self] in self?.isAppActive = false self?.ensureBackgroundTaskState() } @@ -109,7 +106,7 @@ public class SessionBackgroundTaskManager { // background task, but the background task couldn't be begun. // In that case expirationBlock will not be called. fileprivate func addTask(expiration: @escaping () -> ()) -> UInt64? { - return SessionBackgroundTaskManager.synced(self) { [weak self, dependencies] in + return queue.sync { [weak self] () -> UInt64? in let taskId: UInt64 = ((self?.idCounter ?? 0) + 1) self?.idCounter = taskId self?.expirationMap[taskId] = expiration @@ -118,18 +115,15 @@ public class SessionBackgroundTaskManager { self?.expirationMap.removeValue(forKey: taskId) } - self?.continuityTimer?.invalidate() - self?.continuityTimer = nil + if self?.continuityTimer != nil { + self?.continuityTimer?.cancel() + self?.continuityTimer = nil + } // Start observing the background time remaining - if self?.expirationTimeObserver?.isValid != true { + if self?.expirationTimeObserver?.isCancelled == true { self?.hasGottenValidBackgroundTimeRemaining = false - self?.expirationTimeObserver = Timer.scheduledTimerOnMainThread( - withTimeInterval: 1, - repeats: true, - using: dependencies, - block: { _ in self?.expirationTimerDidFire() } - ) + self?.checkExpirationTime(in: .seconds(1)) // Don't know the remaining time so check soon } return taskId @@ -139,7 +133,7 @@ public class SessionBackgroundTaskManager { fileprivate func removeTask(taskId: UInt64?) { guard let taskId: UInt64 = taskId else { return } - SessionBackgroundTaskManager.synced(self) { [weak self, dependencies] in + queue.sync { [weak self, queue] in self?.expirationMap.removeValue(forKey: taskId) // This timer will ensure that we keep the background task active (if necessary) @@ -148,87 +142,93 @@ public class SessionBackgroundTaskManager { // should be able to ensure background tasks by "narrowly" wrapping // their core logic with a SessionBackgroundTask and not worrying about "hand off" // between SessionBackgroundTasks. - self?.continuityTimer?.invalidate() - self?.continuityTimer = Timer.scheduledTimerOnMainThread( - withTimeInterval: 0.25, - using: dependencies, - block: { _ in self?.continuityTimerDidFire() } - ) + if self?.continuityTimer != nil { + self?.continuityTimer?.cancel() + self?.continuityTimer = nil + } + + self?.continuityTimer = DispatchSource.makeTimerSource(queue: queue) + self?.continuityTimer?.schedule(deadline: .now() + .milliseconds(250)) + self?.continuityTimer?.setEventHandler { self?.continuityTimerDidFire() } + self?.continuityTimer?.resume() self?.ensureBackgroundTaskState() } } - /// Begins or end a background task if necessary. + /// Begins or end a background task if necessary + /// + /// **Note:** Should only be called internally within `queue.sync` for thread safety @discardableResult private func ensureBackgroundTaskState() -> Bool { // We can't create background tasks in the SAE, but pretend that we succeeded. guard dependencies[singleton: .appContext].isMainApp else { return true } - return SessionBackgroundTaskManager.synced(self) { [weak self, dependencies] in - // We only want to have a background task if we are: - // a) "not active" AND - // b1) there is one or more active instance of SessionBackgroundTask OR... - // b2) ...there _was_ an active instance recently. - let shouldHaveBackgroundTask: Bool = ( - self?.isAppActive == false && ( - (self?.expirationMap.count ?? 0) > 0 || - self?.continuityTimer != nil - ) + // We only want to have a background task if we are: + // a) "not active" AND + // b1) there is one or more active instance of SessionBackgroundTask OR... + // b2) ...there _was_ an active instance recently. + let shouldHaveBackgroundTask: Bool = ( + self.isAppActive == false && ( + self.expirationMap.count > 0 || + self.continuityTimer != nil ) - let hasBackgroundTask: Bool = (self?.backgroundTaskId != .invalid) - - guard shouldHaveBackgroundTask != hasBackgroundTask else { - // Current state is correct - return true - } - guard !shouldHaveBackgroundTask else { - return (self?.startBackgroundTask() == true) - } - - // Need to end background task. - let maybeBackgroundTaskId: UIBackgroundTaskIdentifier? = self?.backgroundTaskId - self?.backgroundTaskId = .invalid - self?.expirationTimeObserver?.invalidate() - self?.expirationTimeObserver = nil - - if let backgroundTaskId: UIBackgroundTaskIdentifier = maybeBackgroundTaskId, backgroundTaskId != .invalid { - dependencies[singleton: .appContext].endBackgroundTask(backgroundTaskId) - } - + ) + let hasBackgroundTask: Bool = (self.backgroundTaskId != .invalid) + + guard shouldHaveBackgroundTask != hasBackgroundTask else { + // Current state is correct return true } + guard !shouldHaveBackgroundTask else { + return (self.startOverarchingBackgroundTask() == true) + } + + // Need to end background task. + let maybeBackgroundTaskId: UIBackgroundTaskIdentifier? = self.backgroundTaskId + self.backgroundTaskId = .invalid + + if self.expirationTimeObserver != nil { + self.expirationTimeObserver?.cancel() + self.expirationTimeObserver = nil + } + + if let backgroundTaskId: UIBackgroundTaskIdentifier = maybeBackgroundTaskId, backgroundTaskId != .invalid { + dependencies[singleton: .appContext].endBackgroundTask(backgroundTaskId) + } + + return true } - /// Returns `false` if the background task cannot be begun. - private func startBackgroundTask() -> Bool { + /// Returns `false` if the background task cannot be begun + /// + /// **Note:** Should only be called internally within `queue.sync` for thread safety + private func startOverarchingBackgroundTask() -> Bool { guard dependencies[singleton: .appContext].isMainApp else { return false } - return SessionBackgroundTaskManager.synced(self) { [weak self, dependencies] in - self?.backgroundTaskId = dependencies[singleton: .appContext].beginBackgroundTask { - /// Supposedly `[UIApplication beginBackgroundTaskWithExpirationHandler]`'s handler - /// will always be called on the main thread, but in practice we've observed otherwise. - /// - /// See: - /// https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) + self.backgroundTaskId = dependencies[singleton: .appContext].beginBackgroundTask { [weak self] in + /// Supposedly `[UIApplication beginBackgroundTaskWithExpirationHandler]`'s handler + /// will always be called on the main thread, but in practice we've observed otherwise. + /// + /// See: + /// https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) + self?.queue.sync { self?.backgroundTaskExpired() } - - // If the background task could not begin, return false to indicate that - return (self?.backgroundTaskId != .invalid) } + + // If the background task could not begin, return false to indicate that + return (self.backgroundTaskId != .invalid) } + /// **Note:** Should only be called internally within `queue.sync` for thread safety private func backgroundTaskExpired() { - var backgroundTaskId: UIBackgroundTaskIdentifier = .invalid - var expirationMap: [UInt64: () -> ()] = [:] + let backgroundTaskId: UIBackgroundTaskIdentifier = self.backgroundTaskId + let expirationMap: [UInt64: () -> ()] = self.expirationMap + self.backgroundTaskId = .invalid + self.expirationMap.removeAll() - SessionBackgroundTaskManager.synced(self) { [weak self] in - backgroundTaskId = (self?.backgroundTaskId ?? .invalid) - self?.backgroundTaskId = .invalid - self?.expirationTimeObserver?.invalidate() - self?.expirationTimeObserver = nil - - expirationMap = (self?.expirationMap ?? [:]) - self?.expirationMap.removeAll() + if self.expirationTimeObserver != nil { + self.expirationTimeObserver?.cancel() + self.expirationTimeObserver = nil } /// Supposedly `[UIApplication beginBackgroundTaskWithExpirationHandler]`'s handler @@ -247,33 +247,44 @@ public class SessionBackgroundTaskManager { } } + private func checkExpirationTime(in interval: DispatchTimeInterval) { + expirationTimeObserver = DispatchSource.makeTimerSource(queue: queue) + expirationTimeObserver?.schedule(deadline: .now() + interval) + expirationTimeObserver?.setEventHandler { [weak self] in self?.expirationTimerDidFire() } + expirationTimeObserver?.resume() + } + + /// Timer will always fire on the `queue` so no need to `queue.sync` private func continuityTimerDidFire() { - SessionBackgroundTaskManager.synced(self) { [weak self] in - self?.continuityTimer?.invalidate() - self?.continuityTimer = nil - self?.ensureBackgroundTaskState() - } + continuityTimer = nil + ensureBackgroundTaskState() } + /// Timer will always fire on the `queue` so no need to `queue.sync` private func expirationTimerDidFire() { + expirationTimeObserver = nil + guard dependencies[singleton: .appContext].isMainApp else { return } let backgroundTimeRemaining: TimeInterval = dependencies[singleton: .appContext].backgroundTimeRemaining - SessionBackgroundTaskManager.synced(self) { [weak self] in - // It takes the OS a little while to update the 'backgroundTimeRemaining' value so if it hasn't been updated - // yet then don't do anything - guard self?.hasGottenValidBackgroundTimeRemaining == true || backgroundTimeRemaining != .greatestFiniteMagnitude else { - return - } - - self?.hasGottenValidBackgroundTimeRemaining = true - - // If there is more than 5 seconds remaining then no need to do anything yet (plenty of time to continue running) - guard backgroundTimeRemaining <= 5 else { return } - - // There isn't a lot of time remaining so trigger the expiration - self?.backgroundTaskExpired() + /// It takes the OS a little while to update the 'backgroundTimeRemaining' value so if it hasn't been updated yet then don't do anything + guard self.hasGottenValidBackgroundTimeRemaining == true || backgroundTimeRemaining != .greatestFiniteMagnitude else { + self.checkExpirationTime(in: .seconds(1)) + return + } + + self.hasGottenValidBackgroundTimeRemaining = true + + switch backgroundTimeRemaining { + /// There is more than 10 seconds remaining so no need to do anything yet (plenty of time to continue running) + case 10...: self.checkExpirationTime(in: .seconds(5)) + + /// There is between 5 and 10 seconds so poll more frequently just in case + case 5..<10: self.checkExpirationTime(in: .milliseconds(2500)) + + /// There isn't a lot of time remaining so trigger the expiration + default: self.backgroundTaskExpired() } } } @@ -282,8 +293,6 @@ public class SessionBackgroundTaskManager { public class SessionBackgroundTask { private let dependencies: Dependencies - - /// This property should only be accessed while synchronized on this instance private var taskId: UInt64? private let label: String private var completion: ((SessionBackgroundTaskState) -> ())? @@ -308,86 +317,31 @@ public class SessionBackgroundTask { // MARK: - Functions - @discardableResult private static func synced(_ lock: Any, closure: () -> T) -> T { - objc_sync_enter(lock) - let result: T = closure() - objc_sync_exit(lock) - return result - } - private func startBackgroundTask() { - // Make a local copy of completion to ensure that it is called exactly once - var completion: ((SessionBackgroundTaskState) -> ())? - - self.taskId = dependencies[singleton: .backgroundTaskManager].addTask { [weak self] in - Threading.dispatchMainThreadSafe { - guard let strongSelf = self else { return } - - SessionBackgroundTask.synced(strongSelf) { - self?.taskId = nil - completion = self?.completion - self?.completion = nil - } - - completion?(.expired) - } - } - - // If we didn't get a taskId then the background task could not be started so - // we should call the completion block with a 'couldNotStart' error - guard taskId == nil else { return } - - SessionBackgroundTask.synced(self) { [weak self] in - completion = self?.completion - self?.completion = nil + taskId = dependencies[singleton: .backgroundTaskManager].addTask { [weak self] in + self?.taskExpired() } - if completion != nil { - Threading.dispatchMainThreadSafe { - completion?(.couldNotStart) - } + if taskId == nil { + completion?(.couldNotStart) + completion = nil } } public func cancel() { guard taskId != nil else { return } - // Make a local copy of completion to ensure that it is called exactly once - var completion: ((SessionBackgroundTaskState) -> ())? - - SessionBackgroundTask.synced(self) { [weak self, dependencies] in - dependencies[singleton: .backgroundTaskManager].removeTask(taskId: self?.taskId) - completion = self?.completion - self?.taskId = nil - self?.completion = nil - } - - // endBackgroundTask must be called on the main thread. - if completion != nil { - Threading.dispatchMainThreadSafe { - completion?(.cancelled) - } - } + dependencies[singleton: .backgroundTaskManager].removeTask(taskId: taskId) + completion?(.cancelled) + completion = nil } private func endBackgroundTask() { - guard taskId != nil else { return } - - // Make a local copy of completion since this method is called by `dealloc` - var completion: ((SessionBackgroundTaskState) -> ())? - - SessionBackgroundTask.synced(self) { [weak self, dependencies] in - dependencies[singleton: .backgroundTaskManager].removeTask(taskId: self?.taskId) - completion = self?.completion - self?.taskId = nil - self?.completion = nil - } - - // endBackgroundTask must be called on the main thread. - if completion != nil { - Threading.dispatchMainThreadSafe { - completion?(.cancelled) - } - } + cancel() + } + + private func taskExpired() { + completion?(.expired) + completion = nil } } From a080d676187ab3b4a426349dd0662383463134f8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Mar 2025 13:14:51 +1100 Subject: [PATCH 11/24] Fixed a couple of bugs and logging tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Refactored the remaining `SNLog` calls • Added support for a 'customSuffix' to the log categories (similar to the 'customPrefix' it allows category name manipulation whilst keeping the existing log level settings) • Improved logging in the PN extension • Fixed an issue where the PN extension would end up with duplicate logs every time a new PN was received • Fixed an issue where the PN extension would needlessly dispatch it's setup to the main thread • Fixed an issue where the PN extension would try to read from the database after suspending it • Fixed an issue where the PN extension could try to complete on a non-main thread (eg. db threads) which _might_ cause odd behaviours --- .../Calls/Call Management/SessionCall.swift | 8 +- .../Call Management/SessionCallManager.swift | 2 +- .../ConversationVC+Interaction.swift | 12 +- Session/Conversations/ConversationVC.swift | 2 +- .../Conversations/ConversationViewModel.swift | 10 +- .../GlobalSearchViewController.swift | 10 +- Session/Home/HomeViewModel.swift | 10 +- .../ImagePickerController.swift | 4 +- .../MediaDetailViewController.swift | 6 +- .../MediaGalleryViewModel.swift | 2 +- .../Notifications/NotificationPresenter.swift | 2 +- Session/Settings/HelpViewModel.swift | 10 +- .../Shared/QRCodeScanningViewController.swift | 16 +- .../Shared/SessionTableViewController.swift | 16 +- .../Shared/Types/ObservableTableSource.swift | 14 +- .../Database/Models/Attachment.swift | 2 +- .../Database/Models/Profile.swift | 2 +- .../Jobs/CheckForAppUpdatesJob.swift | 6 +- .../LibSession+ConvoInfoVolatile.swift | 6 +- .../LibSession+UserGroups.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 14 +- .../Control Messages/CallMessage.swift | 2 +- .../ClosedGroupControlMessage.swift | 8 +- .../DataExtractionNotification.swift | 4 +- .../ExpirationTimerUpdate.swift | 2 +- ...roupUpdateDeleteMemberContentMessage.swift | 2 +- .../GroupUpdateInfoChangeMessage.swift | 2 +- .../GroupUpdateInviteMessage.swift | 2 +- .../GroupUpdateInviteResponseMessage.swift | 2 +- .../GroupUpdateMemberChangeMessage.swift | 2 +- .../GroupUpdateMemberLeftMessage.swift | 2 +- ...pUpdateMemberLeftNotificationMessage.swift | 2 +- .../GroupUpdatePromoteMessage.swift | 2 +- .../MessageRequestResponse.swift | 2 +- .../Control Messages/ReadReceipt.swift | 4 +- .../Control Messages/TypingIndicator.swift | 4 +- .../Control Messages/UnsendRequest.swift | 4 +- .../VisibleMessage+LinkPreview.swift | 4 +- .../VisibleMessage+OpenGroupInvitation.swift | 4 +- .../VisibleMessage+Profile.swift | 6 +- .../VisibleMessage+Quote.swift | 8 +- .../VisibleMessage+Reaction.swift | 2 +- .../Visible Messages/VisibleMessage.swift | 2 +- .../MessageReceiver+Calls.swift | 2 +- .../MessageReceiver+LegacyClosedGroups.swift | 28 ++-- .../MessageReceiver+TypingIndicators.swift | 2 +- .../MessageReceiver+VisibleMessages.swift | 2 +- .../MessageSender+LegacyClosedGroups.swift | 12 +- .../Notifications/PushNotificationAPI.swift | 4 +- .../Utilities/Data+Utilities.swift | 8 +- .../Utilities/MessageWrapper.swift | 6 +- .../Utilities/Preferences+Sound.swift | 10 +- .../SNProtoEnvelope+Conversion.swift | 2 +- .../_TestUtilities/MockLibSessionCache.swift | 5 +- .../NotificationServiceExtension.swift | 137 +++++++++++------- .../Models/DeleteAllBeforeResponse.swift | 4 +- .../Models/DeleteAllMessagesResponse.swift | 4 +- .../Models/RevokeSubaccountResponse.swift | 4 +- .../Models/SendMessageResponse.swift | 4 +- .../Models/SnodeReceivedMessage.swift | 2 +- .../Models/UnrevokeSubaccountResponse.swift | 4 +- .../Models/UpdateExpiryAllResponse.swift | 4 +- .../Models/UpdateExpiryResponse.swift | 4 +- SessionSnodeKit/Types/PreparedRequest.swift | 2 +- .../Types/ValidatableResponse.swift | 10 ++ .../Database/Models/Identity.swift | 2 +- .../Types/PagedDatabaseObserver.swift | 2 +- SessionUtilitiesKit/General/Logging.swift | 51 +++++-- SessionUtilitiesKit/Media/MediaUtils.swift | 28 ++-- .../AttachmentPrepViewController.swift | 2 +- SignalUtilitiesKit/Utilities/AppSetup.swift | 7 +- 71 files changed, 368 insertions(+), 212 deletions(-) diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 1db250309..37874596e 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -275,10 +275,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { receiveCompletion: { [weak self] result in switch result { case .finished: - SNLog("[Calls] Offer message sent") + Log.info(.calls, "Offer message sent") self?.updateCallDetailedStatus?("Sending Connection Candidates") case .failure(let error): - SNLog("[Calls] Error initializing call after 5 retries: \(error), ending call...") + Log.error(.calls, "Error initializing call after 5 retries: \(error), ending call...") self?.handleCallInitializationFailed() } } @@ -291,14 +291,14 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { hasStartedConnecting = true if let sdp = remoteSDP { - SNLog("[Calls] Got remote sdp already") + Log.info(.calls, "Got remote sdp already") self.updateCallDetailedStatus?("Answering Call") webRTCSession.handleRemoteSDP(sdp, from: sessionId) // This sends an answer message internally } } func answerSessionCallInBackground() { - SNLog("[Calls] Answering call in background") + Log.info(.calls, "Answering call in background") self.answerSessionCall() } diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index badb53a7f..be7f4c96c 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -144,7 +144,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } func handleCallEnded() { - SNLog("[Calls] Call ended.") + Log.info(.calls, "Call ended.") WebRTCSession.current = nil dependencies[defaults: .appGroup, key: .isCallOngoing] = false dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index df1e4c851..fd8c7968b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -344,7 +344,7 @@ extension ConversationVC: Permissions.requestMicrophonePermissionIfNeeded(using: viewModel.dependencies) if Permissions.microphone != .granted { - SNLog("Proceeding without microphone access. Any recorded video will be silent.") + Log.warn(.conversation, "Proceeding without microphone access. Any recorded video will be silent.") } let sendMediaNavController = SendMediaNavigationController.showingCameraFirst( @@ -1107,7 +1107,7 @@ extension ConversationVC: guard let originalFilePath: String = mediaView.attachment.originalFilePath(using: viewModel.dependencies), viewModel.dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) - else { return SNLog("Missing video file") } + else { return Log.warn(.conversation, "Missing video file") } /// When playing media we need to change the AVAudioSession to 'playback' mode so the device "silent mode" /// doesn't prevent video audio from playing @@ -2337,7 +2337,7 @@ extension ConversationVC: self.audioRecorder = audioRecorder } catch { - SNLog("Couldn't start audio recording due to error: \(error).") + Log.error(.conversation, "Couldn't start audio recording due to error: \(error).") return cancelVoiceMessageRecording() } @@ -2353,7 +2353,7 @@ extension ConversationVC: guard successfullyPrepared && startedRecording else { - SNLog(successfullyPrepared ? "Couldn't record audio." : "Couldn't prepare audio recorder.") + Log.error(.conversation, (successfullyPrepared ? "Couldn't record audio." : "Couldn't prepare audio recorder.")) // Dispatch to the next run loop to avoid DispatchQueue.main.async { @@ -2412,7 +2412,9 @@ extension ConversationVC: let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, sourceFilename: nil, shouldDeleteOnDeinit: true, using: viewModel.dependencies) self.audioRecorder = nil - guard let dataSource = dataSourceOrNil else { return SNLog("Couldn't load recorded data.") } + guard let dataSource = dataSourceOrNil else { + return Log.error(.conversation, "Couldn't load recorded data.") + } // Create attachment let fileName = ("messageVoice".localized() as NSString) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 829b67f6b..523226598 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -2139,7 +2139,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return nil default: - SNLog("[ConversationVC] Warning: Processing unhandled cell type when marking as read, this could result in intermittent failures") + Log.warn(.conversation, "Processing unhandled cell type when marking as read, this could result in intermittent failures") return nil } }) diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 097eb588a..11f8c6e64 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -11,6 +11,14 @@ import SessionMessagingKit import SessionUtilitiesKit import SessionUIKit +// MARK: - Log.Category + +public extension Log.Category { + static let conversation: Log.Category = .create("Conversation", defaultLevel: .info) +} + +// MARK: - ConversationViewModel + public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHolder { public typealias SectionModel = ArraySection @@ -361,7 +369,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold } } .removeDuplicates() - .handleEvents(didFail: { SNLog("[ConversationViewModel] Observation failed with error: \($0)") }) + .handleEvents(didFail: { Log.error(.conversation, "Observation failed with error: \($0)") }) } public func updateThreadData(_ updatedData: SessionThreadViewModel) { diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7b8fe65fa..83457f25f 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -8,6 +8,14 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("GlobalSearch", defaultLevel: .warn) +} + +// MARK: - GlobalSearchViewController + class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UITableViewDelegate, UITableViewDataSource { fileprivate typealias SectionModel = ArraySection @@ -287,7 +295,7 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI catch { // Don't log the 'interrupt' error as that's just the user typing too fast if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { - SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + Log.error(.cat, "Failed to find results due to error: \(error)") } return .failure(error) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index d900281b0..58fa8b065 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -7,6 +7,14 @@ import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("HomeViewModel", defaultLevel: .warn) +} + +// MARK: - HomeViewModel + public class HomeViewModel: NavigatableStateHolder { public let navigatableState: NavigatableState = NavigatableState() @@ -227,7 +235,7 @@ public class HomeViewModel: NavigatableStateHolder { try HomeViewModel.retrieveState(db, using: dependencies) } .removeDuplicates() - .handleEvents(didFail: { SNLog("[HomeViewModel] Observation failed with error: \($0)") }) + .handleEvents(didFail: { Log.error(.cat, "Observation failed with error: \($0)") }) private static func retrieveState( _ db: Database, diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 2d09d864d..c8eebdec1 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -483,7 +483,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else { - SNLog("Failed to select cell for asset at \(indexPath.item)") + Log.error(.media, "Failed to select cell for asset at \(indexPath.item)") delegate.imagePicker(self, failedToRetrieveAssetAt: indexPath.item, forCount: photoCollectionContents.assetCount) return } @@ -529,7 +529,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath) guard let assetItem: PhotoPickerAssetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) else { - SNLog("Failed to style cell for asset at \(indexPath.item)") + Log.error(.media, "Failed to style cell for asset at \(indexPath.item)") return cell } diff --git a/Session/Media Viewing & Editing/MediaDetailViewController.swift b/Session/Media Viewing & Editing/MediaDetailViewController.swift index 2c644c7c6..601a54420 100644 --- a/Session/Media Viewing & Editing/MediaDetailViewController.swift +++ b/Session/Media Viewing & Editing/MediaDetailViewController.swift @@ -92,7 +92,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { updateUICallback() }, failure: { - SNLog("Could not load media.") + Log.error(.media, "Could not load media.") } ) } @@ -175,7 +175,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { let viewSize: CGSize = self.scrollView.bounds.size guard imageSize.width > 0 && imageSize.height > 0 else { - SNLog("Invalid image dimensions (\(imageSize.width), \(imageSize.height))") + Log.error(.media, "Invalid image dimensions (\(imageSize.width), \(imageSize.height))") return } @@ -350,7 +350,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { guard let originalFilePath: String = self.galleryItem.attachment.originalFilePath(using: dependencies), dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) - else { return SNLog("Missing video file") } + else { return Log.error(.media, "Missing video file") } let videoUrl: URL = URL(fileURLWithPath: originalFilePath) let player: AVPlayer = AVPlayer(url: videoUrl) diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 177351d90..726662bd3 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -395,7 +395,7 @@ public class MediaGalleryViewModel { .fetchAll(db) } .removeDuplicates() - .handleEvents(didFail: { SNLog("[MediaGalleryViewModel] Observation failed with error: \($0)") }) + .handleEvents(didFail: { Log.error(.media, "Gallery observation failed with error: \($0)") }) } @discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] { diff --git a/Session/Notifications/NotificationPresenter.swift b/Session/Notifications/NotificationPresenter.swift index b5c99409d..8ff8995b7 100644 --- a/Session/Notifications/NotificationPresenter.swift +++ b/Session/Notifications/NotificationPresenter.swift @@ -137,7 +137,7 @@ public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate, } guard notificationBody != nil || notificationTitle != nil else { - SNLog("AppNotifications error: No notification content") + Log.info("AppNotifications error: No notification content") return } diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index a2d6b9403..19744dddf 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -8,6 +8,14 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let version: Log.Category = .create("Version", defaultLevel: .info) +} + +// MARK: - HelpViewModel + class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { typealias TableItem = Section @@ -204,7 +212,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa using dependencies: Dependencies, onShareComplete: (() -> ())? = nil ) { - Log.info("[Version] \(dependencies[cache: .appVersion].versionInfo)") + Log.info(.version, "\(dependencies[cache: .appVersion].versionInfo)") Log.flush() guard diff --git a/Session/Shared/QRCodeScanningViewController.swift b/Session/Shared/QRCodeScanningViewController.swift index 5729c1fba..47c88dad6 100644 --- a/Session/Shared/QRCodeScanningViewController.swift +++ b/Session/Shared/QRCodeScanningViewController.swift @@ -5,10 +5,20 @@ import AVFoundation import SessionUIKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("QRCode", defaultLevel: .warn) +} + +// MARK: - QRScannerDelegate + protocol QRScannerDelegate: AnyObject { func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onSuccess: (() -> ())?, onError: (() -> ())?) } +// MARK: - QRCodeScanningViewController + class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { public weak var scanDelegate: QRScannerDelegate? @@ -123,7 +133,7 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj let device: AVCaptureDevice = maybeDevice, let input: AVCaptureInput = try? AVCaptureDeviceInput(device: device) else { - return SNLog("Failed to retrieve the device for enabling the QRCode scanning camera") + return Log.error(.cat, "Failed to retrieve the device for enabling the QRCode scanning camera") } // Image output @@ -141,11 +151,11 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj if capture.canAddOutput(metadataOutput) { capture.addOutput(metadataOutput) } guard !capture.inputs.isEmpty && capture.outputs.count == 2 else { - return SNLog("Failed to attach the input/output to the capture session") + return Log.error(.cat, "Failed to attach the input/output to the capture session") } guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else { - return SNLog("The output is unable to process QR codes") + return Log.error(.cat, "The output is unable to process QR codes") } // Specify that we want to capture QR Codes (Needs to be done after being added diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index 50bf80b1e..4a93f88e6 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -9,10 +9,20 @@ import SessionUtilitiesKit import SessionMessagingKit import SignalUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("SessionTableViewController", defaultLevel: .info) +} + +// MARK: - SessionViewModelAccessible + protocol SessionViewModelAccessible { var viewModelType: AnyObject.Type { get } } +// MARK: - SessionTableViewController + class SessionTableViewController: BaseVC, UITableViewDataSource, UITableViewDelegate, SessionViewModelAccessible where ViewModel: (SessionTableViewModel & ObservableTableSource) { typealias Section = ViewModel.Section typealias TableItem = ViewModel.TableItem @@ -235,11 +245,11 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa // If we got an error then try to restart the stream once, otherwise log the error guard self?.dataStreamJustFailed == false else { - SNLog("Unable to recover database stream in '\(title)' settings with error: \(error)") + Log.error(.cat, "Unable to recover database stream in '\(title)' settings with error: \(error)") return } - SNLog("Atempting recovery for database stream in '\(title)' settings with error: \(error)") + Log.info(.cat, "Atempting recovery for database stream in '\(title)' settings with error: \(error)") self?.dataStreamJustFailed = true self?.startObservingChanges(didReturnFromBackground: didReturnFromBackground) @@ -467,7 +477,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa cell.update(with: threadInfo.id, using: viewModel.dependencies) default: - SNLog("[SessionTableViewController] Got invalid combination of cellType: \(viewModel.cellType) and tableData: \(SessionCell.Info.self)") + Log.error(.cat, "[SessionTableViewController] Got invalid combination of cellType: \(viewModel.cellType) and tableData: \(SessionCell.Info.self)") } return cell diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index 37feb246b..d6e18b9c1 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -6,6 +6,14 @@ import Combine import DifferenceKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static func cat(_ viewModel: Any) -> Log.Category { + return .create("ObservableTableSource", customSuffix: "-\(type(of: viewModel))", defaultLevel: .warn) + } +} + // MARK: - ObservableTableSource public protocol ObservableTableSource: AnyObject, SectionedTableData { @@ -203,11 +211,10 @@ public enum ObservationBuilder { scheduling: dependencies[singleton: .scheduler], onError: { error in let log: String = [ - "[\(type(of: viewModel))]", // stringlint:ignore "Observation failed with error:", // stringlint:ignore "\(error)" // stringlint:ignore ].joined(separator: " ") - SNLog(log) + Log.error(.cat(viewModel), log) subject.send(completion: Subscribers.Completion.failure(error)) }, onChange: { subject.send($0) } @@ -259,11 +266,10 @@ public enum ObservationBuilder { scheduling: dependencies[singleton: .scheduler], onError: { error in let log: String = [ - "[\(type(of: viewModel))]", // stringlint:ignore "Observation failed with error:", // stringlint:ignore "\(error)" // stringlint:ignore ].joined(separator: " ") - SNLog(log) + Log.error(.cat(viewModel), log) subject.send(completion: Subscribers.Completion.failure(error)) }, onChange: { subject.send($0) } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 65d60dbb7..c1135cc68 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -476,7 +476,7 @@ extension Attachment { return try builder.build() } catch { - SNLog("Couldn't construct attachment proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct attachment proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index d44e56b45..8a66d774c 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -187,7 +187,7 @@ public extension Profile { return try dataMessageProto.build() } catch { - SNLog("Couldn't construct profile proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index 7241471fa..96be65f0f 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -72,9 +72,11 @@ public enum CheckForAppUpdatesJob: JobExecutor { }, receiveValue: { _, versionInfo in switch versionInfo.prerelease { - case .none: Log.info(.cat, "Latest version: \(versionInfo.version)") + case .none: + Log.info(.cat, "Latest version: \(versionInfo.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") + case .some(let prerelease): - Log.info(.cat, "Latest version: \(versionInfo.version), pre-release version: \(prerelease.version)") + Log.info(.cat, "Latest version: \(versionInfo.version), pre-release version: \(prerelease.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") } } ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift index ca6b61843..22f299e26 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift @@ -141,7 +141,7 @@ internal extension LibSession { try validChanges.forEach { threadInfo in guard var cThreadId: [CChar] = threadInfo.threadId.cString(using: .utf8) else { - SNLog("Unable to upsert contact volatile info to LibSession: \(LibSessionError.invalidCConversion)") + Log.error(.libSession, "Unable to upsert contact volatile info to LibSession: \(LibSessionError.invalidCConversion)") throw LibSessionError.invalidCConversion } @@ -200,7 +200,7 @@ internal extension LibSession { var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cString(using: .utf8), var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo.map({ Array(Data(hex: $0.publicKey)) }) else { - SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info") + Log.error(.libSession, "Unable to create community conversation when updating last read timestamp due to missing URL info") return } @@ -747,7 +747,7 @@ public extension LibSession { ) } else { - SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update") + Log.error(.libSession, "Ignoring unknown conversation type when iterating through volatile conversation info update") } convo_info_volatile_iterator_advance(convoIterator) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 3ded5ed11..e959f717d 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -722,7 +722,7 @@ internal extension LibSession { var cBaseUrl: [CChar] = community.urlInfo.server.cString(using: .utf8), var cRoom: [CChar] = community.urlInfo.roomToken.cString(using: .utf8) else { - SNLog("Unable to upsert community conversation to LibSession: \(LibSessionError.invalidCConversion)") + Log.error(.libSession, "Unable to upsert community conversation to LibSession: \(LibSessionError.invalidCConversion)") throw LibSessionError.invalidCConversion } diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 4e6855a2e..39b775c2a 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -205,13 +205,13 @@ public extension LibSession { // MARK: - State Management - public func loadState(_ db: Database) { + public func loadState(_ db: Database, requestId: String?) { // Ensure we have the ed25519 key and that we haven't already loaded the state before // we continue guard configStore.isEmpty, let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) - else { return Log.warn(.libSession, "Ignoring loadState due to existing state") } + else { return Log.warn(.libSession, "Ignoring loadState\(requestId.map { " for \($0)" } ?? "") due to existing state") } // Retrieve the existing dumps from the database let existingDumps: [ConfigDump] = ((try? ConfigDump.fetchSet(db)) ?? []) @@ -258,7 +258,7 @@ public extension LibSession { userSessionId: userSessionId, userEd25519KeyPair: ed25519KeyPair ) - Log.info(.libSession, "Completed loadState") + Log.info(.libSession, "Completed loadState\(requestId.map { " for \($0)" } ?? "")") } public func loadDefaultStatesFor( @@ -894,7 +894,7 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT // MARK: - State Management - func loadState(_ db: Database) + func loadState(_ db: Database, requestId: String?) func loadDefaultStatesFor( userConfigVariants: Set, groups: [ClosedGroup], @@ -974,6 +974,10 @@ public extension LibSessionCacheType { func withCustomBehaviour(_ behaviour: LibSession.CacheBehaviour, for sessionId: SessionId, change: @escaping () throws -> ()) throws { try withCustomBehaviour(behaviour, for: sessionId, variant: nil, change: change) } + + func loadState(_ db: Database) { + loadState(db, requestId: nil) + } } private final class NoopLibSessionCache: LibSessionCacheType { @@ -987,7 +991,7 @@ private final class NoopLibSessionCache: LibSessionCacheType { // MARK: - State Management - func loadState(_ db: Database) {} + func loadState(_ db: Database, requestId: String?) {} func loadDefaultStatesFor( userConfigVariants: Set, groups: [ClosedGroup], diff --git a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift index 4fa764143..31353dcbf 100644 --- a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift @@ -201,7 +201,7 @@ public final class CallMessage: ControlMessage { return try contentProto.build() } catch { - SNLog("Couldn't construct call message proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct call message proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift index 9d0c0f6bf..ce824f9f4 100644 --- a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift @@ -176,7 +176,7 @@ public final class ClosedGroupControlMessage: ControlMessage { do { return try result.build() } catch { - SNLog("Couldn't construct key pair wrapper proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct key pair wrapper proto from: \(self).") return nil } } @@ -298,7 +298,7 @@ public final class ClosedGroupControlMessage: ControlMessage { public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { guard let kind = kind else { - SNLog("Couldn't construct closed group update proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).") return nil } do { @@ -312,7 +312,7 @@ public final class ClosedGroupControlMessage: ControlMessage { do { closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build()) } catch { - SNLog("Couldn't construct closed group update proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).") return nil } closedGroupControlMessage.setMembers(members) @@ -346,7 +346,7 @@ public final class ClosedGroupControlMessage: ControlMessage { contentProto.setDataMessage(try dataMessageProto.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct closed group update proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift b/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift index 593ec44db..74f77c00c 100644 --- a/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift +++ b/SessionMessagingKit/Messages/Control Messages/DataExtractionNotification.swift @@ -85,7 +85,7 @@ public final class DataExtractionNotification: ControlMessage { public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { guard let kind = kind else { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } do { @@ -104,7 +104,7 @@ public final class DataExtractionNotification: ControlMessage { setDisappearingMessagesConfigurationIfNeeded(on: contentProto) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift index d97e8ac45..bc33f2f11 100644 --- a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift +++ b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift @@ -64,7 +64,7 @@ public final class ExpirationTimerUpdate: ControlMessage { contentProto.setDataMessage(try dataMessageProto.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct expiration timer update proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct expiration timer update proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateDeleteMemberContentMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateDeleteMemberContentMessage.swift index 94c834a08..883fd6891 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateDeleteMemberContentMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateDeleteMemberContentMessage.swift @@ -141,7 +141,7 @@ public final class GroupUpdateDeleteMemberContentMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift index 314074848..8d777e768 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift @@ -167,7 +167,7 @@ public final class GroupUpdateInfoChangeMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteMessage.swift index 7468a468b..54b7d6d19 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteMessage.swift @@ -170,7 +170,7 @@ public final class GroupUpdateInviteMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteResponseMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteResponseMessage.swift index 6bcaaa0c1..bab78e074 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteResponseMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInviteResponseMessage.swift @@ -87,7 +87,7 @@ public final class GroupUpdateInviteResponseMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberChangeMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberChangeMessage.swift index f1c3d3889..0c2a49ccc 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberChangeMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberChangeMessage.swift @@ -162,7 +162,7 @@ public final class GroupUpdateMemberChangeMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftMessage.swift index 2988173f4..a2a33c9d7 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftMessage.swift @@ -43,7 +43,7 @@ public final class GroupUpdateMemberLeftMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftNotificationMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftNotificationMessage.swift index 67707f13b..a4aff6197 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftNotificationMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateMemberLeftNotificationMessage.swift @@ -43,7 +43,7 @@ public final class GroupUpdateMemberLeftNotificationMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdatePromoteMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdatePromoteMessage.swift index 2bd129455..12c076d2c 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdatePromoteMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdatePromoteMessage.swift @@ -95,7 +95,7 @@ public final class GroupUpdatePromoteMessage: ControlMessage { contentProto.setDataMessage(try dataMessage.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct data extraction notification proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift index a7b502e7e..407c303e2 100644 --- a/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift +++ b/SessionMessagingKit/Messages/Control Messages/MessageRequestResponse.swift @@ -77,7 +77,7 @@ public final class MessageRequestResponse: ControlMessage { contentProto.setMessageRequestResponse(try messageRequestResponseProto.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct unsend request proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift b/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift index 9ca32f87b..5a154285f 100644 --- a/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift +++ b/SessionMessagingKit/Messages/Control Messages/ReadReceipt.swift @@ -56,7 +56,7 @@ public final class ReadReceipt: ControlMessage { public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { guard let timestamps = timestamps else { - SNLog("Couldn't construct read receipt proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct read receipt proto from: \(self).") return nil } let receiptProto = SNProtoReceiptMessage.builder(type: .read) @@ -69,7 +69,7 @@ public final class ReadReceipt: ControlMessage { setDisappearingMessagesConfigurationIfNeeded(on: contentProto) return try contentProto.build() } catch { - SNLog("Couldn't construct read receipt proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct read receipt proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift b/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift index 281b87b3b..f6ca3d3c7 100644 --- a/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift +++ b/SessionMessagingKit/Messages/Control Messages/TypingIndicator.swift @@ -84,7 +84,7 @@ public final class TypingIndicator: ControlMessage { public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { guard let timestampMs = sentTimestampMs, let kind = kind else { - SNLog("Couldn't construct typing indicator proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct typing indicator proto from: \(self).") return nil } let typingIndicatorProto = SNProtoTypingMessage.builder(timestamp: timestampMs, action: kind.toProto()) @@ -94,7 +94,7 @@ public final class TypingIndicator: ControlMessage { contentProto.setTypingMessage(try typingIndicatorProto.build()) return try contentProto.build() } catch { - SNLog("Couldn't construct typing indicator proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct typing indicator proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift b/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift index 5af46ece0..711abb7d5 100644 --- a/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift +++ b/SessionMessagingKit/Messages/Control Messages/UnsendRequest.swift @@ -63,7 +63,7 @@ public final class UnsendRequest: ControlMessage { public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { guard let timestamp = timestamp, let author = author else { - SNLog("Couldn't construct unsend request proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).") return nil } let unsendRequestProto = SNProtoUnsendRequest.builder(timestamp: timestamp, author: author) @@ -75,7 +75,7 @@ public final class UnsendRequest: ControlMessage { setDisappearingMessagesConfigurationIfNeeded(on: contentProto) return try contentProto.build() } catch { - SNLog("Couldn't construct unsend request proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift index 6dc958c40..bc7adb3f0 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift @@ -36,7 +36,7 @@ public extension VisibleMessage { public func toProto(_ db: Database) -> SNProtoDataMessagePreview? { guard let url = url else { - SNLog("Couldn't construct link preview proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct link preview proto from: \(self).") return nil } let linkPreviewProto = SNProtoDataMessagePreview.builder(url: url) @@ -53,7 +53,7 @@ public extension VisibleMessage { do { return try linkPreviewProto.build() } catch { - SNLog("Couldn't construct link preview proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct link preview proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift index 87e92a555..f0e3edbae 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+OpenGroupInvitation.swift @@ -27,14 +27,14 @@ public extension VisibleMessage { public func toProto() -> SNProtoDataMessageOpenGroupInvitation? { guard let url = url, let name = name else { - SNLog("Couldn't construct open group invitation proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct open group invitation proto from: \(self).") return nil } let openGroupInvitationProto = SNProtoDataMessageOpenGroupInvitation.builder(url: url, name: name) do { return try openGroupInvitationProto.build() } catch { - SNLog("Couldn't construct open group invitation proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct open group invitation proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index e97bc0528..6cc6ca9e8 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -69,7 +69,7 @@ public extension VisibleMessage { let dataMessageProtoBuilder = try? toProtoBuilder(), let result = try? dataMessageProtoBuilder.build() else { - SNLog("Couldn't construct profile proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).") return nil } @@ -93,7 +93,7 @@ public extension VisibleMessage { public func toProto(isApproved: Bool) -> SNProtoMessageRequestResponse? { guard let displayName = displayName else { - SNLog("Couldn't construct profile proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).") return nil } let messageRequestResponseProto = SNProtoMessageRequestResponse.builder( @@ -110,7 +110,7 @@ public extension VisibleMessage { messageRequestResponseProto.setProfile(try profileProto.build()) return try messageRequestResponseProto.build() } catch { - SNLog("Couldn't construct profile proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift index 865717d40..6e898cb7c 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift @@ -40,7 +40,7 @@ public extension VisibleMessage { public func toProto(_ db: Database) -> SNProtoDataMessageQuote? { guard let timestamp = timestamp, let publicKey = publicKey else { - SNLog("Couldn't construct quote proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).") return nil } let quoteProto = SNProtoDataMessageQuote.builder(id: timestamp, author: publicKey) @@ -49,7 +49,7 @@ public extension VisibleMessage { do { return try quoteProto.build() } catch { - SNLog("Couldn't construct quote proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).") return nil } } @@ -70,13 +70,13 @@ public extension VisibleMessage { quotedAttachmentProto.setContentType(attachment.contentType) if let fileName = attachment.sourceFilename { quotedAttachmentProto.setFileName(fileName) } guard let attachmentProto = attachment.buildProto() else { - return SNLog("Ignoring invalid attachment for quoted message.") + return Log.warn(.messageSender, "Ignoring invalid attachment for quoted message.") } quotedAttachmentProto.setThumbnail(attachmentProto) do { try quoteProto.addAttachments(quotedAttachmentProto.build()) } catch { - SNLog("Couldn't construct quoted attachment proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct quoted attachment proto from: \(self).") } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift index 60c42833a..edf541920 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift @@ -86,7 +86,7 @@ public extension VisibleMessage { do { return try reactionProto.build() } catch { - SNLog("Couldn't construct quote proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 9c62dab42..3c75d60e8 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -202,7 +202,7 @@ public final class VisibleMessage: Message { proto.setDataMessage(try dataMessage.build()) return try proto.build() } catch { - SNLog("Couldn't construct visible message proto from: \(self).") + Log.warn(.messageSender, "Couldn't construct visible message proto from: \(self).") return nil } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index a301039d9..1a52ead6b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -94,7 +94,7 @@ extension MessageReceiver { guard db[.areCallsEnabled] && Permissions.microphone == .granted else { let state: CallMessage.MessageInfo.State = (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied) - SNLog("[MessageReceiver+Calls] Microphone permission is \(AVAudioSession.sharedInstance().recordPermission)") + Log.info(.calls, "Microphone permission is \(AVAudioSession.sharedInstance().recordPermission)") if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: state, using: dependencies) { let thread: SessionThread = try SessionThread.upsert( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift index fce34fe77..fce6470c9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift @@ -109,7 +109,7 @@ extension MessageReceiver { !ClosedGroupKeyPair .filter(ClosedGroupKeyPair.Columns.threadKeyPairHash == newKeyPair.threadKeyPairHash) .isNotEmpty(db) - else { return SNLog("Ignoring outdated NEW legacy group message due to more recent config state") } + else { return Log.info(.messageReceiver, "Ignoring outdated NEW legacy group message due to more recent config state") } try newKeyPair.insert(db) return @@ -298,14 +298,14 @@ extension MessageReceiver { let legacyGroupId: String = (explicitGroupPublicKey?.toHexString() ?? threadId) guard let userKeyPair: KeyPair = Identity.fetchUserKeyPair(db) else { - return SNLog("Couldn't find user X25519 key pair.") + return Log.error(.messageReceiver, "Couldn't find user X25519 key pair.") } guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: legacyGroupId) else { - return SNLog("Ignoring closed group encryption key pair for nonexistent group.") + return Log.warn(.messageReceiver, "Ignoring closed group encryption key pair for nonexistent group.") } guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return } guard let sender: String = message.sender, groupAdmins.contains(where: { $0.profileId == sender }) else { - return SNLog("Ignoring closed group encryption key pair from non-admin.") + return Log.info(.messageReceiver, "Ignoring closed group encryption key pair from non-admin.") } // Find our wrapper and decrypt it if possible let userPublicKey: String = SessionId(.standard, publicKey: userKeyPair.publicKey).hexString @@ -325,18 +325,14 @@ extension MessageReceiver { ) ).plaintext } - catch { - return SNLog("Couldn't decrypt closed group encryption key pair.") - } + catch { return Log.error(.messageReceiver, "Couldn't decrypt closed group encryption key pair.") } // Parse it let proto: SNProtoKeyPair do { proto = try SNProtoKeyPair.parseData(plaintext) } - catch { - return SNLog("Couldn't parse closed group encryption key pair.") - } + catch { return Log.error(.messageReceiver, "Couldn't parse closed group encryption key pair.") } do { let keyPair: ClosedGroupKeyPair = ClosedGroupKeyPair( @@ -357,13 +353,13 @@ extension MessageReceiver { } catch { if case DatabaseError.SQLITE_CONSTRAINT_UNIQUE = error { - return SNLog("Ignoring duplicate closed group encryption key pair.") + return Log.info(.messageReceiver, "Ignoring duplicate closed group encryption key pair.") } throw error } - SNLog("Received a new closed group encryption key pair.") + Log.info(.messageReceiver, "Received a new closed group encryption key pair.") } private static func handleClosedGroupNameChanged( @@ -542,7 +538,7 @@ extension MessageReceiver { allMembers .filter({ $0.role == .admin }) .contains(where: { $0.profileId == sender }) - else { return SNLog("Ignoring invalid closed group update.") } + else { return Log.warn(.messageReceiver, "Ignoring invalid closed group update.") } // Update libSession try? LibSession.update( @@ -682,7 +678,7 @@ extension MessageReceiver { ) throws { guard let sender: String = message.sender else { return } guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: threadId) else { - return SNLog("Ignoring group update for nonexistent group.") + return Log.warn(.messageReceiver, "Ignoring group update for nonexistent group.") } let timestampMs: Int64 = ( @@ -699,14 +695,14 @@ extension MessageReceiver { case .legacyGroup: // Check that the message isn't from before the group was created guard Double(message.sentTimestampMs ?? 0) > closedGroup.formationTimestamp else { - return SNLog("Ignoring legacy group update from before thread was created.") + return Log.warn(.messageReceiver, "Ignoring legacy group update from before thread was created.") } // If these values are missing then we probably won't be able to validly handle the message guard let allMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db), allMembers.contains(where: { $0.profileId == sender }) - else { return SNLog("Ignoring legacy group update from non-member.") } + else { return Log.warn(.messageReceiver, "Ignoring legacy group update from non-member.") } try legacyGroupChanges(sender, closedGroup, allMembers) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift index b016bb4d5..6bf23974e 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift @@ -47,7 +47,7 @@ extension MessageReceiver { dependencies[singleton: .typingIndicators].didStopTyping(db, threadId: threadId, direction: .incoming) default: - SNLog("Unknown TypingIndicator Kind ignored") + Log.warn(.messageReceiver, "Unknown TypingIndicator Kind ignored") return } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index dafeb02c9..cd6f41521 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -115,7 +115,7 @@ extension MessageReceiver { ) case .group: - SNLog("Ignoring message with invalid sender.") + Log.info(.messageReceiver, "Ignoring message with invalid sender.") throw MessageReceiverError.invalidSender } }() diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift index de9556932..4a8df8196 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift @@ -319,7 +319,7 @@ extension MessageSender { // Get the group, check preconditions & prepare guard (try? SessionThread.exists(db, id: legacyGroupSessionId)) == true else { - SNLog("Can't update nonexistent closed group.") + Log.warn(.messageSender, "Can't update nonexistent closed group.") throw MessageSenderError.noThread } guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: legacyGroupSessionId) else { @@ -555,12 +555,12 @@ extension MessageSender { using dependencies: Dependencies ) -> AnyPublisher { guard !removedMembers.contains(userSessionId.hexString) else { - SNLog("Invalid closed group update.") + Log.warn(.messageSender, "Invalid closed group update.") return Fail(error: MessageSenderError.invalidClosedGroupUpdate) .eraseToAnyPublisher() } guard allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userSessionId.hexString }) else { - SNLog("Only an admin can remove members from a group.") + Log.warn(.messageSender, "Only an admin can remove members from a group.") return Fail(error: MessageSenderError.invalidClosedGroupUpdate) .eraseToAnyPublisher() } @@ -645,7 +645,7 @@ extension MessageSender { using dependencies: Dependencies ) { guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else { - return SNLog("Couldn't send key pair for nonexistent closed group.") + return Log.warn(.messageSender, "Couldn't send key pair for nonexistent closed group.") } guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { return @@ -654,7 +654,7 @@ extension MessageSender { return } guard allGroupMembers.contains(where: { $0.role == .standard && $0.profileId == publicKey }) else { - return SNLog("Refusing to send latest encryption key pair to non-member.") + return Log.error(.messageSender, "Refusing to send latest encryption key pair to non-member.") } // Get the latest encryption key pair @@ -692,7 +692,7 @@ extension MessageSender { ) ) - SNLog("Sending latest encryption key pair to: \(publicKey).") + Log.info(.messageSender, "Sending latest encryption key pair to: \(publicKey).") try MessageSender.send( db, message: ClosedGroupControlMessage( diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 2c20df116..79900c13f 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -492,7 +492,7 @@ public enum PushNotificationAPI { let notification: BencodeResponse = try? BencodeDecoder(using: dependencies) .decode(BencodeResponse.self, from: decryptedData) else { - SNLog("Failed to decrypt or decode notification") + Log.error(.cat, "Failed to decrypt or decode notification") return (nil, .invalid, .failure) } @@ -506,7 +506,7 @@ public enum PushNotificationAPI { notification.info.dataLength == notificationData.count, !notificationData.isEmpty else { - SNLog("Get notification data failed") + Log.error(.cat, "Get notification data failed") return (nil, notification.info, .failureNoContent) } diff --git a/SessionMessagingKit/Utilities/Data+Utilities.swift b/SessionMessagingKit/Utilities/Data+Utilities.swift index 47b3e074e..fa68cab62 100644 --- a/SessionMessagingKit/Utilities/Data+Utilities.swift +++ b/SessionMessagingKit/Utilities/Data+Utilities.swift @@ -3,6 +3,12 @@ import Foundation import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("Data", defaultLevel: .warn) +} + // MARK: - Decoding public extension Data { @@ -18,7 +24,7 @@ public extension Data { break } else if bytes[targetIndex] != 0x00 { - SNLog("Failed to remove padding, returning unstripped padding"); + Log.error(.cat, "Failed to remove padding, returning unstripped padding"); return self } } diff --git a/SessionMessagingKit/Utilities/MessageWrapper.swift b/SessionMessagingKit/Utilities/MessageWrapper.swift index c778aa1ec..1e7f97ba3 100644 --- a/SessionMessagingKit/Utilities/MessageWrapper.swift +++ b/SessionMessagingKit/Utilities/MessageWrapper.swift @@ -58,7 +58,7 @@ public enum MessageWrapper { builder.setContent(content) return try builder.build() } catch let error { - SNLog("Failed to wrap message in envelope: \(error).") + Log.error(.messageSender, "Failed to wrap message in envelope: \(error).") throw Error.failedToWrapMessageInEnvelope } } @@ -71,7 +71,7 @@ public enum MessageWrapper { messageBuilder.setRequest(try requestBuilder.build()) return try messageBuilder.build() } catch let error { - SNLog("Failed to wrap envelope in web socket message: \(error).") + Log.error(.messageSender, "Failed to wrap envelope in web socket message: \(error).") throw Error.failedToWrapEnvelopeInWebSocketMessage } } @@ -90,7 +90,7 @@ public enum MessageWrapper { }() return try SNProtoEnvelope.parseData(envelopeData) } catch let error { - SNLog("Failed to unwrap data: \(error).") + Log.error(.messageSender, "Failed to unwrap data: \(error).") throw Error.failedToUnwrapData } } diff --git a/SessionMessagingKit/Utilities/Preferences+Sound.swift b/SessionMessagingKit/Utilities/Preferences+Sound.swift index 6fd0d418b..8812f2fa1 100644 --- a/SessionMessagingKit/Utilities/Preferences+Sound.swift +++ b/SessionMessagingKit/Utilities/Preferences+Sound.swift @@ -6,6 +6,14 @@ import GRDB import DifferenceKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("Preferences.Sound", defaultLevel: .warn) +} + +// MARK: - Preferences + public extension Preferences { enum Sound: Int, Codable, DatabaseValueConvertible, EnumIntSetting, Differentiable { public static var defaultiOSIncomingRingtone: Sound = .opening @@ -153,7 +161,7 @@ public extension Preferences { public func notificationSound(isQuiet: Bool) -> UNNotificationSound { guard let filename: String = filename(quiet: isQuiet) else { - SNLog("[Preferences.Sound] filename was unexpectedly nil") + Log.warn(.cat, "Filename was unexpectedly nil") return UNNotificationSound.default } diff --git a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift index 5cb291067..98d331cd1 100644 --- a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift +++ b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit public extension SNProtoEnvelope { static func from(_ message: SnodeReceivedMessage) -> SNProtoEnvelope? { guard let result = try? MessageWrapper.unwrap(data: message.data) else { - SNLog("Failed to unwrap data for message: \(String(reflecting: message)).") + Log.error(.messageReceiver, "Failed to unwrap data for message: \(String(reflecting: message)).") return nil } diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index f722acc8a..54e6beff2 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -13,7 +13,10 @@ class MockLibSessionCache: Mock, LibSessionCacheType { // MARK: - State Management - func loadState(_ db: Database) { mockNoReturn(untrackedArgs: [db]) } + func loadState(_ db: Database, requestId: String?) { + mockNoReturn(args: [requestId], untrackedArgs: [db]) + } + func loadDefaultStatesFor( userConfigVariants: Set, groups: [ClosedGroup], diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 8af99a3ef..759182def 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -12,11 +12,18 @@ import SessionSnodeKit import SignalUtilitiesKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("NotificationServiceExtension", defaultLevel: .info) +} + +// MARK: - NotificationServiceExtension + public final class NotificationServiceExtension: UNNotificationServiceExtension { // Called via the OS so create a default 'Dependencies' instance private var dependencies: Dependencies = Dependencies.createEmpty() private var startTime: CFTimeInterval = 0 - private var fallbackRunId: String = "N/A" // stringlint:ignore private var contentHandler: ((UNNotificationContent) -> Void)? private var request: UNNotificationRequest? @ThreadSafe private var hasCompleted: Bool = false @@ -32,9 +39,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: Did receive a remote push notification request override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - let runId: String = UUID().uuidString self.startTime = CACurrentMediaTime() - self.fallbackRunId = runId self.contentHandler = contentHandler self.request = request @@ -47,14 +52,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Abort if the main app is running guard !dependencies[defaults: .appGroup, key: .isMainAppActive] else { - return self.completeSilenty(.ignoreDueToMainAppRunning, runId: runId) + return self.completeSilenty(.ignoreDueToMainAppRunning, requestId: request.identifier) } guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { - return self.completeSilenty(.ignoreDueToNoContentFromApple, runId: runId) + return self.completeSilenty(.ignoreDueToNoContentFromApple, requestId: request.identifier) } - Log.info("didReceive called with runId: \(runId).") + Log.info(.cat, "didReceive called with requestId: \(request.identifier).") /// Create the context if we don't have it (needed before _any_ interaction with the database) if !dependencies[singleton: .appContext].isValid { @@ -65,14 +70,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } /// Actually perform the setup - DispatchQueue.main.sync { - self.performSetup(runId: runId) { [weak self] in - self?.handleNotification(notificationContent, runId: runId) - } + self.performSetup(requestId: request.identifier) { [weak self] in + self?.handleNotification(notificationContent, requestId: request.identifier) } } - private func handleNotification(_ notificationContent: UNMutableNotificationContent, runId: String) { + private func handleNotification(_ notificationContent: UNMutableNotificationContent, requestId: String) { let (maybeData, metadata, result) = PushNotificationAPI.processNotification( notificationContent: notificationContent, using: dependencies @@ -92,21 +95,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension threadVariant: nil, threadDisplayName: nil, resolution: .errorProcessing(result), - runId: runId + requestId: requestId ) case (.success, _), (.legacySuccess, _), (.failure, _): - return self.completeSilenty(.errorProcessing(result), runId: runId) + return self.completeSilenty(.errorProcessing(result), requestId: requestId) // Just log if the notification was too long (a ~2k message should be able to fit so // these will most commonly be call or config messages) case (.successTooLong, _): - return self.completeSilenty(.ignoreDueToContentSize(metadata), runId: runId) + return self.completeSilenty(.ignoreDueToContentSize(metadata), requestId: requestId) - case (.failureNoContent, _): return self.completeSilenty(.errorNoContent(metadata), runId: runId) - case (.legacyFailure, _): return self.completeSilenty(.errorNoContentLegacy, runId: runId) + case (.failureNoContent, _): return self.completeSilenty(.errorNoContent(metadata), requestId: requestId) + case (.legacyFailure, _): return self.completeSilenty(.errorNoContentLegacy, requestId: requestId) case (.legacyForceSilent, _): - return self.completeSilenty(.ignoreDueToNonLegacyGroupLegacyNotification, runId: runId) + return self.completeSilenty(.ignoreDueToNonLegacyGroupLegacyNotification, requestId: requestId) } } @@ -242,7 +245,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension using: dependencies ) - return self?.handleSuccessForIncomingCall(db, for: callMessage, runId: runId) + return self?.handleSuccessForIncomingCall(db, for: callMessage, requestId: requestId) } // Perform any required post-handling logic @@ -294,8 +297,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } db.afterNextTransaction( - onCommit: { _ in self?.completeSilenty(.success(metadata), runId: runId) }, - onRollback: { _ in self?.completeSilenty(.errorTransactionFailure, runId: runId) } + onCommit: { _ in self?.completeSilenty(.success(metadata), requestId: requestId) }, + onRollback: { _ in self?.completeSilenty(.errorTransactionFailure, requestId: requestId) } ) } catch { @@ -307,23 +310,23 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension DispatchQueue.main.async { switch (error, processedThreadVariant, metadata.namespace.isConfigNamespace) { case (MessageReceiverError.noGroupKeyPair, _, _): - self?.completeSilenty(.errorLegacyGroupKeysMissing, runId: runId) + self?.completeSilenty(.errorLegacyGroupKeysMissing, requestId: requestId) case (MessageReceiverError.outdatedMessage, _, _): - self?.completeSilenty(.ignoreDueToOutdatedMessage, runId: runId) + self?.completeSilenty(.ignoreDueToOutdatedMessage, requestId: requestId) case (MessageReceiverError.ignorableMessage, _, _): - self?.completeSilenty(.ignoreDueToRequiresNoNotification, runId: runId) + self?.completeSilenty(.ignoreDueToRequiresNoNotification, requestId: requestId) case (MessageReceiverError.duplicateMessage, _, _), (MessageReceiverError.duplicateControlMessage, _, _), (MessageReceiverError.duplicateMessageNewSnode, _, _): - self?.completeSilenty(.ignoreDueToDuplicateMessage, runId: runId) + self?.completeSilenty(.ignoreDueToDuplicateMessage, requestId: requestId) /// If it was a `decryptionFailed` error, but it was for a config namespace then just fail silently (don't /// want to show the fallback notification in this case) case (MessageReceiverError.decryptionFailed, _, true): - self?.completeSilenty(.errorMessageHandling(.decryptionFailed), runId: runId) + self?.completeSilenty(.errorMessageHandling(.decryptionFailed), requestId: requestId) /// If it was a `decryptionFailed` error for a group conversation and the group doesn't exist or /// doesn't have auth info (ie. group destroyed or member kicked), then just fail silently (don't want @@ -336,7 +339,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension group.authData != nil ) else { - self?.completeSilenty(.errorMessageHandling(.decryptionFailed), runId: runId) + self?.completeSilenty(.errorMessageHandling(.decryptionFailed), requestId: requestId) return } @@ -347,7 +350,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension threadVariant: processedThreadVariant, threadDisplayName: threadDisplayName, resolution: .errorMessageHandling(.decryptionFailed), - runId: runId + requestId: requestId ) case (let msgError as MessageReceiverError, _, _): @@ -357,7 +360,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension threadVariant: processedThreadVariant, threadDisplayName: threadDisplayName, resolution: .errorMessageHandling(msgError), - runId: runId + requestId: requestId ) default: @@ -367,7 +370,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension threadVariant: processedThreadVariant, threadDisplayName: threadDisplayName, resolution: .errorOther(error), - runId: runId + requestId: requestId ) } } @@ -384,12 +387,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: Setup - private func performSetup(runId: String, completion: @escaping () -> Void) { - Log.info("Performing setup for runId: \(runId).") + private func performSetup(requestId: String, completion: @escaping () -> Void) { + Log.info(.cat, "Performing setup for requestId: \(requestId).") dependencies.warmCache(cache: .appVersion) AppSetup.setupEnvironment( + requestId: requestId, appSpecificBlock: { [dependencies] in // stringlint:ignore_start Log.setup(with: Logger( @@ -416,12 +420,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension }, migrationsCompletion: { [weak self, dependencies] result in switch result { - case .failure(let error): self?.completeSilenty(.errorDatabaseMigrations(error), runId: runId) + case .failure(let error): self?.completeSilenty(.errorDatabaseMigrations(error), requestId: requestId) case .success: DispatchQueue.main.async { // Ensure storage is actually valid guard dependencies[singleton: .storage].isValid else { - self?.completeSilenty(.errorDatabaseInvalid, runId: runId) + self?.completeSilenty(.errorDatabaseInvalid, requestId: requestId) return } @@ -431,7 +435,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // so it is possible that could change in the future. If it does, do nothing // and don't disturb the user. Messages will be processed when they open the app. guard dependencies[singleton: .storage, key: .isReadyForAppExtensions] else { - self?.completeSilenty(.errorNotReadyForExtensions, runId: runId) + self?.completeSilenty(.errorNotReadyForExtensions, requestId: requestId) return } @@ -454,10 +458,18 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension override public func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - completeSilenty(.errorTimeout, runId: fallbackRunId) + completeSilenty(.errorTimeout, requestId: (request?.identifier ?? "N/A")) // stringlint:ignore } - private func completeSilenty(_ resolution: NotificationResolution, runId: String) { + private func completeSilenty(_ resolution: NotificationResolution, requestId: String) { + // This can be called from within database threads so to prevent blocking and weird + // behaviours make sure to send it to the main thread instead + guard Thread.isMainThread else { + return DispatchQueue.main.async { [weak self] in + self?.completeSilenty(resolution, requestId: requestId) + } + } + // Ensure we only run this once guard _hasCompleted.performUpdateAndMap({ (true, $0) }) == false else { return } @@ -474,8 +486,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } let duration: CFTimeInterval = (CACurrentMediaTime() - startTime) - Log.custom(resolution.logLevel, [], "\(resolution) after \(.seconds(duration), unit: .ms), runId: \(runId).") + Log.custom(resolution.logLevel, [.cat], "\(resolution) after \(.seconds(duration), unit: .ms), requestId: \(requestId).") Log.flush() + Log.reset() self.contentHandler!(silentContent) } @@ -483,7 +496,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func handleSuccessForIncomingCall( _ db: Database, for callMessage: CallMessage, - runId: String + requestId: String ) { if #available(iOSApplicationExtension 14.5, *), Preferences.isCallKitSupported { guard let caller: String = callMessage.sender, let timestamp = callMessage.sentTimestampMs else { return } @@ -499,14 +512,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in if let error = error { - Log.error("Failed to notify main app of call message: \(error).") + Log.error(.cat, "Failed to notify main app of call message: \(error).") dependencies[singleton: .storage].read { db in - self?.handleFailureForVoIP(db, for: callMessage, runId: runId) + self?.handleFailureForVoIP(db, for: callMessage, requestId: requestId) } } else { dependencies[defaults: .appGroup, key: .lastCallPreOffer] = Date() - self?.completeSilenty(.successCall, runId: runId) + self?.completeSilenty(.successCall, requestId: requestId) } } } @@ -517,11 +530,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) } else { - self.handleFailureForVoIP(db, for: callMessage, runId: runId) + self.handleFailureForVoIP(db, for: callMessage, requestId: requestId) } } - private func handleFailureForVoIP(_ db: Database, for callMessage: CallMessage, runId: String) { + private func handleFailureForVoIP(_ db: Database, for callMessage: CallMessage, requestId: String) { let notificationContent = UNMutableNotificationContent() notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ] notificationContent.title = Constants.app_name @@ -545,16 +558,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension UNUserNotificationCenter.current().add(request) { error in if let error = error { - Log.error("Failed to add notification request due to error: \(error).") + Log.error(.cat, "Failed to add notification request for requestId: \(requestId) due to error: \(error).") } semaphore.signal() } semaphore.wait() - Log.info("Add remote notification request.") + Log.info(.cat, "Add remote notification request for requestId: \(requestId).") db.afterNextTransaction( - onCommit: { [weak self] _ in self?.completeSilenty(.errorCallFailure, runId: runId) }, - onRollback: { [weak self] _ in self?.completeSilenty(.errorTransactionFailure, runId: runId) } + onCommit: { [weak self] _ in self?.completeSilenty(.errorCallFailure, requestId: requestId) }, + onRollback: { [weak self] _ in self?.completeSilenty(.errorTransactionFailure, requestId: requestId) } ) } @@ -564,24 +577,42 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension threadVariant: SessionThread.Variant?, threadDisplayName: String?, resolution: NotificationResolution, - runId: String + requestId: String ) { + // This can be called from within database threads so to prevent blocking and weird + // behaviours make sure to send it to the main thread instead + guard Thread.isMainThread else { + return DispatchQueue.main.async { [weak self] in + self?.handleFailure( + for: content, + metadata: metadata, + threadVariant: threadVariant, + threadDisplayName: threadDisplayName, + resolution: resolution, + requestId: requestId + ) + } + } + let duration: CFTimeInterval = (CACurrentMediaTime() - startTime) - Log.error("\(resolution) after \(.seconds(duration), unit: .ms), showing generic failure message for message from namespace: \(metadata.namespace), runId: \(runId).") - Log.flush() + let previewType: Preferences.NotificationPreviewType = dependencies[singleton: .storage, key: .preferencesNotificationPreviewType] + .defaulting(to: .nameAndPreview) + Log.error(.cat, "\(resolution) after \(.seconds(duration), unit: .ms), showing generic failure message for message from namespace: \(metadata.namespace), requestId: \(requestId).") + /// Now we are done with the database, we should suspend it if !dependencies[defaults: .appGroup, key: .isMainAppActive] { dependencies[singleton: .storage].suspendDatabaseAccess() } + /// Clear the logger + Log.flush() + Log.reset() + content.title = Constants.app_name content.userInfo = [ NotificationServiceExtension.isFromRemoteKey: true ] /// If it's a notification for a group conversation, the notification preferences are right and we have a name for the group /// then we should include it in the notification content - let previewType: Preferences.NotificationPreviewType = dependencies[singleton: .storage, key: .preferencesNotificationPreviewType] - .defaulting(to: .nameAndPreview) - switch (threadVariant, previewType, threadDisplayName) { case (.group, .nameAndPreview, .some(let name)), (.group, .nameNoPreview, .some(let name)), (.legacyGroup, .nameAndPreview, .some(let name)), (.legacyGroup, .nameNoPreview, .some(let name)): diff --git a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift b/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift index 5ed2d5282..e55052a26 100644 --- a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift +++ b/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift @@ -28,10 +28,10 @@ extension DeleteAllBeforeResponse: ValidatableResponse { result[next.key] = false if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't delete data from: \(next.key).") + Log.warn(.validator(self), "Couldn't delete data from: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift b/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift index e539b6f8b..966102b20 100644 --- a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift +++ b/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift @@ -70,10 +70,10 @@ extension DeleteAllMessagesResponse: ValidatableResponse { result[next.key] = false if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't delete data from: \(next.key).") + Log.warn(.validator(self), "Couldn't delete data from: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/RevokeSubaccountResponse.swift b/SessionSnodeKit/Models/RevokeSubaccountResponse.swift index ea6f54a4e..0b6b44f1f 100644 --- a/SessionSnodeKit/Models/RevokeSubaccountResponse.swift +++ b/SessionSnodeKit/Models/RevokeSubaccountResponse.swift @@ -26,10 +26,10 @@ extension RevokeSubaccountResponse: ValidatableResponse { let encodedSignature: Data = Data(base64Encoded: signatureBase64) else { if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't revoke subaccount from: \(next.key).") + Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/SendMessageResponse.swift b/SessionSnodeKit/Models/SendMessageResponse.swift index d4830e7a5..8bea2dfa5 100644 --- a/SessionSnodeKit/Models/SendMessageResponse.swift +++ b/SessionSnodeKit/Models/SendMessageResponse.swift @@ -105,10 +105,10 @@ extension SendMessagesResponse: ValidatableResponse { result[next.key] = false if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't store message on: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't store message on: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't store message on: \(next.key).") + Log.warn(.validator(self), "Couldn't store message on: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/SnodeReceivedMessage.swift b/SessionSnodeKit/Models/SnodeReceivedMessage.swift index 0e75b1f60..c950a74a6 100644 --- a/SessionSnodeKit/Models/SnodeReceivedMessage.swift +++ b/SessionSnodeKit/Models/SnodeReceivedMessage.swift @@ -22,7 +22,7 @@ public struct SnodeReceivedMessage: CustomDebugStringConvertible { rawMessage: GetMessagesResponse.RawMessage ) { guard let data: Data = Data(base64Encoded: rawMessage.base64EncodedDataString) else { - SNLog("Failed to decode data for message: \(rawMessage).") + Log.error(.network, "Failed to decode data for message: \(rawMessage).") return nil } diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift b/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift index fdb30c94b..3b3f9827e 100644 --- a/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift +++ b/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift @@ -26,10 +26,10 @@ extension UnrevokeSubaccountResponse: ValidatableResponse { let encodedSignature: Data = Data(base64Encoded: signatureBase64) else { if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't revoke subaccount from: \(next.key).") + Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift b/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift index 76afc92d4..e2655910c 100644 --- a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift +++ b/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift @@ -70,10 +70,10 @@ extension UpdateExpiryAllResponse: ValidatableResponse { result[next.key] = [] if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't update expiry from: \(next.key).") + Log.warn(.validator(self), "Couldn't update expiry from: \(next.key).") } return } diff --git a/SessionSnodeKit/Models/UpdateExpiryResponse.swift b/SessionSnodeKit/Models/UpdateExpiryResponse.swift index d8c35234e..c1cafb8c7 100644 --- a/SessionSnodeKit/Models/UpdateExpiryResponse.swift +++ b/SessionSnodeKit/Models/UpdateExpiryResponse.swift @@ -72,10 +72,10 @@ extension UpdateExpiryResponse: ValidatableResponse { result[next.key] = UpdateExpiryResponseResult(changed: [:], unchanged: [:], didError: true) if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - SNLog("Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).") + Log.warn(.validator(self), "Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { - SNLog("Couldn't update expiry from: \(next.key).") + Log.warn(.validator(self), "Couldn't update expiry from: \(next.key).") } return } diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionSnodeKit/Types/PreparedRequest.swift index c6cf77171..819c6537e 100644 --- a/SessionSnodeKit/Types/PreparedRequest.swift +++ b/SessionSnodeKit/Types/PreparedRequest.swift @@ -415,7 +415,7 @@ extension Network.PreparedRequest: ErasedPreparedRequest { public func encodeForBatchRequest(to encoder: Encoder) throws { switch batchRequestVariant { case .unsupported: - SNLog("Attempted to encode unsupported request type \(endpointName) as a batch subrequest") + Log.critical("Attempted to encode unsupported request type \(endpointName) as a batch subrequest") case .sogs: var container: KeyedEncodingContainer = encoder.container(keyedBy: Network.BatchRequest.Child.CodingKeys.self) diff --git a/SessionSnodeKit/Types/ValidatableResponse.swift b/SessionSnodeKit/Types/ValidatableResponse.swift index f29687273..7aaa0de95 100644 --- a/SessionSnodeKit/Types/ValidatableResponse.swift +++ b/SessionSnodeKit/Types/ValidatableResponse.swift @@ -3,6 +3,16 @@ import Foundation import SessionUtilitiesKit +// MARK: - Log.Category + +internal extension Log.Category { + static func validator(_ response: Any) -> Log.Category { + return .create("ResponseValidator", customSuffix: "-\(type(of: response))", defaultLevel: .warn) + } +} + +// MARK: - ValidatableResponse + internal protocol ValidatableResponse { associatedtype ValidationData associatedtype ValidationResponse diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift index 4934d3309..732dbb156 100644 --- a/SessionUtilitiesKit/Database/Models/Identity.swift +++ b/SessionUtilitiesKit/Database/Models/Identity.swift @@ -143,7 +143,7 @@ public extension Identity { ] // stringlint:ignore_stop - SNLog("Failed to retrieve keys for mnemonic generation (\(dbStates.joined(separator: ", ")))") + Log.critical("Failed to retrieve keys for mnemonic generation (\(dbStates.joined(separator: ", ")))") throw StorageError.objectNotFound } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index f0dd0ff29..70d683165 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -498,7 +498,7 @@ public class PagedDatabaseObserver: TransactionObserver where let currentPageInfo: PagedData.PageInfo = self.pageInfo if case .initialPageAround(_) = target, currentPageInfo.currentCount > 0 { - SNLog("Unable to load initialPageAround if there is already data") + Log.warn(.cat, "Unable to load initialPageAround if there is already data") return } diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 92a552064..de216aba4 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -55,23 +55,26 @@ public enum Log { public struct Category: Hashable { public let rawValue: String fileprivate let customPrefix: String + fileprivate let customSuffix: String public let defaultLevel: Log.Level fileprivate static let identifierPrefix: String = "logLevel-" fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" } - private init(rawValue: String, customPrefix: String, defaultLevel: Log.Level) { + private init(rawValue: String, customPrefix: String, customSuffix: String, defaultLevel: Log.Level) { /// If we've already registered this category then assume the original has the correct `defaultLevel` and only /// modify the `customPrefix` value switch AllLoggingCategories.existingCategory(for: rawValue) { case .some(let existingCategory): self.rawValue = existingCategory.rawValue self.customPrefix = customPrefix + self.customSuffix = customSuffix self.defaultLevel = existingCategory.defaultLevel case .none: self.rawValue = rawValue self.customPrefix = customPrefix + self.customSuffix = customSuffix self.defaultLevel = defaultLevel AllLoggingCategories.register(category: self) @@ -84,16 +87,27 @@ public enum Log { self.init( rawValue: identifier.substring(from: Category.identifierPrefix.count), customPrefix: "", + customSuffix: "", defaultLevel: .default ) } - public init(rawValue: String, customPrefix: String = "") { - self.init(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: .default) + public init(rawValue: String, customPrefix: String = "", customSuffix: String = "") { + self.init(rawValue: rawValue, customPrefix: customPrefix, customSuffix: customSuffix, defaultLevel: .default) } - @discardableResult public static func create(_ rawValue: String, customPrefix: String = "", defaultLevel: Log.Level) -> Log.Category { - return Log.Category(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: defaultLevel) + @discardableResult public static func create( + _ rawValue: String, + customPrefix: String = "", + customSuffix: String = "", + defaultLevel: Log.Level + ) -> Log.Category { + return Log.Category( + rawValue: rawValue, + customPrefix: customPrefix, + customSuffix: customSuffix, + defaultLevel: defaultLevel + ) } } @@ -167,6 +181,10 @@ public enum Log { DDLog.flushLog() } + public static func reset() { + Log._logger.set(to: nil) + } + // MARK: - Log Functions fileprivate static func empty() { @@ -187,6 +205,7 @@ public enum Log { } } + // FIXME: Would be nice to properly require a category for all logs public static func verbose( _ msg: String, file: StaticString = #file, @@ -366,7 +385,6 @@ public enum Log { public class Logger { private let dependencies: Dependencies private let primaryPrefix: String - private let forceNSLog: Bool @ThreadSafeObject private var systemLoggers: [String: SystemLoggerType] = [:] fileprivate let fileLogger: DDFileLogger @ThreadSafe fileprivate var isSuspended: Bool = true @@ -375,12 +393,10 @@ public class Logger { public init( primaryPrefix: String, customDirectory: String? = nil, - forceNSLog: Bool = false, using dependencies: Dependencies ) { self.dependencies = dependencies self.primaryPrefix = primaryPrefix - self.forceNSLog = forceNSLog switch customDirectory { case .none: self.fileLogger = DDFileLogger() @@ -409,6 +425,12 @@ public class Logger { self.loadExtensionLogsAndResumeLogging() } + deinit { + // Need to ensure we remove the `fileLogger` from `DDLog` otherwise we will get duplicate + // log entries + DDLog.remove(fileLogger) + } + // MARK: - Functions fileprivate func setPendingLogsRetriever(_ callback: @escaping () -> [Log.LogInfo]) { @@ -565,7 +587,13 @@ public class Logger { (DispatchQueue.isDBWriteQueue ? "DBWrite" : nil) ] .compactMap { $0 } - .appending(contentsOf: categories.map { "\($0.customPrefix)\($0.rawValue)" }) + .appending( + contentsOf: categories + /// No point doubling up but we want to allow categories which match the `primaryPrefix` so that we + /// have a mechanism for providing a different "default" log level for a specific target + .filter { $0.rawValue != primaryPrefix } + .map { "\($0.customPrefix)\($0.rawValue)\($0.customSuffix)" } + ) .joined(separator: ", ") return "[\(prefixes)] " @@ -769,8 +797,3 @@ public struct AllLoggingCategories: FeatureOption { public var title: String = "AllLoggingCategories" public let subtitle: String? = nil } - -// FIXME: Remove this once everything has been updated to use the new `Log.x()` methods. -public func SNLog(_ message: String, forceNSLog: Bool = false) { - Log.info(message) -} diff --git a/SessionUtilitiesKit/Media/MediaUtils.swift b/SessionUtilitiesKit/Media/MediaUtils.swift index 0497a83b2..dd4ae7d12 100644 --- a/SessionUtilitiesKit/Media/MediaUtils.swift +++ b/SessionUtilitiesKit/Media/MediaUtils.swift @@ -3,10 +3,20 @@ import UIKit import AVFoundation +// MARK: - Log.Category + +public extension Log.Category { + static let media: Log.Category = .create("MediaUtils", defaultLevel: .warn) +} + +// MARK: - MediaError + public enum MediaError: Error { case failure(description: String) } +// MARK: - MediaUtils + public enum MediaUtils { public static var maxFileSizeAnimatedImage: UInt { SNUtilitiesKit.maxFileSize } public static var maxFileSizeImage: UInt { SNUtilitiesKit.maxFileSize } @@ -19,7 +29,7 @@ public enum MediaUtils { public static let maxVideoDimensions: CGFloat = 3 * 1024 public static func thumbnail(forImageAtPath path: String, maxDimension: CGFloat, type: String, using dependencies: Dependencies) throws -> UIImage { - SNLog("thumbnailing image: \(path)") + Log.verbose(.media, "Thumbnailing image: \(path)") guard dependencies[singleton: .fileManager].fileExists(atPath: path) else { throw MediaError.failure(description: "Media file missing.") @@ -37,7 +47,7 @@ public enum MediaUtils { } public static func thumbnail(forVideoAtPath path: String, maxDimension: CGFloat, using dependencies: Dependencies) throws -> UIImage { - SNLog("thumbnailing video: \(path)") + Log.verbose(.media, "Thumbnailing video: \(path)") guard isVideoOfValidContentTypeAndSize(path: path, using: dependencies) else { throw MediaError.failure(description: "Media file has missing or invalid length.") @@ -61,7 +71,7 @@ public enum MediaUtils { public static func isValidVideo(path: String, using dependencies: Dependencies) -> Bool { guard isVideoOfValidContentTypeAndSize(path: path, using: dependencies) else { - SNLog("Media file has missing or invalid length.") + Log.error(.media, "Media file has missing or invalid length.") return false } @@ -72,21 +82,21 @@ public enum MediaUtils { private static func isVideoOfValidContentTypeAndSize(path: String, using dependencies: Dependencies) -> Bool { guard dependencies[singleton: .fileManager].fileExists(atPath: path) else { - SNLog("Media file missing.") + Log.error(.media, "Media file missing.") return false } let fileExtension = URL(fileURLWithPath: path).pathExtension guard let contentType: String = UTType.sessionMimeType(for: fileExtension) else { - SNLog("Media file has unknown content type.") + Log.error(.media, "Media file has unknown content type.") return false } guard UTType.isVideo(contentType) else { - SNLog("Media file has invalid content type.") + Log.error(.media, "Media file has invalid content type.") return false } guard let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path) else { - SNLog("Media file has unknown length.") + Log.error(.media, "Media file has unknown length.") return false } return UInt(fileSize) <= SNUtilitiesKit.maxFileSize @@ -100,11 +110,11 @@ public enum MediaUtils { maxTrackSize.height = max(maxTrackSize.height, trackSize.height) } if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 { - SNLog("Invalid video size: \(maxTrackSize)") + Log.error(.media, "Invalid video size: \(maxTrackSize)") return false } if maxTrackSize.width > maxVideoDimensions || maxTrackSize.height > maxVideoDimensions { - SNLog("Invalid video dimensions: \(maxTrackSize)") + Log.error(.media, "Invalid video dimensions: \(maxTrackSize)") return false } return true diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index aaea34c0e..03f5edadb 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -242,7 +242,7 @@ public class AttachmentPrepViewController: OWSViewController { } @objc public func playButtonTapped() { - guard let fileUrl: URL = attachment.dataUrl else { return SNLog("Missing video file") } + guard let fileUrl: URL = attachment.dataUrl else { return Log.error(.media, "Missing video file") } let player: AVPlayer = AVPlayer(url: fileUrl) let viewController: AVPlayerViewController = AVPlayerViewController() diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index 32f93de56..cdd78c43c 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -9,6 +9,7 @@ import SessionUtilitiesKit public enum AppSetup { public static func setupEnvironment( + requestId: String? = nil, additionalMigrationTargets: [MigratableTarget.Type] = [], appSpecificBlock: (() -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, @@ -37,9 +38,10 @@ public enum AppSetup { proximityMonitoringManager: OWSProximityMonitoringManagerImpl(using: dependencies), windowManager: OWSWindowManager(default: ()) ) - appSpecificBlock?() + appSpecificBlock?() runPostSetupMigrations( + requestId: requestId, backgroundTask: backgroundTask, additionalMigrationTargets: additionalMigrationTargets, migrationProgressChanged: migrationProgressChanged, @@ -53,6 +55,7 @@ public enum AppSetup { } public static func runPostSetupMigrations( + requestId: String? = nil, backgroundTask: SessionBackgroundTask? = nil, additionalMigrationTargets: [MigratableTarget.Type] = [], migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, @@ -90,7 +93,7 @@ public enum AppSetup { userSessionId: userSessionId, using: dependencies ) - cache.loadState(db) + cache.loadState(db, requestId: requestId) dependencies.set(cache: .libSession, to: cache) } From cd919284f0c09e9cc2f7c9e2c72d2893768ce83d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 7 Mar 2025 14:20:45 +1100 Subject: [PATCH 12/24] Updated the invalid config errors to include the pubkey for debugging --- .../LibSession+SharedGroup.swift | 22 ++++++------ .../LibSession+SessionMessagingKit.swift | 36 ++++++++++--------- .../LibSession/LibSessionError.swift | 4 +-- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift index 0c88db8c4..254b88007 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift @@ -29,7 +29,7 @@ internal extension LibSessionCacheType { guard groupState[.groupKeys] != nil && groupState[.groupInfo] != nil && groupState[.groupMembers] != nil else { Log.error(.libSession, "Group config objects were null") - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString) } groupState.forEach { variant, config in @@ -80,7 +80,7 @@ internal extension LibSession { // Extract the conf objects from the state to load in the initial data guard case .groupKeys(let groupKeysConf, let groupInfoConf, let groupMembersConf) = groupState[.groupKeys] else { Log.error(.libSession, "Group config objects were null") - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString) } // Set the initial values in the confs @@ -219,7 +219,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) try groups_members_init( &groupMembersConf, &groupIdentityPublicKey, @@ -227,7 +227,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) try groups_keys_init( &groupKeysConf, @@ -239,7 +239,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) case .none: try groups_info_init( @@ -249,7 +249,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) try groups_members_init( &groupMembersConf, &groupIdentityPublicKey, @@ -257,7 +257,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) try groups_keys_init( &groupKeysConf, @@ -269,7 +269,7 @@ internal extension LibSession { nil, 0, &error - ).orThrow(error: error) + ).orThrow(error: error, groupSessionId: groupSessionId) } guard @@ -278,7 +278,7 @@ internal extension LibSession { let membersConf: UnsafeMutablePointer = groupMembersConf else { Log.error(.libSession, "Group config objects were null") - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString) } // Define the config state map and load it into memory @@ -360,11 +360,11 @@ internal extension LibSessionCacheType { } private extension Int32 { - func orThrow(error: [CChar]) throws { + func orThrow(error: [CChar], groupSessionId: SessionId) throws { guard self != 0 else { return } Log.error(.libSession, "Unable to create group config objects: \(String(cString: error))") - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString) } } diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 39b775c2a..d4da2c91b 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -323,8 +323,8 @@ public extension LibSession { switch (variant, groupEd25519SecretKey) { case (.invalid, _): - throw LibSessionError.unableToCreateConfigObject - .logging("Unable to create \(variant.rawValue) config object") + throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) + .logging("Unable to create \(variant.rawValue) config object for: \(sessionId.hexString)") case (.userProfile, _), (.contacts, _), (.convoInfoVolatile, _), (.userGroups, _): return try (userConfigInitCalls[variant]?( @@ -334,7 +334,7 @@ public extension LibSession { (cachedDump?.length ?? 0), &error )) - .toConfig(conf, variant: variant, error: error) + .toConfig(conf, variant: variant, error: error, sessionId: sessionId) case (.groupInfo, .some(var adminSecretKey)), (.groupMembers, .some(var adminSecretKey)): var identityPublicKey: [UInt8] = sessionId.publicKey @@ -347,7 +347,7 @@ public extension LibSession { (cachedDump?.length ?? 0), &error )) - .toConfig(conf, variant: variant, error: error) + .toConfig(conf, variant: variant, error: error, sessionId: sessionId) case (.groupKeys, .some(var adminSecretKey)): var identityPublicKey: [UInt8] = sessionId.publicKey @@ -356,8 +356,8 @@ public extension LibSession { case .groupInfo(let infoConf) = configStore[sessionId, .groupInfo], case .groupMembers(let membersConf) = configStore[sessionId, .groupMembers] else { - throw LibSessionError.unableToCreateConfigObject - .logging("Unable to create \(variant.rawValue) config object for \(sessionId): Group info and member config states not loaded") + throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) + .logging("Unable to create \(variant.rawValue) config object for \(sessionId), group info \(configStore[sessionId, .groupInfo] != nil ? "loaded" : "not loaded") and member config \(configStore[sessionId, .groupMembers] != nil ? "loaded" : "not loaded")") } return try groups_keys_init( @@ -371,7 +371,7 @@ public extension LibSession { (cachedDump?.length ?? 0), &error ) - .toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error) + .toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error, sessionId: sessionId) // It looks like C doesn't deal will passing pointers to null variables well so we need // to explicitly pass 'nil' for the admin key in this case @@ -386,7 +386,7 @@ public extension LibSession { (cachedDump?.length ?? 0), &error )) - .toConfig(conf, variant: variant, error: error) + .toConfig(conf, variant: variant, error: error, sessionId: sessionId) // It looks like C doesn't deal will passing pointers to null variables well so we need // to explicitly pass 'nil' for the admin key in this case @@ -397,8 +397,8 @@ public extension LibSession { case .groupInfo(let infoConf) = configStore[sessionId, .groupInfo], case .groupMembers(let membersConf) = configStore[sessionId, .groupMembers] else { - throw LibSessionError.unableToCreateConfigObject - .logging("Unable to create \(variant.rawValue) config object for \(sessionId): Group info and member config states not loaded") + throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) + .logging("Unable to create \(variant.rawValue) config object for \(sessionId), group info \(configStore[sessionId, .groupInfo] != nil ? "loaded" : "not loaded") and member config \(configStore[sessionId, .groupMembers] != nil ? "loaded" : "not loaded")") } return try groups_keys_init( @@ -412,7 +412,7 @@ public extension LibSession { (cachedDump?.length ?? 0), &error ) - .toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error) + .toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error, sessionId: sessionId) } } @@ -1069,10 +1069,11 @@ private extension Optional where Wrapped == Int32 { func toConfig( _ maybeConf: UnsafeMutablePointer?, variant: ConfigDump.Variant, - error: [CChar] + error: [CChar], + sessionId: SessionId ) throws -> LibSession.Config { guard self == 0, let conf: UnsafeMutablePointer = maybeConf else { - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) .logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))") } @@ -1084,7 +1085,7 @@ private extension Optional where Wrapped == Int32 { case .groupInfo: return .groupInfo(conf) case .groupMembers: return .groupMembers(conf) - case .groupKeys, .invalid: throw LibSessionError.unableToCreateConfigObject + case .groupKeys, .invalid: throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) } } } @@ -1095,16 +1096,17 @@ private extension Int32 { info: UnsafeMutablePointer, members: UnsafeMutablePointer, variant: ConfigDump.Variant, - error: [CChar] + error: [CChar], + sessionId: SessionId ) throws -> LibSession.Config { guard self == 0, let conf: UnsafeMutablePointer = maybeConf else { - throw LibSessionError.unableToCreateConfigObject + throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) .logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))") } switch variant { case .groupKeys: return .groupKeys(conf, info: info, members: members) - default: throw LibSessionError.unableToCreateConfigObject + default: throw LibSessionError.unableToCreateConfigObject(sessionId.hexString) } } } diff --git a/SessionUtilitiesKit/LibSession/LibSessionError.swift b/SessionUtilitiesKit/LibSession/LibSessionError.swift index 3523ab219..47ea04708 100644 --- a/SessionUtilitiesKit/LibSession/LibSessionError.swift +++ b/SessionUtilitiesKit/LibSession/LibSessionError.swift @@ -6,7 +6,7 @@ import Foundation import SessionUtil public enum LibSessionError: Error, CustomStringConvertible { - case unableToCreateConfigObject + case unableToCreateConfigObject(String) case invalidConfigObject case invalidDataProvided case invalidConfigAccess @@ -117,7 +117,7 @@ public enum LibSessionError: Error, CustomStringConvertible { public var description: String { switch self { - case .unableToCreateConfigObject: return "Unable to create config object (LibSessionError.unableToCreateConfigObject)." + case .unableToCreateConfigObject(let pubkey): return "Unable to create config object for: \(pubkey) (LibSessionError.unableToCreateConfigObject)." case .invalidConfigObject: return "Invalid config object (LibSessionError.invalidConfigObject)." case .invalidDataProvided: return "Invalid data provided (LibSessionError.invalidDataProvided)." case .invalidConfigAccess: return "Invalid config access (LibSessionError.invalidConfigAccess)." From 7c4249b082a0e167be47151c82c6c2c67e7a6634 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 5 Mar 2025 14:44:12 +1100 Subject: [PATCH 13/24] Cleaned up some of the ScreenLock logic --- Session.xcodeproj/project.pbxproj | 32 +- Session/Meta/AppDelegate.swift | 5 +- ...eenLockUI.swift => ScreenLockWindow.swift} | 342 ++++++++---------- .../SAEScreenLockViewController.swift | 2 +- .../ScreenLockViewController.swift | 5 +- .../General}/ScreenLock.swift | 42 +-- 6 files changed, 189 insertions(+), 239 deletions(-) rename Session/Shared/{ScreenLockUI.swift => ScreenLockWindow.swift} (50%) rename {SignalUtilitiesKit/Screen Lock => SessionUIKit/Components}/ScreenLockViewController.swift (96%) rename {SignalUtilitiesKit/Screen Lock => SessionUtilitiesKit/General}/ScreenLock.swift (91%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5dacdb83c..f5f2846bc 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -311,7 +311,6 @@ C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; }; - C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; }; C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF304255B6DBE007E1867 /* ImageCache.swift */; }; C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; }; C38EF372255B6DCC007E1867 /* MediaMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF358255B6DCC007E1867 /* MediaMessageView.swift */; }; @@ -685,8 +684,6 @@ FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; - FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; }; - FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; }; FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; }; FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; }; FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; }; @@ -708,6 +705,9 @@ FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; + FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; + FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; + FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; @@ -1579,7 +1579,6 @@ C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = SessionUIKit/Components/PlaceholderIcon.swift; sourceTree = SOURCE_ROOT; }; C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfilePictureView.swift; path = SessionUIKit/Components/ProfilePictureView.swift; sourceTree = SOURCE_ROOT; }; C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIViewController+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIViewController+Utilities.swift"; sourceTree = SOURCE_ROOT; }; - C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/ScreenLock.swift"; sourceTree = SOURCE_ROOT; }; C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; }; C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; }; C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAudioPlayer.h; path = SessionMessagingKit/Utilities/OWSAudioPlayer.h; sourceTree = SOURCE_ROOT; }; @@ -1899,8 +1898,7 @@ FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = ""; }; - FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = ""; }; - FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; + FD52090828B59411006098F6 /* ScreenLockWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockWindow.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = ""; }; @@ -1918,6 +1916,8 @@ FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = ""; }; FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; + FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; + FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; @@ -2826,6 +2826,7 @@ C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */, C38EF2EF255B6DBB007E1867 /* Weak.swift */, + FD6673FE2D77F9BE00041530 /* ScreenLock.swift */, FD5D201D27B0D87C00FEA984 /* SessionId.swift */, 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */, 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */, @@ -2864,7 +2865,7 @@ FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */, 9422569E2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift */, B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */, - FD52090828B59411006098F6 /* ScreenLockUI.swift */, + FD52090828B59411006098F6 /* ScreenLockWindow.swift */, FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */, FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */, FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */, @@ -3122,6 +3123,7 @@ FD52090628B49738006098F6 /* ConfirmationModal.swift */, C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */, C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */, + FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */, FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */, FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */, FD0B77AF29B69A65009169BA /* TopBannerController.swift */, @@ -3136,7 +3138,6 @@ children = ( C33FD9B7255A54A300E217F9 /* Meta */, C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */, - C36096EE25AD21BC008B62B2 /* Screen Lock */, C3851CD225624B060061EEB0 /* Shared Views */, C360970125AD22D3008B62B2 /* Shared View Controllers */, C3CA3B11255CF17200F4C6D4 /* Utilities */, @@ -3320,15 +3321,6 @@ path = "Media Viewing & Editing"; sourceTree = ""; }; - C36096EE25AD21BC008B62B2 /* Screen Lock */ = { - isa = PBXGroup; - children = ( - C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */, - FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */, - ); - path = "Screen Lock"; - sourceTree = ""; - }; C360970125AD22D3008B62B2 /* Shared View Controllers */ = { isa = PBXGroup; children = ( @@ -5791,6 +5783,7 @@ FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */, 7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */, + FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */, FDE754BE2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift in Sources */, FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */, C331FFE02558FB0000070591 /* SearchBar.swift in Sources */, @@ -5826,7 +5819,6 @@ C38EF389255B6DD2007E1867 /* AttachmentTextView.swift in Sources */, C38EF3FF255B6DF7007E1867 /* TappableView.swift in Sources */, C38EF3C2255B6DE7007E1867 /* ImageEditorPaletteView.swift in Sources */, - C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */, C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */, C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */, C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, @@ -5851,7 +5843,6 @@ C38EF3FA255B6DF7007E1867 /* DirectionalPanGestureRecognizer.swift in Sources */, C38EF3BB255B6DE7007E1867 /* ImageEditorStrokeItem.swift in Sources */, C38EF3C0255B6DE7007E1867 /* ImageEditorCropViewController.swift in Sources */, - FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */, C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */, C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */, C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */, @@ -6007,6 +5998,7 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, + FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */, @@ -6319,7 +6311,6 @@ files = ( FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */, FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */, - FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */, 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */, FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */, FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, @@ -6430,6 +6421,7 @@ FDE754B72C9B96BB002A2623 /* WebRTCSession+MessageHandling.swift in Sources */, FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */, 7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */, + FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */, FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */, 9422569C2C23F8F000C0FDBF /* QRCodeScreen.swift in Sources */, 4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index e39048605..eb5605ec0 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -97,13 +97,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Note: Intentionally dispatching sync as we want to wait for these to complete before // continuing DispatchQueue.main.sync { - ScreenLockUI.shared.setupWithRootWindow(rootWindow: mainWindow, using: dependencies) + dependencies[singleton: .screenLock].setupWithRootWindow(rootWindow: mainWindow) OWSWindowManager.shared().setup( withRootWindow: mainWindow, - screenBlockingWindow: ScreenLockUI.shared.screenBlockingWindow, + screenBlockingWindow: dependencies[singleton: .screenLock].window, backgroundWindowLevel: .background ) - ScreenLockUI.shared.startObserving() } }, migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in diff --git a/Session/Shared/ScreenLockUI.swift b/Session/Shared/ScreenLockWindow.swift similarity index 50% rename from Session/Shared/ScreenLockUI.swift rename to Session/Shared/ScreenLockWindow.swift index 6d8ca7ed5..0d3979903 100644 --- a/Session/Shared/ScreenLockUI.swift +++ b/Session/Shared/ScreenLockWindow.swift @@ -1,122 +1,79 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import LocalAuthentication import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SignalUtilitiesKit -class ScreenLockUI { - public static let shared: ScreenLockUI = ScreenLockUI() - - private var dependencies: Dependencies? - - public lazy var screenBlockingWindow: UIWindow = { - let result: UIWindow = UIWindow() - result.isHidden = false - result.windowLevel = .background - result.isOpaque = true - result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) - result.rootViewController = self.screenBlockingViewController - - return result - }() - - private lazy var screenBlockingViewController: ScreenLockViewController = { - let result: ScreenLockViewController = ScreenLockViewController { [weak self] in - guard self?.appIsInactiveOrBackground == false else { - // This button can be pressed while the app is inactive - // for a brief window while the iOS auth UI is dismissing. - return - } +// MARK: - Singleton - Log.info("unlockButtonWasTapped") +public extension Singleton { + static let screenLock: SingletonConfig = Dependencies.create( + identifier: "screenLock", + createInstance: { dependencies in ScreenLockWindow(using: dependencies) } + ) +} - self?.didLastUnlockAttemptFail = false - self?.ensureUI() - } - - return result - }() +/// Obscures the app screen: +/// +/// * In the app switcher. +/// * During 'Screen Lock' unlock process. +public class ScreenLockWindow { + private let dependencies: Dependencies - /// Unlike UIApplication.applicationState, this state reflects the notifications, i.e. "did become active", "will resign active", - /// "will enter foreground", "did enter background". - /// - /// We want to update our state to reflect these transitions and have the "update" logic be consistent with "last reported" - /// state. i.e. when you're responding to "will resign active", we need to behave as though we're already inactive. + /// Indicates whether or not the user is currently locked out of the app. Should only be set if `db[.isScreenLockEnabled]`. /// - /// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the - /// app switcher. - private var appIsInactiveOrBackground: Bool = false { - didSet { - if self.appIsInactiveOrBackground { - if !self.isShowingScreenLockUI { - self.didLastUnlockAttemptFail = false - self.tryToActivateScreenLockBasedOnCountdown() - } - } - else if !self.didUnlockJustSucceed { - self.tryToActivateScreenLockBasedOnCountdown() - } - - self.didUnlockJustSucceed = false - self.ensureUI() - } - } - private var appIsInBackground: Bool = false { - didSet { - self.didUnlockJustSucceed = false - self.tryToActivateScreenLockBasedOnCountdown() - self.ensureUI() - } - } - + /// * The user is locked out by default on app launch. + /// * The user is also locked out if the app is sent to the background + @ThreadSafe private var isScreenLockLocked: Bool = false + private var isShowingScreenLockUI: Bool = false private var didUnlockJustSucceed: Bool = false private var didLastUnlockAttemptFail: Bool = false - + /// We want to remain in "screen lock" mode while "local auth" UI is dismissing. So we lazily clear isShowingScreenLockUI /// using this property. private var shouldClearAuthUIWhenActive: Bool = false - - /// Indicates whether or not the user is currently locked out of the app. Should only be set if db[.isScreenLockEnabled]. - /// - /// * The user is locked out by default on app launch. - /// * The user is also locked out if the app is sent to the background - @ThreadSafe private var isScreenLockLocked: Bool = false - // Determines what the state of the app should be. - private var desiredUIState: ScreenLockViewController.State { - if isScreenLockLocked { - if appIsInactiveOrBackground { - Log.verbose("desiredUIState: screen protection 1.") - return .protection - } - - Log.verbose("desiredUIState: screen lock 2.") - return (isShowingScreenLockUI ? .protection : .lock) - } - - if !self.appIsInactiveOrBackground { - // App is inactive or background. - Log.verbose("desiredUIState: none 3."); - return .none; - } + // MARK: - UI + + public lazy var window: UIWindow = { + let result: UIWindow = UIWindow() + result.isHidden = false + result.windowLevel = .background + result.isOpaque = true + result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) + result.rootViewController = self.viewController - if SessionEnvironment.shared?.isRequestingPermission == true { - return .none; + return result + }() + + private lazy var viewController: ScreenLockViewController = ScreenLockViewController { [weak self, dependencies] in + guard dependencies[singleton: .appContext].isAppForegroundAndActive else { + // This button can be pressed while the app is inactive + // for a brief window while the iOS auth UI is dismissing. + return } - Log.verbose("desiredUIState: screen protection 4.") - return .protection; + Log.info(.screenLock, "unlockButtonWasTapped") + + self?.didLastUnlockAttemptFail = false + self?.ensureUI() } // MARK: - Lifecycle + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + deinit { NotificationCenter.default.removeObserver(self) } - + + // MARK: - Observations + private func observeNotifications() { NotificationCenter.default.addObserver( self, @@ -150,56 +107,72 @@ class ScreenLockUI { ) } - public func setupWithRootWindow(rootWindow: UIWindow, using dependencies: Dependencies) { - self.dependencies = dependencies - self.screenBlockingWindow.frame = rootWindow.bounds - } - - public func startObserving() { - self.appIsInactiveOrBackground = (UIApplication.shared.applicationState != .active) - + public func setupWithRootWindow(rootWindow: UIWindow) { + self.window.frame = rootWindow.bounds self.observeNotifications() - - // Hide the screen blocking window until "app is ready" to - // avoid blocking the loading view. + + /// Hide the screen blocking window until "app is ready" to avoid blocking the loading view updateScreenBlockingWindow(state: .none, animated: false) - - // Initialize the screen lock state. - // - // It's not safe to access OWSScreenLock.isScreenLockEnabled - // until the app is ready. - dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in - DispatchQueue.global(qos: .background).async { - self?.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true) - - DispatchQueue.main.async { - self?.ensureUI() - } + + /// Initialize the screen lock state. + /// + /// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready + dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in + self?.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true) + + switch Thread.isMainThread { + case true: self?.ensureUI() + case false: DispatchQueue.main.async { self?.ensureUI() } } } } // MARK: - Functions + private func determineDesiredUIState() -> ScreenLockViewController.State { + if isScreenLockLocked { + if dependencies[singleton: .appContext].isNotInForeground { + Log.verbose(.screenLock, "App not in foreground, desiredUIState is: protection.") + return .protection + } + + Log.verbose(.screenLock, "App in foreground and locked, desiredUIState is: \(isShowingScreenLockUI ? "protection" : "lock").") + return (isShowingScreenLockUI ? .protection : .lock) + } + + if dependencies[singleton: .appContext].isAppForegroundAndActive { + // App is inactive or background. + Log.verbose(.screenLock, "App in foreground and not locked, desiredUIState is: none.") + return .none; + } + + if SessionEnvironment.shared?.isRequestingPermission == true { + Log.verbose(.screenLock, "App requesting permissions and not locked, desiredUIState is: none.") + return .none; + } + + Log.verbose(.screenLock, "desiredUIState is: protection.") + return .protection; + } + private func tryToActivateScreenLockBasedOnCountdown() { - guard dependencies?[singleton: .appReadiness].isAppReady == true else { - // It's not safe to access OWSScreenLock.isScreenLockEnabled - // until the app is ready. - // - // We don't need to try to lock the screen lock; - // It will be initialized by `setupWithRootWindow`. - Log.verbose("tryToActivateScreenLockUponBecomingActive NO 0") + guard dependencies[singleton: .appReadiness].isAppReady else { + /// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready + /// + /// We don't need to try to lock the screen lock; + /// It will be initialized by `setupWithRootWindow` + Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 0") return } - guard dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true else { - // Screen lock is not enabled. - Log.verbose("tryToActivateScreenLockUponBecomingActive NO 1") - return; + guard dependencies[singleton: .storage, key: .isScreenLockEnabled] else { + /// Screen lock is not enabled. + Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 1") + return } guard !isScreenLockLocked else { - // Screen lock is already activated. - Log.verbose("tryToActivateScreenLockUponBecomingActive NO 2") - return; + /// Screen lock is already activated. + Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 2") + return } self.isScreenLockLocked = true @@ -210,49 +183,52 @@ class ScreenLockUI { /// * The blocking window has the correct state. /// * That we show the "iOS auth UI to unlock" if necessary. private func ensureUI() { - guard dependencies?[singleton: .appReadiness].isAppReady == true else { - dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in + guard dependencies[singleton: .appReadiness].isAppReady else { + dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in self?.ensureUI() } return } - let desiredUIState: ScreenLockViewController.State = self.desiredUIState - Log.verbose("ensureUI: \(desiredUIState)") + let desiredUIState: ScreenLockViewController.State = determineDesiredUIState() + Log.verbose(.screenLock, "ensureUI: \(desiredUIState)") - // Show the "iOS auth UI to unlock" if necessary. + /// Show the "iOS auth UI to unlock" if necessary. if desiredUIState == .lock && !didLastUnlockAttemptFail { tryToPresentAuthUIToUnlockScreenLock() } - // Note: We want to regenerate the 'desiredUIState' as if we are about to show the - // 'unlock screen' UI then we shouldn't show the "unlock" button - updateScreenBlockingWindow(state: self.desiredUIState, animated: true) + /// Note: We want to regenerate the `desiredUIState` as if we are about to show the "unlock screen" UI then we + /// shouldn't show the "unlock" button + updateScreenBlockingWindow(state: determineDesiredUIState(), animated: true) } private func tryToPresentAuthUIToUnlockScreenLock() { - guard !isShowingScreenLockUI else { return } // We're already showing the auth UI; abort - guard !appIsInactiveOrBackground else { return } // Never show the auth UI unless active + /// If we're already showing the auth UI; or the app isn't active then don't do anything + guard + !isShowingScreenLockUI, + dependencies[singleton: .appContext].isAppForegroundAndActive + else { return } - Log.info("try to unlock screen lock") + Log.info(.screenLock, "Try to unlock screen lock") isShowingScreenLockUI = true - ScreenLock.shared.tryToUnlockScreenLock( + ScreenLock.tryToUnlockScreenLock( success: { [weak self] in - Log.info("unlock screen lock succeeded.") + Log.info(.screenLock, "Unlock screen lock succeeded") self?.isShowingScreenLockUI = false self?.isScreenLockLocked = false self?.didUnlockJustSucceed = true self?.ensureUI() }, failure: { [weak self] error in - Log.info("unlock screen lock failed.") + Log.info(.screenLock, "Unlock screen lock failed") self?.clearAuthUIWhenActive() self?.didLastUnlockAttemptFail = true self?.showScreenLockFailureAlert(message: "\(error)") }, unexpectedFailure: { [weak self] error in - Log.info("unlock screen lock unexpectedly failed.") + Log.warn(.screenLock, "Unlock screen lock unexpectedly failed") // Local Authentication isn't working properly. // This isn't covered by the docs or the forums but in practice @@ -262,7 +238,7 @@ class ScreenLockUI { } }, cancel: { [weak self] in - Log.info("unlock screen lock cancelled.") + Log.info(.screenLock, "Unlock screen lock cancelled") self?.clearAuthUIWhenActive() self?.didLastUnlockAttemptFail = true @@ -277,7 +253,7 @@ class ScreenLockUI { private func showScreenLockFailureAlert(message: String) { let modal: ConfirmationModal = ConfirmationModal( - targetView: screenBlockingWindow.rootViewController?.view, + targetView: viewController.view, info: ConfirmationModal.Info( title: "authenticateFailed".localized(), body: .text(message), @@ -286,36 +262,7 @@ class ScreenLockUI { afterClosed: { [weak self] in self?.ensureUI() } // After the alert, update the UI ) ) - screenBlockingWindow.rootViewController?.present(modal, animated: true) - } - - /// 'Screen Blocking' window obscures the app screen: - /// - /// * In the app switcher. - /// * During 'Screen Lock' unlock process. - private func createScreenBlockingWindow(rootWindow: UIWindow) { - let window: UIWindow = UIWindow(frame: rootWindow.bounds) - window.isHidden = false - window.windowLevel = .background - window.isOpaque = true - window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) - - let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in - guard self?.appIsInactiveOrBackground == false else { - // This button can be pressed while the app is inactive - // for a brief window while the iOS auth UI is dismissing. - return - } - - Log.info("unlockButtonWasTapped") - - self?.didLastUnlockAttemptFail = false - self?.ensureUI() - } - window.rootViewController = viewController - - self.screenBlockingWindow = window - self.screenBlockingViewController = viewController + viewController.present(modal, animated: true) } /// The "screen blocking" window has three possible states: @@ -327,7 +274,7 @@ class ScreenLockUI { let shouldShowBlockWindow: Bool = (state != .none) OWSWindowManager.shared().isScreenBlockActive = shouldShowBlockWindow - self.screenBlockingViewController.updateUI(state: state, animated: animated) + self.viewController.updateUI(state: state, animated: animated) } // MARK: - Events @@ -335,7 +282,7 @@ class ScreenLockUI { private func clearAuthUIWhenActive() { // For continuity, continue to present blocking screen in "screen lock" mode while // dismissing the "local auth UI". - if self.appIsInactiveOrBackground { + if !dependencies[singleton: .appContext].isAppForegroundAndActive { self.shouldClearAuthUIWhenActive = true } else { @@ -345,42 +292,61 @@ class ScreenLockUI { } @objc private func applicationDidBecomeActive() { - if self.shouldClearAuthUIWhenActive { - self.shouldClearAuthUIWhenActive = false - self.isShowingScreenLockUI = false + if shouldClearAuthUIWhenActive { + shouldClearAuthUIWhenActive = false + isShowingScreenLockUI = false + } + + if !didUnlockJustSucceed { + tryToActivateScreenLockBasedOnCountdown() } - self.appIsInactiveOrBackground = false + didUnlockJustSucceed = false + ensureUI() } + /// When the OS shows the TouchID/FaceID/Pin UI the application will resign active (and we don't want to re-authenticate if we are + /// already locked) + /// + /// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the app switcher @objc private func applicationWillResignActive() { - self.appIsInactiveOrBackground = true + if !isShowingScreenLockUI { + didLastUnlockAttemptFail = false + tryToActivateScreenLockBasedOnCountdown() + } + + didUnlockJustSucceed = false + ensureUI() } @objc private func applicationWillEnterForeground() { - self.appIsInBackground = false + didUnlockJustSucceed = false + tryToActivateScreenLockBasedOnCountdown() + ensureUI() } @objc private func applicationDidEnterBackground() { - self.appIsInBackground = true + didUnlockJustSucceed = false + tryToActivateScreenLockBasedOnCountdown() + ensureUI() } /// Whenever the device date/time is edited by the user, trigger screen lock immediately if enabled. @objc private func clockDidChange() { - Log.info("clock did change") + Log.info(.screenLock, "clock did change") - guard dependencies?[singleton: .appReadiness].isAppReady == true else { + guard dependencies[singleton: .appReadiness].isAppReady == true else { // It's not safe to access OWSScreenLock.isScreenLockEnabled // until the app is ready. // // We don't need to try to lock the screen lock; // It will be initialized by `setupWithRootWindow`. - Log.verbose("clockDidChange 0") + Log.verbose(.screenLock, "clockDidChange 0") return; } DispatchQueue.global(qos: .background).async { [dependencies] in - self.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true) + self.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true) DispatchQueue.main.async { // NOTE: this notifications fires _before_ applicationDidBecomeActive, diff --git a/SessionShareExtension/SAEScreenLockViewController.swift b/SessionShareExtension/SAEScreenLockViewController.swift index 171c05dba..81d970a69 100644 --- a/SessionShareExtension/SAEScreenLockViewController.swift +++ b/SessionShareExtension/SAEScreenLockViewController.swift @@ -107,7 +107,7 @@ final class SAEScreenLockViewController: ScreenLockViewController { isShowingAuthUI = true - ScreenLock.shared.tryToUnlockScreenLock( + ScreenLock.tryToUnlockScreenLock( success: { [weak self] in Log.assertOnMainThread() Log.info("unlock screen lock succeeded.") diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift b/SessionUIKit/Components/ScreenLockViewController.swift similarity index 96% rename from SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift rename to SessionUIKit/Components/ScreenLockViewController.swift index 26f215092..39b6ba851 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift +++ b/SessionUIKit/Components/ScreenLockViewController.swift @@ -1,7 +1,6 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import UIKit -import SessionUIKit open class ScreenLockViewController: UIViewController { public enum State { @@ -36,7 +35,7 @@ open class ScreenLockViewController: UIViewController { public lazy var unlockButton: SessionButton = { let result: SessionButton = SessionButton(style: .bordered, size: .medium) result.translatesAutoresizingMaskIntoConstraints = false - result.setTitle("lockAppUnlock".localized(), for: .normal) + result.setTitle("lockAppUnlock".localizedSNUIKit(), for: .normal) result.addTarget(self, action: #selector(showUnlockUI), for: .touchUpInside) result.isHidden = true diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift b/SessionUtilitiesKit/General/ScreenLock.swift similarity index 91% rename from SignalUtilitiesKit/Screen Lock/ScreenLock.swift rename to SessionUtilitiesKit/General/ScreenLock.swift index 230b37d28..edc9b930c 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift +++ b/SessionUtilitiesKit/General/ScreenLock.swift @@ -1,10 +1,7 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. -import Foundation -import GRDB +import UIKit import LocalAuthentication -import SessionMessagingKit -import SessionUtilitiesKit // MARK: - Log.Category @@ -14,7 +11,17 @@ public extension Log.Category { // MARK: - ScreenLock -public class ScreenLock { +public enum ScreenLock { + public static let screenLockTimeoutDefault = (15 * 60) + public static let screenLockTimeouts = [ + 1 * 60, + 5 * 60, + 15 * 60, + 30 * 60, + 1 * 60 * 60, + 0 + ] + public enum ScreenLockError: Error { case general(description: String) } @@ -26,18 +33,6 @@ public class ScreenLock { case unexpectedFailure(error: String) } - public let screenLockTimeoutDefault = (15 * 60) - public let screenLockTimeouts = [ - 1 * 60, - 5 * 60, - 15 * 60, - 30 * 60, - 1 * 60 * 60, - 0 - ] - - public static let shared: ScreenLock = ScreenLock() - // MARK: - Methods /// This method should only be called: @@ -48,7 +43,7 @@ public class ScreenLock { /// /// * Asynchronously. /// * On the main thread. - public func tryToUnlockScreenLock( + public static func tryToUnlockScreenLock( success: @escaping (() -> Void), failure: @escaping ((Error) -> Void), unexpectedFailure: @escaping ((Error) -> Void), @@ -57,8 +52,7 @@ public class ScreenLock { Log.assertOnMainThread() tryToVerifyLocalAuthentication( - // Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to - // unlock 'screen lock'. + // Description of how and the app uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. localizedReason: "authenticateToOpen" .put(key: "app_name", value: Constants.app_name) .localized() @@ -93,7 +87,7 @@ public class ScreenLock { /// /// * Asynchronously. /// * On the main thread. - private func tryToVerifyLocalAuthentication( + private static func tryToVerifyLocalAuthentication( localizedReason: String, completion completionParam: @escaping ((Outcome) -> Void) ) { @@ -155,7 +149,7 @@ public class ScreenLock { // MARK: - Outcome - private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> Outcome { + private static func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> Outcome { if let error = errorParam { guard let laError = error as? LAError else { return .failure(error: defaultErrorDescription) @@ -222,7 +216,7 @@ public class ScreenLock { // MARK: - Context - private func screenLockContext() -> LAContext { + private static func screenLockContext() -> LAContext { let context = LAContext() // Never recycle biometric auth. From 637723d0262233e5d3084078974d6a06c64f4836 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Mar 2025 10:14:28 +1100 Subject: [PATCH 14/24] Fixed a crash when opening message info in a community --- .../Context Menu/ContextMenuVC+Action.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index b18d036d2..c2e284003 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -250,9 +250,12 @@ extension ContextMenuVC { else { return false } if cellViewModel.threadVariant == .community { - return dependencies[singleton: .openGroupManager].doesOpenGroupSupport( - capability: .reactions, - on: cellViewModel.threadOpenGroupServer + return ( + !forMessageInfoScreen && + dependencies[singleton: .openGroupManager].doesOpenGroupSupport( + capability: .reactions, + on: cellViewModel.threadOpenGroupServer + ) ) } return (threadViewModel.threadIsMessageRequest != true && !forMessageInfoScreen) From a62cce20122873150fdb0eaf467f0b7ca24f9ac9 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 12 Mar 2025 16:56:02 +1100 Subject: [PATCH 15/24] post merging dev --- .../xcshareddata/swiftpm/Package.resolved | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e3a76718a..d35983be3 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "688abd5f50453ed3433c8c6bf57bb60964d1c199902c2bff846544c6691cd18c", + "originHash" : "e3fdf2f44acd1f05dab295d0c9e3faf05f5e4461d512be1d5a77af42e0a25e48", "pins" : [ { "identity" : "cocoalumberjack", @@ -82,6 +82,15 @@ "version" : "5.2.0" } }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", + "version" : "3.0.0" + } + }, { "identity" : "quick", "kind" : "remoteSourceControl", From e1f38f3761d4faa335ad4354448df127b8a638c5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Mar 2025 09:47:20 +1100 Subject: [PATCH 16/24] Wrap the `getValue` logic in a `performMap` to ensure thread safety --- .../Dependency Injection/Dependencies.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 47810b082..906aabb5a 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -386,14 +386,18 @@ private extension Dependencies { /// Convenience method to retrieve the existing dependency instance from memory in a thread-safe way private func getValue(_ key: String, of variant: DependencyStorage.Key.Variant) -> T? { - guard let typedValue: DependencyStorage.Value = storage.instances[variant.key(key)] else { return nil } - guard let result: T = typedValue.value(as: T.self) else { - /// If there is a value stored for the key, but it's not the right type then something has gone wrong, and we should log - Log.critical("Failed to convert stored dependency '\(variant.key(key))' to expected type: \(T.self)") - return nil + return _storage.performMap { storage in + guard let typedValue: DependencyStorage.Value = storage.instances[variant.key(key)] else { + return nil + } + guard let result: T = typedValue.value(as: T.self) else { + /// If there is a value stored for the key, but it's not the right type then something has gone wrong, and we should log + Log.critical("Failed to convert stored dependency '\(variant.key(key))' to expected type: \(T.self)") + return nil + } + + return result } - - return result } /// Convenience method to store a dependency instance in memory in a thread-safe way From e29758e401e3e7bc644963905435f98ae204a00b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Mar 2025 16:45:45 +1100 Subject: [PATCH 17/24] Updated GRDB and refactored internal Storage operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated to GRDB 7.3.0 (from 6.29.3) • Updated the ConfigMessageReceiveJob and MessageReceiveJob to use the `writeAsync` function (instead of the blocking `write` function) so that they aren't subject to the `Storage.transactionDeadlockTimeoutSeconds` • Refactored the `Storage.performOperation` and `Storage.performPublisherOperation` to rely on the new cancellable async/await `Task` logic that GRDB 7 supports (as apparently the other async methods don't support cancellation...) • Cleaned up some "Sendable" related warnings • Minor tweaks to `Log.assertOnMainThread` to make it a little more readable --- Session.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Session/Conversations/ConversationVC.swift | 15 +- Session/Home/HomeVC.swift | 5 +- .../DocumentTitleViewController.swift | 6 +- .../MediaTileViewController.swift | 6 +- .../Shared/Types/PagedObservationSource.swift | 5 +- .../Jobs/ConfigMessageReceiveJob.swift | 31 +- .../Jobs/MessageReceiveJob.swift | 119 +++---- SessionUtilitiesKit/Database/Storage.swift | 300 ++++++++---------- .../Database/StorageError.swift | 5 + SessionUtilitiesKit/General/Logging.swift | 29 +- 12 files changed, 278 insertions(+), 249 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d36be785d..ec830e84f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -10262,7 +10262,7 @@ repositoryURL = "https://github.com/session-foundation/session-grdb-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 106.29.3; + minimumVersion = 107.3.0; }; }; FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e3a76718a..3d5e300d3 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/session-grdb-swift.git", "state" : { - "revision" : "b3643613f1e0f392fa41072ee499da93b4c06b67", - "version" : "106.29.3" + "revision" : "c69f8bf8a7ede8727c20f7c36eeffd3f55598487", + "version" : "107.3.0" } }, { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 829b67f6b..2f303b22f 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -422,7 +422,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa using: dependencies ) - dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) + /// Dispatch adding the database observation to a background thread + DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in + dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver) + } super.init(nibName: nil, bundle: nil) } @@ -704,14 +707,18 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Stop observing changes self?.stopObservingChanges() - dependencies[singleton: .storage].removeObserver(self?.viewModel.pagedDataObserver) + DispatchQueue.global(qos: .userInitiated).async { + dependencies[singleton: .storage].removeObserver(self?.viewModel.pagedDataObserver) + } // Swap the observing to the updated thread let newestVisibleMessageId: Int64? = self?.fullyVisibleCellViewModels()?.last?.id self?.viewModel.swapToThread(updatedThreadId: unblindedId, focussedMessageId: newestVisibleMessageId) - // Start observing changes again - dependencies[singleton: .storage].addObserver(self?.viewModel.pagedDataObserver) + /// Start observing changes again (on a background thread) + DispatchQueue.global(qos: .userInitiated).async { + dependencies[singleton: .storage].addObserver(self?.viewModel.pagedDataObserver) + } self?.startObservingChanges() return } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 5419046d5..f6646f519 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -31,7 +31,10 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi init(using dependencies: Dependencies) { self.viewModel = HomeViewModel(using: dependencies) - dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) + /// Dispatch adding the database observation to a background thread + DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in + dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver) + } super.init(nibName: nil, bundle: nil) } diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift index 9faa3d9df..450f26619 100644 --- a/Session/Media Viewing & Editing/DocumentTitleViewController.swift +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -34,7 +34,11 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) { self.dependencies = dependencies self.viewModel = viewModel - dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) + + /// Dispatch adding the database observation to a background thread + DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in + dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver) + } super.init(nibName: nil, bundle: nil) } diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index 384ae688d..497356645 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -44,7 +44,11 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) { self.dependencies = dependencies self.viewModel = viewModel - dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) + + /// Dispatch adding the database observation to a background thread + DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in + dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver) + } super.init(nibName: nil, bundle: nil) } diff --git a/Session/Shared/Types/PagedObservationSource.swift b/Session/Shared/Types/PagedObservationSource.swift index 33c0da78e..9337cc2c8 100644 --- a/Session/Shared/Types/PagedObservationSource.swift +++ b/Session/Shared/Types/PagedObservationSource.swift @@ -17,7 +17,10 @@ protocol PagedObservationSource { extension PagedObservationSource { public func didInit(using dependencies: Dependencies) { - dependencies[singleton: .storage].addObserver(pagedDataObserver) + /// Dispatch adding the database observation to a background thread + DispatchQueue.global(qos: .userInitiated).async { [weak pagedDataObserver] in + dependencies[singleton: .storage].addObserver(pagedDataObserver) + } } } diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index ce94a9409..db74dfa73 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -55,11 +55,8 @@ public enum ConfigMessageReceiveJob: JobExecutor { return failure(job, JobRunnerError.missingRequiredDetails, true) } - var lastError: Error? - - dependencies[singleton: .storage].write { db in - // Send any SharedConfigMessages to the LibSession to handle it - do { + dependencies[singleton: .storage].writeAsync( + updates: { db in try dependencies.mutate(cache: .libSession) { cache in try cache.handleConfigMessages( db, @@ -67,19 +64,19 @@ public enum ConfigMessageReceiveJob: JobExecutor { messages: details.messages ) } - } - catch { lastError = error } - } - - // Handle the result - switch lastError { - case .some(let error): - Log.error(.cat, "Couldn't receive config message due to error: \(error)") - removeDependencyOnMessageReceiveJobs() - failure(job, error, true) + }, + completion: { result in + // Handle the result + switch result { + case .failure(let error): + Log.error(.cat, "Couldn't receive config message due to error: \(error)") + removeDependencyOnMessageReceiveJobs() + failure(job, error, true) - case .none: success(job, false) - } + case .success: success(job, false) + } + } + ) } } diff --git a/SessionMessagingKit/Jobs/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/MessageReceiveJob.swift index b4de9ba33..53e7f2f0c 100644 --- a/SessionMessagingKit/Jobs/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/MessageReceiveJob.swift @@ -51,65 +51,74 @@ public enum MessageReceiveJob: JobExecutor { } } - dependencies[singleton: .storage].write { db in - for (messageInfo, protoContent) in messageData { - do { - try MessageReceiver.handle( - db, - threadId: threadId, - threadVariant: messageInfo.threadVariant, - message: messageInfo.message, - serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, - associatedWithProto: protoContent, - using: dependencies - ) + dependencies[singleton: .storage].writeAsync( + updates: { db -> Error? in + for (messageInfo, protoContent) in messageData { + do { + try MessageReceiver.handle( + db, + threadId: threadId, + threadVariant: messageInfo.threadVariant, + message: messageInfo.message, + serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, + associatedWithProto: protoContent, + using: dependencies + ) + } + catch { + // If the current message is a permanent failure then override it with the + // new error (we want to retry if there is a single non-permanent error) + switch error { + // Ignore duplicate and self-send errors (these will usually be caught during + // parsing but sometimes can get past and conflict at database insertion - eg. + // for open group messages) we also don't bother logging as it results in + // excessive logging which isn't useful) + case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, + DatabaseError.SQLITE_CONSTRAINT, // Sometimes thrown for UNIQUE + MessageReceiverError.duplicateMessage, + MessageReceiverError.duplicateControlMessage, + MessageReceiverError.selfSend: + break + + case let receiverError as MessageReceiverError where !receiverError.isRetryable: + Log.error(.cat, "Permanently failed message due to error: \(error)") + continue + + default: + Log.error(.cat, "Couldn't receive message due to error: \(error)") + lastError = error + + // We failed to process this message but it is a retryable error + // so add it to the list to re-process + remainingMessagesToProcess.append(messageInfo) + } + } } - catch { - // If the current message is a permanent failure then override it with the - // new error (we want to retry if there is a single non-permanent error) - switch error { - // Ignore duplicate and self-send errors (these will usually be caught during - // parsing but sometimes can get past and conflict at database insertion - eg. - // for open group messages) we also don't bother logging as it results in - // excessive logging which isn't useful) - case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, - DatabaseError.SQLITE_CONSTRAINT, // Sometimes thrown for UNIQUE - MessageReceiverError.duplicateMessage, - MessageReceiverError.duplicateControlMessage, - MessageReceiverError.selfSend: - break - - case let receiverError as MessageReceiverError where !receiverError.isRetryable: - Log.error(.cat, "Permanently failed message due to error: \(error)") - continue + + // If any messages failed to process then we want to update the job to only include + // those failed messages + guard !remainingMessagesToProcess.isEmpty else { return nil } + + updatedJob = try job + .with(details: Details(messages: remainingMessagesToProcess)) + .defaulting(to: job) + .upserted(db) + + return lastError + }, + completion: { result in + // TODO: [REFACTOR] Need to test this!!! + // Handle the result + switch result { + case .failure(let error): failure(updatedJob, error, false) + case .success(.some(let error as MessageReceiverError)) where !error.isRetryable: + failure(updatedJob, error, true) - default: - Log.error(.cat, "Couldn't receive message due to error: \(error)") - lastError = error - - // We failed to process this message but it is a retryable error - // so add it to the list to re-process - remainingMessagesToProcess.append(messageInfo) - } + case .success(.some(let error)): failure(updatedJob, error, false) + case .success: success(updatedJob, false) } } - - // If any messages failed to process then we want to update the job to only include - // those failed messages - guard !remainingMessagesToProcess.isEmpty else { return } - - updatedJob = try job - .with(details: Details(messages: remainingMessagesToProcess)) - .defaulting(to: job) - .upserted(db) - } - - // Handle the result - switch lastError { - case let error as MessageReceiverError where !error.isRetryable: failure(updatedJob, error, true) - case .some(let error): failure(updatedJob, error, false) - case .none: success(updatedJob, false) - } + ) } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 42b9f2a71..b81fa1edf 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -53,6 +53,9 @@ open class Storage { private static let writeTransactionStartTimeout: TimeInterval = 5 /// If a transaction takes longer than this duration then we should fail the transaction rather than keep hanging + /// + /// **Note:** This timeout only applies to synchronous operations (the assumption being that if we know an operation is going to + /// take a long time then we should probably be handling it asynchronously rather than a synchronous way) private static let transactionDeadlockTimeoutSeconds: Int = 5 private static var sharedDatabaseDirectoryPath: String { "\(SessionFileManager.nonInjectedAppSharedDataDirectoryPath)/database" } @@ -142,11 +145,6 @@ open class Storage { var config = Configuration() config.label = Storage.queuePrefix config.maximumReaderCount = 10 /// Increase the max read connection limit - Default is 5 - - /// It seems we should do this per https://github.com/groue/GRDB.swift/pull/1485 but with this change - /// we then need to define how long a write transaction should wait for before timing out (read transactions always run - /// in`DEFERRED` mode so won't be affected by these settings) - config.defaultTransactionKind = .immediate config.busyMode = .timeout(Storage.writeTransactionStartTimeout) /// Load in the SQLCipher keys @@ -551,6 +549,13 @@ open class Storage { case valid(DatabaseWriter) case invalid(Error) + var forcedError: Error { + switch self { + case .valid: return StorageError.validStorageIncorrectlyHandledAsError + case .invalid(let error): return error + } + } + init(_ storage: Storage?) { switch (storage?.isSuspended, storage?.isValid, storage?.dbWriter) { case (true, _, _): self = .invalid(StorageError.databaseSuspended) @@ -559,38 +564,46 @@ open class Storage { } } - static func logIfNeeded(_ error: Error, isWrite: Bool) { + fileprivate static func logIfNeeded(_ error: Error, info: Storage.CallInfo) { + let action: String = (info.isWrite ? "write" : "read") + switch error { case DatabaseError.SQLITE_ABORT, DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ERROR: let message: String = ((error as? DatabaseError)?.message ?? "Unknown") - Log.error(.storage, "Database \(isWrite ? "write" : "read") failed due to error: \(message)") + Log.error(.storage, "Database \(action) failed due to error: \(message) - [ \(info.callInfo) ]") case StorageError.databaseInvalid: - Log.error(.storage, "Database \(isWrite ? "write" : "read") failed as the database is invalid.") + Log.error(.storage, "Database \(action) failed as the database is invalid - [ \(info.callInfo) ]") case StorageError.databaseSuspended: - Log.error(.storage, "Database \(isWrite ? "write" : "read") failed as the database is suspended.") + Log.error(.storage, "Database \(action) failed as the database is suspended - [ \(info.callInfo) ]") case StorageError.transactionDeadlockTimeout: - Log.critical("[Storage] Database \(isWrite ? "write" : "read") failed due to a potential synchronous query deadlock timeout.") + Log.critical(.storage, "Database \(action) failed due to a potential synchronous query deadlock timeout - [ \(info.callInfo) ]") default: break } } - static func logIfNeeded(_ error: Error, isWrite: Bool) -> T? { - logIfNeeded(error, isWrite: isWrite) + fileprivate static func logIfNeeded(_ error: Error, info: Storage.CallInfo) -> T? { + logIfNeeded(error, info: info) return nil } - static func logIfNeeded(_ error: Error, isWrite: Bool) -> AnyPublisher { - logIfNeeded(error, isWrite: isWrite) + fileprivate static func logIfNeeded(_ error: Error, info: Storage.CallInfo) -> AnyPublisher { + logIfNeeded(error, info: info) return Fail(error: error).eraseToAnyPublisher() } } // MARK: - Operations + /// Internal type to wrap the database operation `Task` so it can be cancelled when used with `Combine` (since GRDB doesn't + /// actually handle publishers publishers) + final class TaskHolder { + var task: Task<(), Never>? + } + private static func track( _ db: Database, _ info: CallInfo, @@ -634,122 +647,108 @@ open class Storage { _ dependencies: Dependencies, _ operation: @escaping (Database) throws -> T, _ asyncCompletion: ((Result) -> Void)? = nil - ) -> Result { - // A serial queue for synchronizing completion updates. - let syncQueue = DispatchQueue(label: "com.session.performOperation.syncQueue") + ) -> (result: Result, task: Task<(), Never>?) { + /// Ensure we are in a valid state + let storageState: StorageState = StorageState(info.storage) + + guard case .valid(let dbWriter) = storageState else { + if info.isAsync { asyncCompletion?(.failure(storageState.forcedError)) } + return (.failure(storageState.forcedError), nil) + } - weak var queryDb: Database? - var didTimeout: Bool = false + /// Setup required variables + let syncQueue = DispatchQueue(label: "com.session.performOperation.syncQueue") + let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) var operationResult: Result? - let semaphore: DispatchSemaphore? = (info.isAsync ? nil : DispatchSemaphore(value: 0)) let logErrorIfNeeded: (Result) -> Result = { result in switch result { case .success: break - case .failure(let error): StorageState.logIfNeeded(error, isWrite: info.isWrite) + case .failure(let error): StorageState.logIfNeeded(error, info: info) } return result } + /// Convenience function to remove duplication func completeOperation(with result: Result) { syncQueue.sync { - guard !didTimeout && operationResult == nil else { return } + guard operationResult == nil else { return } operationResult = result - semaphore?.signal() + semaphore.signal() - // For async operations, log and invoke the completion closure. + /// For async operations, log and invoke the completion closure. if info.isAsync { asyncCompletion?(logErrorIfNeeded(result)) } } } - /// Perform the actual operation - switch (StorageState(info.storage), info.isWrite) { - case (.invalid(let error), _): completeOperation(with: .failure(error)) - case (.valid(let dbWriter), true): - dbWriter.asyncWrite( - { db in - syncQueue.sync { queryDb = db } - defer { syncQueue.sync { queryDb = nil } } - + let task: Task<(), Never> = Task { + return await withThrowingTaskGroup(of: T.self) { group in + /// Add the task to perform the actual database operation + group.addTask { + let trackedOperation: @Sendable (Database) throws -> T = { db in if dependencies[feature: .forceSlowDatabaseQueries] { Thread.sleep(forTimeInterval: 1) } return try Storage.track(db, info, operation) - }, - completion: { _, dbResult in completeOperation(with: dbResult) } - ) + } + + return (info.isWrite ? + try await dbWriter.write(trackedOperation) : + try await dbWriter.read(trackedOperation) + ) + } - case (.valid(let dbWriter), false): - dbWriter.asyncRead { dbResult in - do { - switch dbResult { - case .failure(let error): throw error - case .success(let db): - syncQueue.sync { queryDb = db } - defer { syncQueue.sync { queryDb = nil } } - - if dependencies[feature: .forceSlowDatabaseQueries] { - Thread.sleep(forTimeInterval: 1) - } - - completeOperation(with: .success(try Storage.track(db, info, operation))) + /// If this is a syncronous task then we want to the operation to timeout to ensure we don't unintentionally + /// create a deadlock + if !info.isAsync { + group.addTask { + let timeoutNanoseconds: UInt64 = UInt64(Storage.transactionDeadlockTimeoutSeconds * 1_000_000_000) + + /// If the debugger is attached then we want to have a lot of shorter sleep iterations as the clock doesn't get + /// paused when stopped on a breakpoint (and we don't want to end up having a bunch of false positive + /// database timeouts while debugging code) + /// + /// **Note:** `isDebuggerAttached` will always return `false` in production builds + if isDebuggerAttached() { + let numIterations: UInt64 = 50 + + for _ in (0..? = await group.nextResult() + group.cancelAll() + completeOperation(with: result ?? .failure(StorageError.invalidQueryResult)) } - - return logErrorIfNeeded(operationResult ?? .failure(StorageError.transactionDeadlockTimeout)) } /// For the `async` operation the returned value should be ignored so just return the `invalidQueryResult` error - return .failure(StorageError.invalidQueryResult) + guard !info.isAsync else { + return (.failure(StorageError.invalidQueryResult), task) + } + + /// Block until we have a result + semaphore.wait() + return (logErrorIfNeeded(operationResult ?? .failure(StorageError.transactionDeadlockTimeout)), task) } private func performPublisherOperation( @@ -759,59 +758,33 @@ open class Storage { isWrite: Bool, _ operation: @escaping (Database) throws -> T ) -> AnyPublisher { + let info: CallInfo = CallInfo(self, fileName, functionName, lineNumber, (isWrite ? .asyncWrite : .asyncRead)) + switch StorageState(self) { - case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: false) + case .invalid(let error): return StorageState.logIfNeeded(error, info: info) case .valid: /// **Note:** GRDB does have `readPublisher`/`writePublisher` functions but it appears to asynchronously /// trigger both the `output` and `complete` closures at the same time which causes a lot of unexpected /// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code /// for more information see https://github.com/groue/GRDB.swift/issues/1334) /// - /// Instead of this we are just using `Deferred { Future {} }` which is executed on the specified scheduled - /// which behaves in a much more expected way than the GRDB `readPublisher`/`writePublisher` does - let info: CallInfo = CallInfo(self, fileName, functionName, lineNumber, .syncWrite) + /// Instead of this we are just using `Deferred { Future {} }` which is executed on the specified scheduler + /// (which behaves in a much more expected way than the GRDB `readPublisher`/`writePublisher` does) + /// and hooking that into our `performOperation` function which uses the GRDB async/await functions that support + /// cancellation (as we want to support cancellation as well) + let holder: TaskHolder = TaskHolder() + return Deferred { [dependencies] in Future { resolver in - resolver(Storage.performOperation(info, dependencies, operation)) + let (_, task) = Storage.performOperation(info, dependencies, operation) { result in + resolver(result) + } + holder.task = task } - }.eraseToAnyPublisher() - } - } - - private static func debugWait(semaphore: DispatchSemaphore, info: CallInfo) -> DispatchTimeoutResult { - let pollQueue: DispatchQueue = DispatchQueue(label: "com.session.debugWaitTimer.\(UUID().uuidString)") - let standardPollInterval: DispatchTimeInterval = .milliseconds(100) - var iterations: Int = 0 - let maxIterations: Int = ((Storage.transactionDeadlockTimeoutSeconds * 1000) / standardPollInterval.milliseconds) - let pollCompletionSemaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - - /// Stagger the size of the `pollIntervals` to avoid holding up the thread in case the query resolves very quickly (this - /// means the timeout will occur ~500ms early but helps prevent false main thread lag appearing when debugging that wouldn't - /// affect production) - let pollIntervals: [DispatchTimeInterval] = [ - .milliseconds(5), .milliseconds(5), .milliseconds(10), .milliseconds(10), .milliseconds(10), - standardPollInterval - ] - - func pollSemaphore() { - iterations += 1 - - guard iterations < maxIterations && semaphore.wait(timeout: .now()) != .success else { - pollCompletionSemaphore.signal() - return - } - - let nextInterval: DispatchTimeInterval = pollIntervals[min(iterations, pollIntervals.count - 1)] - pollQueue.asyncAfter(deadline: .now() + nextInterval) { - pollSemaphore() - } + } + .handleEvents(receiveCancel: { holder.task?.cancel() }) + .eraseToAnyPublisher() } - - /// Poll the semaphore in a background queue - pollQueue.asyncAfter(deadline: .now() + pollIntervals[0]) { pollSemaphore() } - pollCompletionSemaphore.wait() // Wait indefinitely for the timer semaphore - - return (iterations >= 50 ? .timedOut : .success) } // MARK: - Functions @@ -823,8 +796,8 @@ open class Storage { updates: @escaping (Database) throws -> T? ) -> T? { switch Storage.performOperation(CallInfo(self, file, funcN, line, .syncWrite), dependencies, updates) { - case .failure: return nil - case .success(let result): return result + case (.failure, _): return nil + case (.success(let result), _): return result } } @@ -854,8 +827,8 @@ open class Storage { _ value: @escaping (Database) throws -> T? ) -> T? { switch Storage.performOperation(CallInfo(self, file, funcN, line, .syncRead), dependencies, value) { - case .failure: return nil - case .success(let result): return result + case (.failure, _): return nil + case (.success(let result), _): return result } } @@ -896,30 +869,32 @@ open class Storage { ) } + /// Add a database observation + /// + /// **Note:** This function **MUST NOT** be called from the main thread public func addObserver(_ observer: TransactionObserver?) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } guard let observer: TransactionObserver = observer else { return } - // Note: This actually triggers a write to the database so can be blocked by other - // writes, since it's usually called on the main thread when creating a view controller - // this can result in the UI hanging - to avoid this we dispatch (and hope there isn't - // negative impact) - DispatchQueue.global(qos: .default).async { - dbWriter.add(transactionObserver: observer) - } + /// This actually triggers a write to the database so can be blocked by other writes so shouldn't be called on the main thread, + /// we don't dispatch to an async thread in here because `TransactionObserver` isn't `Sendable` so instead just require + /// that it isn't called on the main thread + Log.assertNotOnMainThread() + dbWriter.add(transactionObserver: observer) } + /// Remove a database observation + /// + /// **Note:** This function **MUST NOT** be called from the main thread public func removeObserver(_ observer: TransactionObserver?) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } guard let observer: TransactionObserver = observer else { return } - // Note: This actually triggers a write to the database so can be blocked by other - // writes, since it's usually called on the main thread when creating a view controller - // this can result in the UI hanging - to avoid this we dispatch (and hope there isn't - // negative impact) - DispatchQueue.global(qos: .default).async { - dbWriter.remove(transactionObserver: observer) - } + /// This actually triggers a write to the database so can be blocked by other writes so shouldn't be called on the main thread, + /// we don't dispatch to an async thread in here because `TransactionObserver` isn't `Sendable` so instead just require + /// that it isn't called on the main thread + Log.assertNotOnMainThread() + dbWriter.remove(transactionObserver: observer) } } @@ -1028,7 +1003,7 @@ private extension Storage { result?.timer = nil let action: String = (info.isWrite ? "write" : "read") - Log.warn("[Storage] Slow \(action) taking longer than \(Storage.slowTransactionThreshold, format: ".2", omitZeroDecimal: true)s - [ \(info.callInfo) ]") + Log.warn(.storage, "Slow \(action) taking longer than \(Storage.slowTransactionThreshold, format: ".2", omitZeroDecimal: true)s - [ \(info.callInfo) ]") result?.wasSlowTransaction = true } result.timer?.resume() @@ -1044,7 +1019,7 @@ private extension Storage { let end: CFTimeInterval = CACurrentMediaTime() let action: String = (info.isWrite ? "write" : "read") - Log.warn("[Storage] Slow \(action) completed after \(end - start, format: ".2", omitZeroDecimal: true)s - [ \(info.callInfo) ]") + Log.warn(.storage, "Slow \(action) completed after \(end - start, format: ".2", omitZeroDecimal: true)s - [ \(info.callInfo) ]") } } } @@ -1159,13 +1134,18 @@ public extension Storage { } } -#if DEBUG +/// Function to determine if the debugger is attached +/// +/// **Note:** Only contains logic when `DEBUG` is defined, otherwise it always returns false func isDebuggerAttached() -> Bool { +#if DEBUG var info = kinfo_proc() var size = MemoryLayout.stride var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] let sysctlResult = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) guard sysctlResult == 0 else { return false } return (info.kp_proc.p_flag & P_TRACED) != 0 -} +#else + return false #endif +} diff --git a/SessionUtilitiesKit/Database/StorageError.swift b/SessionUtilitiesKit/Database/StorageError.swift index 31981be8d..058456750 100644 --- a/SessionUtilitiesKit/Database/StorageError.swift +++ b/SessionUtilitiesKit/Database/StorageError.swift @@ -14,7 +14,12 @@ public enum StorageError: Error { case keySpecInaccessible case decodingFailed case invalidQueryResult + + /// This error is thrown when a synchronous operation takes longer than `Storage.transactionDeadlockTimeoutSeconds`, + /// the assumption being that if we know an operation is going to take a long time then we should probably be handling it asynchronously + /// rather than a synchronous way case transactionDeadlockTimeout + case validStorageIncorrectlyHandledAsError case failedToSave case objectNotFound diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 92a552064..110ade7bb 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -335,12 +335,29 @@ public enum Log { function: StaticString = #function, line: UInt = #line ) { - guard !Thread.isMainThread else { return } - - let filename: String = URL(fileURLWithPath: "\(file)").lastPathComponent - let formattedMessage: String = "[\(filename):\(line) \(function)] Must be on main thread." - custom(.critical, [], formattedMessage, file: file, function: function, line: line) - assertionFailure(formattedMessage) + switch Thread.isMainThread { + case true: return + case false: + let filename: String = URL(fileURLWithPath: "\(file)").lastPathComponent + let formattedMessage: String = "[\(filename):\(line) \(function)] Must be on main thread." + custom(.critical, [], formattedMessage, file: file, function: function, line: line) + assertionFailure(formattedMessage) + } + } + + public static func assertNotOnMainThread( + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line + ) { + switch Thread.isMainThread { + case false: return + case true: + let filename: String = URL(fileURLWithPath: "\(file)").lastPathComponent + let formattedMessage: String = "[\(filename):\(line) \(function)] Must NOT be on main thread." + custom(.critical, [], formattedMessage, file: file, function: function, line: line) + assertionFailure(formattedMessage) + } } public static func custom( From e11375fdebf6fc36af190577336b1622fb96e824 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Mar 2025 09:30:29 +1100 Subject: [PATCH 18/24] Fixed the broken unit tests --- SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 4 ++-- SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift | 1 + SessionSnodeKit/Types/PreparedRequest.swift | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 7afb36218..e274a5941 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -321,7 +321,7 @@ public enum OpenGroupAPI { using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) - .map { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in + .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRoomResponse: Any? = response.data .first(where: { key, _ in @@ -372,7 +372,7 @@ public enum OpenGroupAPI { using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) - .map { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in + .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data .first(where: { key, _ in diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 595dbcde2..87f362c45 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -17,6 +17,7 @@ class OpenGroupAPISpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) + dependencies.forceSynchronous = true } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionSnodeKit/Types/PreparedRequest.swift index c6cf77171..224e8340f 100644 --- a/SessionSnodeKit/Types/PreparedRequest.swift +++ b/SessionSnodeKit/Types/PreparedRequest.swift @@ -488,7 +488,7 @@ public extension Network.PreparedRequest { /// Due to the way prepared requests work we need to cast between different types and as a result can't avoid potentially /// throwing when mapping so the `map` function just calls through to the `tryMap` function, but we have both to make /// the interface more consistent for dev use - func map(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest { + func map(transform: @escaping (ResponseInfoType, R) -> O) -> Network.PreparedRequest { return tryMap(transform: transform) } From 846aa695c26cb692d987477d4aeff75408d9bbf0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Mar 2025 10:14:40 +1100 Subject: [PATCH 19/24] Updated searching to use a publisher and cancel it (instead of db interrupt) --- .../Conversations/ConversationSearch.swift | 49 ++++----- Session/Conversations/ConversationVC.swift | 4 +- .../GlobalSearchViewController.swift | 100 ++++++++---------- .../Utilities/Database+Utilities.swift | 6 -- 4 files changed, 70 insertions(+), 89 deletions(-) diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index 1e2e59c11..ca6515ca8 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import SignalUtilitiesKit import SessionUIKit @@ -85,31 +86,25 @@ extension ConversationSearchController: UISearchResultsUpdating { } let threadId: String = self.threadId - - DispatchQueue.global(qos: .default).async { [weak self] in - let results: [Interaction.TimestampInfo]? = dependencies[singleton: .storage].read { db -> [Interaction.TimestampInfo] in - self?.resultsBar.willStartSearching(readConnection: db) - - return try Interaction.idsForTermWithin( + let searchCancellable: AnyCancellable = dependencies[singleton: .storage] + .readPublisher { db -> [Interaction.TimestampInfo] in + try Interaction.idsForTermWithin( threadId: threadId, pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) ) .fetchAll(db) } - - // If we didn't get results back then we most likely interrupted the query so - // should ignore the results (if there are no results we would succeed and get - // an empty array back) - guard let results: [Interaction.TimestampInfo] = results else { return } - - DispatchQueue.main.async { - guard let strongSelf = self else { return } - - self?.resultsBar.stopLoading() - self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds()) - self?.delegate?.conversationSearchController(strongSelf, didUpdateSearchResults: results, searchText: searchText) - } - } + .subscribe(on: DispatchQueue.global(qos: .default), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] results in + self?.resultsBar.stopLoading() + self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds()) + self?.delegate?.conversationSearchController(self, didUpdateSearchResults: results, searchText: searchText) + } + ) + self.resultsBar.willStartSearching(searchCancellable: searchCancellable) } } @@ -138,7 +133,7 @@ protocol SearchResultsBarDelegate: AnyObject { public final class SearchResultsBar: UIView { @ThreadSafe private var hasResults: Bool = false @ThreadSafeObject private var results: [Interaction.TimestampInfo] = [] - @ThreadSafeObject private var readConnection: Database? = nil + @ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil var currentIndex: Int? weak var resultsBarDelegate: SearchResultsBarDelegate? @@ -275,8 +270,7 @@ public final class SearchResultsBar: UIView { // MARK: - Content - /// This method will be called within a DB read block - func willStartSearching(readConnection: Database) { + func willStartSearching(searchCancellable: AnyCancellable) { let hasNoExistingResults: Bool = hasResults DispatchQueue.main.async { [weak self] in @@ -287,8 +281,8 @@ public final class SearchResultsBar: UIView { self?.startLoading() } - self.readConnection?.interrupt() - self._readConnection.set(to: readConnection) + currentSearchCancellable?.cancel() + _currentSearchCancellable.set(to: searchCancellable) } func updateResults(results: [Interaction.TimestampInfo]?, visibleItemIds: [Int64]?) { @@ -311,7 +305,6 @@ public final class SearchResultsBar: UIView { return 0 }() - self._readConnection.set(to: nil) self._results.performUpdate { _ in (results ?? []) } self.hasResults = (results != nil) @@ -366,6 +359,6 @@ public final class SearchResultsBar: UIView { public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate { func conversationSearchControllerDependencies() -> Dependencies func currentVisibleIds() -> [Int64] - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo: Interaction.TimestampInfo) + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo: Interaction.TimestampInfo) } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 2f303b22f..54c77a844 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1968,12 +1968,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa func conversationSearchControllerDependencies() -> Dependencies { return viewModel.dependencies } func currentVisibleIds() -> [Int64] { return (fullyVisibleCellViewModels() ?? []).map { $0.id } } - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) { + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) { viewModel.lastSearchedText = searchText tableView.reloadRows(at: tableView.indexPathsForVisibleRows ?? [], with: UITableView.RowAnimation.none) } - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) { + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) { scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight) } diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7b8fe65fa..91163761c 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import DifferenceKit import SessionUIKit @@ -106,7 +107,7 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI ) }() - @ThreadSafeObject private var readConnection: Database? = nil + @ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil private lazy var searchResultSet: SearchResultData = defaultSearchResults private var termForCurrentSearchResultSet: String = "" private var lastSearchText: String? @@ -256,61 +257,54 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI guard force || lastSearchText != searchText else { return } lastSearchText = searchText - - DispatchQueue.global(qos: .default).async { [weak self, dependencies] in - self?.readConnection?.interrupt() - - let result: Result<[SectionModel], Error>? = dependencies[singleton: .storage].read { db -> Result<[SectionModel], Error> in - self?._readConnection.set(to: db) + currentSearchCancellable?.cancel() + + _currentSearchCancellable.set(to: dependencies[singleton: .storage] + .readPublisher { [dependencies] db -> [SectionModel] in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel + .contactsAndGroupsQuery( + userSessionId: userSessionId, + pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText), + searchTerm: searchText + ) + .fetchAll(db) + Thread.sleep(forTimeInterval: 1) + let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel + .messagesQuery( + userSessionId: userSessionId, + pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) + ) + .fetchAll(db) - do { - let userSessionId: SessionId = dependencies[cache: .general].sessionId - let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel - .contactsAndGroupsQuery( - userSessionId: userSessionId, - pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText), - searchTerm: searchText - ) - .fetchAll(db) - let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel - .messagesQuery( - userSessionId: userSessionId, - pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) - ) - .fetchAll(db) - - return .success([ - ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults), - ArraySection(model: .messages, elements: messageResults) - ]) - } - catch { - // Don't log the 'interrupt' error as that's just the user typing too fast - if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { - SNLog("[GlobalSearch] Failed to find results due to error: \(error)") - } - - return .failure(error) - } + return [ + ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults), + ArraySection(model: .messages, elements: messageResults) + ] } - self?._readConnection.set(to: nil) - - DispatchQueue.main.async { - switch result { - case .success(let sections): - self?.termForCurrentSearchResultSet = searchText - self?.searchResultSet = SearchResultData( - state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none, - data: sections - ) - self?.isLoading = false - self?.tableView.reloadData() - self?.refreshTimer = nil - - default: break + .subscribe(on: DispatchQueue.global(qos: .default), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { result in + /// Cancelling the search results in `receiveCompletion` not getting called so we can just log any + /// errors we get without needing to filter out "cancelled search" cases + switch result { + case .finished: break + case .failure(let error): + SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + } + }, + receiveValue: { [weak self] sections in + self?.termForCurrentSearchResultSet = searchText + self?.searchResultSet = SearchResultData( + state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none, + data: sections + ) + self?.isLoading = false + self?.tableView.reloadData() + self?.refreshTimer = nil } - } - } + )) } @objc func cancel() { diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift index 1c9a606e1..add7923f4 100644 --- a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift @@ -21,12 +21,6 @@ public extension Database { return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName) } - func interrupt() { - guard sqliteConnection != nil else { return } - - sqlite3_interrupt(sqliteConnection) - } - /// This is a custom implementation of the `afterNextTransaction` method which executes the closures within their own /// transactions to allow for nesting of 'afterNextTransaction' actions /// From 69c60b0090aba04b5e9f6a8f977e31fd86d14b91 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Mar 2025 10:42:05 +1100 Subject: [PATCH 20/24] Track current db tasks and cancel when suspending --- SessionUtilitiesKit/Database/Storage.swift | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index b81fa1edf..8a2da7c24 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -91,6 +91,9 @@ open class Storage { /// This property gets set the first time we successfully write to the database public private(set) var hasSuccessfullyWritten: Bool = false + /// This property keeps track of all current database tasks and can be used when suspending the database to explicitly + /// cancel any currently running tasks + @ThreadSafeObject private var currentTasks: Set> = [] // MARK: - Initialization @@ -483,7 +486,12 @@ open class Storage { guard !isSuspended else { return } isSuspended = true - Log.info(.storage, "Database access suspended.") + Log.info(.storage, "Database access suspended - cancelling \(currentTasks.count) running task(s).") + + /// Before triggering an `interrupt` (which will forcibly kill in-progress database queries) we want to try to cancel all + /// database tasks to give them a small chance to resolve cleanly before we take a brute-force approach + currentTasks.forEach { $0.cancel() } + _currentTasks.performUpdate { _ in [] } /// Interrupt any open transactions (if this function is called then we are expecting that all processes have finished running /// and don't actually want any more transactions to occur) @@ -660,6 +668,7 @@ open class Storage { let syncQueue = DispatchQueue(label: "com.session.performOperation.syncQueue") let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) var operationResult: Result? + var operationTask: Task<(), Never>? let logErrorIfNeeded: (Result) -> Result = { result in switch result { case .success: break @@ -673,6 +682,7 @@ open class Storage { func completeOperation(with result: Result) { syncQueue.sync { guard operationResult == nil else { return } + info.storage?.removeTask(operationTask) operationResult = result semaphore.signal() @@ -741,6 +751,10 @@ open class Storage { } } + /// Store the task in case we want to + info.storage?.addTask(task) + operationTask = task + /// For the `async` operation the returned value should be ignored so just return the `invalidQueryResult` error guard !info.isAsync else { return (.failure(StorageError.invalidQueryResult), task) @@ -782,13 +796,24 @@ open class Storage { holder.task = task } } - .handleEvents(receiveCancel: { holder.task?.cancel() }) + .handleEvents(receiveCancel: { [weak self] in + holder.task?.cancel() + self?.removeTask(holder.task) + }) .eraseToAnyPublisher() } } // MARK: - Functions + private func addTask(_ task: Task<(), Never>) { + _currentTasks.performUpdate { $0.inserting(task) } + } + + private func removeTask(_ task: Task<(), Never>?) { + _currentTasks.performUpdate { $0.removing(task) } + } + @discardableResult public func write( fileName file: String = #file, functionName funcN: String = #function, From 82b9c18929312291861b771c525e2acbfed55778 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Mar 2025 10:48:24 +1100 Subject: [PATCH 21/24] Fixed an issue where groups with only some config dumps wouldn't load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Cleaned up the interface for loading the "default" state for configs (shouldn't really be used outside of onboarding to tweaked it's interface) • Updated the logic so group configs would be initialised all at once per group (previously it would load them in variant load order, now it will be group -> variant load order) • Fixed an issue where groups in the invited state would incorrectly load their config states --- Session/Onboarding/Onboarding.swift | 10 +- .../LibSession+SessionMessagingKit.swift | 153 +++++++++--------- .../_TestUtilities/MockLibSessionCache.swift | 12 +- 3 files changed, 92 insertions(+), 83 deletions(-) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 45e045080..a43e3e2f3 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -241,11 +241,11 @@ extension Onboarding { /// In order to process the config message we need to create and load a `libSession` cache, but we don't want to load this into /// memory at this stage in case the user cancels the onboarding process part way through let cache: LibSession.Cache = LibSession.Cache(userSessionId: userSessionId, using: dependencies) - cache.loadDefaultStatesFor( - userConfigVariants: [.userProfile], - groups: [], - userSessionId: userSessionId, - userEd25519KeyPair: identity.ed25519KeyPair + cache.loadDefaultStateFor( + variant: .userProfile, + sessionId: userSessionId, + userEd25519KeyPair: identity.ed25519KeyPair, + groupEd25519SecretKey: nil ) try cache.unsafeDirectMergeConfigMessage( swarmPublicKey: userSessionId.hexString, diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 4e6855a2e..47a86b523 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -213,81 +213,90 @@ public extension LibSession { let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { return Log.warn(.libSession, "Ignoring loadState due to existing state") } - // Retrieve the existing dumps from the database - let existingDumps: [ConfigDump] = ((try? ConfigDump.fetchSet(db)) ?? []) - .sorted { lhs, rhs in lhs.variant.loadOrder < rhs.variant.loadOrder } - let existingDumpVariants: Set = existingDumps - .map { $0.variant } - .asSet() - let missingRequiredVariants: Set = ConfigDump.Variant.userVariants - .subtracting(existingDumpVariants) - let groupsByKey: [String: ClosedGroup] = (try? ClosedGroup + /// Retrieve the existing dumps from the database + typealias ConfigInfo = (sessionId: SessionId, variant: ConfigDump.Variant, dump: ConfigDump?) + let existingDumpsByKey: [String: [ConfigDump]] = ((try? ConfigDump.fetchAll(db)) ?? []) + .grouped(by: \.sessionId.hexString) + var configsToLoad: [ConfigInfo] = [] + + /// Load in the user dumps first (it's possible for a user dump to be missing due to some edge-cases so use + /// `ConfigDump.Variant.userVariants` to ensure we will at least load a default state and just assume + /// it will be fixed when we eventually poll for it) + configsToLoad.append( + contentsOf: ConfigDump.Variant.userVariants + .sorted { $0.loadOrder < $1.loadOrder } + .map { variant in + ( + userSessionId, + variant, + existingDumpsByKey[userSessionId.hexString]? + .first(where: { $0.variant == variant }) + ) + } + ) + + /// Then load in dumps for groups + /// + /// Similar to the above it's possible to have a partial group state due to edge-cases where a config could be lost, but also + /// immediately after creating a group (eg. when a crash happens at the right time), for these cases we again assume they + /// will be solved eventually via polling so still want to load their states into memory (if we don't then we likely wouldn't be + /// able to decrypt the poll response and the group would never recover) + /// + /// **Note:** We exclude groups in the `invited` state as they should only have their state loaded once the invitation + /// gets accepted + let allGroups: [ClosedGroup] = (try? ClosedGroup .filter( ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString ) - .fetchAll(db) - .reduce(into: [:]) { result, next in result[next.threadId] = next }) - .defaulting(to: [:]) - let groupsWithNoDumps: [ClosedGroup] = groupsByKey - .values - .filter { group in !existingDumps.contains(where: { $0.sessionId.hexString == group.id }) } - - // Create the config records for each dump - existingDumps.forEach { dump in - configStore[dump.sessionId, dump.variant] = try? loadState( - for: dump.variant, - sessionId: dump.sessionId, + .filter(ClosedGroup.Columns.invited == false) + .fetchAll(db)) + .defaulting(to: []) + let groupsByKey: [String: ClosedGroup] = allGroups + .reduce(into: [:]) { result, group in result[group.threadId] = group } + allGroups.forEach { group in + configsToLoad.append( + contentsOf: ConfigDump.Variant.groupVariants + .sorted { $0.loadOrder < $1.loadOrder } + .map { variant in + ( + SessionId(.group, hex: group.threadId), + variant, + existingDumpsByKey[group.threadId]? + .first(where: { $0.variant == variant }) + ) + } + ) + } + + /// Now that we have fully populated and sorted `configsToLoad` we should load each into memory + configsToLoad.forEach { sessionId, variant, dump in + configStore[sessionId, variant] = try? loadState( + for: variant, + sessionId: sessionId, userEd25519SecretKey: ed25519KeyPair.secretKey, - groupEd25519SecretKey: groupsByKey[dump.sessionId.hexString]? + groupEd25519SecretKey: groupsByKey[sessionId.hexString]? .groupIdentityPrivateKey .map { Array($0) }, - cachedData: dump.data + cachedData: dump?.data ) } - - /// It's possible for there to not be dumps for all of the configs so we load any missing ones to ensure functionality - /// works smoothly - /// - /// It's also possible for a group to get created but for a dump to not be created (eg. when a crash happens at the right time), to - /// handle this we also load the state of any groups which don't have dumps if they aren't in the `invited` state (those in - /// the `invited` state will have their state loaded if the invite is accepted) - loadDefaultStatesFor( - userConfigVariants: missingRequiredVariants, - groups: groupsWithNoDumps, - userSessionId: userSessionId, - userEd25519KeyPair: ed25519KeyPair - ) Log.info(.libSession, "Completed loadState") } - public func loadDefaultStatesFor( - userConfigVariants: Set, - groups: [ClosedGroup], - userSessionId: SessionId, - userEd25519KeyPair: KeyPair + public func loadDefaultStateFor( + variant: ConfigDump.Variant, + sessionId: SessionId, + userEd25519KeyPair: KeyPair, + groupEd25519SecretKey: [UInt8]? ) { - /// Create an empty state for the specified user config variants - userConfigVariants.forEach { variant in - configStore[userSessionId, variant] = try? loadState( - for: variant, - sessionId: userSessionId, - userEd25519SecretKey: userEd25519KeyPair.secretKey, - groupEd25519SecretKey: nil, - cachedData: nil - ) - } - - /// Create empty group states for the provided groups - groups - .filter { $0.invited != true } - .forEach { group in - _ = try? createAndLoadGroupState( - groupSessionId: SessionId(.group, hex: group.id), - userED25519KeyPair: userEd25519KeyPair, - groupIdentityPrivateKey: group.groupIdentityPrivateKey - ) - } + configStore[sessionId, variant] = try? loadState( + for: variant, + sessionId: sessionId, + userEd25519SecretKey: userEd25519KeyPair.secretKey, + groupEd25519SecretKey: groupEd25519SecretKey, + cachedData: nil + ) } internal func loadState( @@ -895,11 +904,11 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT // MARK: - State Management func loadState(_ db: Database) - func loadDefaultStatesFor( - userConfigVariants: Set, - groups: [ClosedGroup], - userSessionId: SessionId, - userEd25519KeyPair: KeyPair + func loadDefaultStateFor( + variant: ConfigDump.Variant, + sessionId: SessionId, + userEd25519KeyPair: KeyPair, + groupEd25519SecretKey: [UInt8]? ) func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config? @@ -988,11 +997,11 @@ private final class NoopLibSessionCache: LibSessionCacheType { // MARK: - State Management func loadState(_ db: Database) {} - func loadDefaultStatesFor( - userConfigVariants: Set, - groups: [ClosedGroup], - userSessionId: SessionId, - userEd25519KeyPair: KeyPair + func loadDefaultStateFor( + variant: ConfigDump.Variant, + sessionId: SessionId, + userEd25519KeyPair: KeyPair, + groupEd25519SecretKey: [UInt8]? ) {} func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool { return false } func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config? { return nil } diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index f722acc8a..08d611313 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -14,13 +14,13 @@ class MockLibSessionCache: Mock, LibSessionCacheType { // MARK: - State Management func loadState(_ db: Database) { mockNoReturn(untrackedArgs: [db]) } - func loadDefaultStatesFor( - userConfigVariants: Set, - groups: [ClosedGroup], - userSessionId: SessionId, - userEd25519KeyPair: KeyPair + func loadDefaultStateFor( + variant: ConfigDump.Variant, + sessionId: SessionId, + userEd25519KeyPair: KeyPair, + groupEd25519SecretKey: [UInt8]? ) { - mockNoReturn(args: [userConfigVariants, groups, userSessionId, userEd25519KeyPair]) + mockNoReturn(args: [variant, sessionId, userEd25519KeyPair, groupEd25519SecretKey]) } func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool { From 975fc52b5dcc98ca7b0dd57a5f8ff8a32c1d876c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Mar 2025 11:07:32 +1100 Subject: [PATCH 22/24] Removed a left over TODO --- SessionMessagingKit/Jobs/MessageReceiveJob.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/SessionMessagingKit/Jobs/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/MessageReceiveJob.swift index 53e7f2f0c..df2980c8a 100644 --- a/SessionMessagingKit/Jobs/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/MessageReceiveJob.swift @@ -107,7 +107,6 @@ public enum MessageReceiveJob: JobExecutor { return lastError }, completion: { result in - // TODO: [REFACTOR] Need to test this!!! // Handle the result switch result { case .failure(let error): failure(updatedJob, error, false) From d21737eca7930f5dea6929a2d64d79c23bf5bfeb Mon Sep 17 00:00:00 2001 From: mpretty-cyro <15862619+mpretty-cyro@users.noreply.github.com> Date: Mon, 17 Mar 2025 00:38:31 +0000 Subject: [PATCH 23/24] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 787 ++++++++++++++++++ 1 file changed, 787 insertions(+) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 840e9bef5..7b4ba14d6 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -12566,11 +12566,23 @@ "adminPromotionNotSent" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo odesláno" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Promotion not sent" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запрос не отправлен" + } } } }, @@ -13062,11 +13074,23 @@ "adminPromotionStatusUnknown" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neznámý stav povýšení" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Promotion status unknown" } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Состояние запроса неизвестно" + } } } }, @@ -17552,6 +17576,46 @@ } } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправление запросов в админы" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправление запросов в админы" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправление запроса в админы" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправление запросов в админы" + } + } + } + } + } + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -71485,6 +71549,12 @@ "value" : "Uprawnienie „Połączenia głosowe i wideo” można włączyć w Ustawieniach uprawnień." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы можете включить разрешение \"Аудио и видео звонки\" в настройках разрешений." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -122207,6 +122277,12 @@ "creatingCall" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vytváření hovoru" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -143980,6 +144056,46 @@ } } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений не могут удалиться с всех ваших устройств" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений не могут удалиться с всех ваших устройств" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это сообщение не может удалиться с всех ваших устройств." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений не могут удалиться с всех ваших устройств" + } + } + } + } + } + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -144678,6 +144794,46 @@ } } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений нельзя удалить для всех" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений нельзя удалить для всех" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это сообщение нельзя удалить для всех" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые из выбранных вами сообщений нельзя удалить для всех" + } + } + } + } + } + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -166354,6 +166510,12 @@ "value" : "Wyświetlana nazwa użytkownika jest widoczna dla użytkowników, grup i społeczności, z którymi użytkownik wchodzi w interakcje." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваше отображаемое имя видно пользователям, группам и сообществам, с которыми вы взаимодействуете." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -189060,6 +189222,12 @@ "value" : "Are you sure you want to delete {group_name}?" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы действительно хотите удалить группу {group_name}?" + } + }, "vi" : { "stringUnit" : { "state" : "translated", @@ -189107,6 +189275,12 @@ "value" : "{group_name} została usunięta przez administratora grupy. Nie będzie można wysyłać więcej wiadomości." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} был удалён администратором группы. Вы больше не сможете отправлять сообщения в этой группе." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -194926,12 +195100,24 @@ "groupInviteNotSent" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvánka nebyla odeslána" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Invite not sent" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглашение не отправлено" + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -194967,6 +195153,12 @@ "value" : "{name} zaprosił Cię do ponownego dołączenia do {group_name}, gdzie jesteś administratorem." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} приглашает вас вернуться в {group_name}, где вы являетесь администратором." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -195008,6 +195200,12 @@ "value" : "Zostałeś zaproszony do ponownego dołączenia do {group_name}, gdzie jesteś administratorem." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вам предложили снова присоединиться к {group_name}, где вы являетесь администратором." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -195871,12 +196069,24 @@ "groupInviteStatusUnknown" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neznámý stav pozvánky" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Invite status unknown" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Состояние приглашения не известно" + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -198283,6 +198493,12 @@ "value" : "Zostałeś zaproszony do dołączenia do grupy. Udostępniono historię czatu." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы были приглашены в группу. История чата была передана." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -204988,6 +205204,12 @@ "value" : "U en {other_name} zijn uitgenodigd om lid te worden van de groep. Gespreksgeschiedenis wordt gedeeld." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы и {other_name} были приглашены в группу. История чата была передана." + } + }, "vi" : { "stringUnit" : { "state" : "translated", @@ -209358,6 +209580,12 @@ "value" : "Nazwa grupy jest widoczna dla wszystkich jej członków." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название группы видно всем участникам группы." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -209854,11 +210082,23 @@ "groupNotUpdatedWarning" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupina nebyla aktualizována déle než 30 dní. Může dojít k problémům s odesíláním zpráv nebo zobrazováním informací o skupině." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Group has not been updated in over 30 days. You may experience issues sending messages or viewing Group information." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Группа не обновлялась более 30 дней. Вы можете столкнуться с проблемами при отправке сообщений или просмотре информации группы." + } } } }, @@ -210362,6 +210602,12 @@ "value" : "In afwachting van verwijdering" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ожидание удаления" + } + }, "vi" : { "stringUnit" : { "state" : "translated", @@ -211331,11 +211577,23 @@ "groupPromotedYouTwo" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vy a {other_name} jste byli povýšeni na správce." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You and {other_name} were promoted to Admin." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы и {other_name} были назначены Администраторами." + } } } }, @@ -214743,6 +215001,12 @@ "value" : "Zostałeś usunięty z grupy." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы были удалены из группы." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -221745,6 +222009,12 @@ "handlingConnectionCandidates" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpracování kandidátů na připojení" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -230372,6 +230642,46 @@ "inviteFailed" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvání selhala" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvání selhala" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvání selhalo" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvání selhala" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -230399,12 +230709,92 @@ } } } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка отправки приглашений" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка отправки приглашений" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка отправки приглашения" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка отправки приглашений" + } + } + } + } + } + } } } }, "inviteFailedDescription" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvánky nemohly být odeslány. Chcete to zkusit znovu?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvánky nemohly být odeslány. Chcete to zkusit znovu?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvánka nemohla být odeslána. Chcete to zkusit znovu?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozvánky nemohly být odeslány. Chcete to zkusit znovu?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -230432,6 +230822,46 @@ } } } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглашения не были отправлены. Повторить попытку?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглашения не были отправлены. Повторить попытку?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглашение не отправлено. Повторить попытку?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглашения не были отправлены. Повторить попытку?" + } + } + } + } + } + } } } }, @@ -232833,55 +233263,115 @@ "legacyGroupAfterDeprecationAdmin" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tato skupina je nyní určena pouze pro čtení. Chcete-li pokračovat v konverzaci, vytvořte tuto skupinu znovu." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "This group is now read-only. Recreate this group to keep chatting." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эта группа доступна только для чтения. Пересоздайте эту группу, чтобы продолжить общаться." + } } } }, "legacyGroupAfterDeprecationMember" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tato skupina je nyní určena pouze pro čtení. Požádejte správce skupiny, aby tuto skupinu znovu vytvořil, abyste mohli pokračovat v chatu." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "This group is now read-only. Ask the group admin to recreate this group to keep chatting." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эта группа доступна только для чтения. Попросите владельца пересоздать группу, чтобы продолжить общаться." + } } } }, "legacyGroupBeforeDeprecationAdmin" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupiny byly aktualizovány! Znovu vytvořte tuto skupinu pro zvýšení spolehlivosti. Tato skupina se stane pouze pro čtení dne {date}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only at {date}." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Группы обновлены! Пересоздайте эту группу для надёжности. Эта группа будет доступна только для чтения {date}." + } } } }, "legacyGroupBeforeDeprecationMember" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupiny byly aktualizovány! Požádejte správce skupiny o znovuvytvoření této skupiny, aby se zvýšila její spolehlivost. Tato skupina se stane pouze pro čtení dne {date}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only at {date}." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Группы обновлены! Попросите владельца пересоздать группу для надёжности. Эта группа будет доступна только для чтения {date}." + } } } }, "legacyGroupChatHistory" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historie chatu se do nové skupiny nepřenese. Veškerou historii chatu ve staré skupině si můžete stále prohlížet." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Chat history will not be transferred to the new group. You can still view all chat history in your old group." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "История переписки не может быть перенесена в новую группу. Вы всё также можете посмотреть всю историю переписки в вашей старой группе." + } } } }, @@ -265646,6 +266136,36 @@ "messageNewYouveGotGroup" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve skupině {group_name} máte %lld nové zprávy." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve skupině {group_name} máte %lld nových zpráv." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve skupině {group_name} máte novou zprávu." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve skupině {group_name} máte %lld nových zpráv." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -265663,6 +266183,36 @@ } } } + }, + "ru" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld новых сообщений в {group_name}." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld новых сообщений в {group_name}." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Новое сообщение в {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld новых сообщений в {group_name}." + } + } + } + } } } }, @@ -279596,6 +280146,12 @@ "value" : "Trieu un sobrenom per a {name}. Això us apareixerà a les vostres converses individuals i de grup." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyberte přezdívku pro {name}. Zobrazí se ve vašich konverzacích jeden na jednoho a ve skupinových." + } + }, "cy" : { "stringUnit" : { "state" : "translated", @@ -280530,6 +281086,12 @@ "value" : "Wprowadź krótszy pseudonim" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, введите более короткий псевдоним" + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -328474,6 +329036,17 @@ } } }, + "permissionsLocalNetworkAccessRequiredIos" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} needs access to local network to make voice and video calls." + } + } + } + }, "permissionsMicrophone" : { "extractionState" : "manual", "localizations" : { @@ -339955,6 +340528,46 @@ "promotionFailed" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhalo" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -339982,12 +340595,92 @@ } } } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевод не удался" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + } + } + } + } + } } } }, "promotionFailedDescription" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -340015,6 +340708,46 @@ } } } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запрос. Повторить попытку?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + } + } + } + } + } } } }, @@ -345769,6 +346502,12 @@ "receivedAnswer" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přijatá odpověď" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -345780,6 +346519,12 @@ "receivingCallOffer" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přijímání nabídky hovoru" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -345791,6 +346536,12 @@ "receivingPreOffer" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přijímání předběžné nabídky" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -354447,12 +355198,24 @@ "recreateGroup" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Znovu vytvořit skupinu" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Recreate Group" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пересоздать группу" + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -370387,6 +371150,12 @@ "sendingCallOffer" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání nabídky hovoru" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -370398,6 +371167,12 @@ "sendingConnectionCandidates" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání kandidátů na připojení" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -379043,11 +379818,23 @@ "shareExtensionNoAccountError" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ups! Vypadá to, že ještě nemáte účet {app_name}.

Před sdílením si ho budete muset v aplikaci {app_name} vytvořit." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Oops! Looks like you don't have a {app_name} account yet.

You'll need to create one in the {app_name} app before you can share." } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ой! Кажется у вас нет аккаунта {app_name}.
\n
Вам нужно создать новый в приложении {app_name}, чтобы поделиться." + } } } }, From a63e58b96bb2623e97870cf703e00bed73c664fd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 17 Mar 2025 16:56:36 +1100 Subject: [PATCH 24/24] Updated the version & build numbers --- Session.xcodeproj/project.pbxproj | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 727e18466..4759ec572 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -705,12 +705,12 @@ FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; - FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; - FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; - FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; FD6673F62D7021E700041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F52D7021E700041530 /* SessionUtil */; }; FD6673F82D7021F200041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F72D7021F200041530 /* SessionUtil */; }; FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F92D7021F800041530 /* SessionUtil */; }; + FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; + FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; + FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; @@ -7861,7 +7861,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 557; + CURRENT_PROJECT_VERSION = 561; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7900,7 +7900,7 @@ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; - MARKETING_VERSION = 2.9.0; + MARKETING_VERSION = 2.9.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -7939,7 +7939,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 557; + CURRENT_PROJECT_VERSION = 561; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -7974,7 +7974,7 @@ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; - MARKETING_VERSION = 2.9.0; + MARKETING_VERSION = 2.9.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8483,7 +8483,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 488; + CURRENT_PROJECT_VERSION = 561; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8524,7 +8524,7 @@ ); IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.9.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9151,7 +9151,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 488; + CURRENT_PROJECT_VERSION = 561; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9187,7 +9187,7 @@ ); IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.9.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -10170,8 +10170,8 @@ requirement = { kind = upToNextMajorVersion; minimumVersion = 3.0.0; - }; - }; + }; + }; FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/session-foundation/libsession-util-spm"; @@ -10283,7 +10283,7 @@ isa = XCSwiftPackageProductDependency; package = 946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */; productName = Punycode; - }; + }; FD0150512CA2446D005B08A1 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */;