From 34fea96db3ea1ef5cb9993912fcbcced9edd7e35 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 4 Jul 2022 17:36:48 +1000 Subject: [PATCH] Fixed a bunch more bugs around push notifications and avatars Added code to prevent the garbage collection job from auto-running more often than once every 23 hours Fixed a bug where if the first avatar you try to add is your own, it could fail due to the folder not getting created Fixed a bug where updating your profile would store and send an invalid profile picture url against your profile Fixed an issue where the closed group icon wouldn't appear as the double icon when it couldn't retrieve a second profile Fixed a bug where the device might not correctly register for push notifications in some cases Fixed a bug where interacting with a notification when the app is in the background (but not closed) wasn't doing anything Fixed a bug where the SyncPushTokensJob wouldn't re-run correctly in some cases if the user was already registered Updated the profile avatar downloading logic to only download avatars if they have been updated Updated the migration and OpenGroupManager to force Session-run open groups to always use the OpenGroupAPI.defaultServer value --- Session/Meta/AppDelegate.swift | 12 +-- Session/Notifications/SyncPushTokensJob.swift | 7 +- Session/Shared/FullConversationCell.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 8 +- .../Jobs/Types/GarbageCollectionJob.swift | 15 ++++ .../Open Groups/OpenGroupManager.swift | 78 +++++++++++++++---- .../MessageReceiver+ClosedGroups.swift | 6 +- .../Sending & Receiving/MessageReceiver.swift | 6 +- .../Notifications/PushNotificationAPI.swift | 62 ++++++++------- .../SessionThreadViewModel.swift | 12 +++ .../Utilities/ProfileManager.swift | 9 ++- .../Open Groups/OpenGroupManagerSpec.swift | 59 ++++++++++++++ .../NotificationServiceExtension.swift | 18 ++--- .../SimplifiedConversationCell.swift | 3 +- SessionSnodeKit/SnodeAPI.swift | 78 +++++++++---------- .../Migrations/_002_SetupStandardJobs.swift | 8 ++ .../General/SNUserDefaults.swift | 1 + .../Profile Pictures/ProfilePictureView.swift | 69 ++++++++-------- 18 files changed, 315 insertions(+), 139 deletions(-) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 556337aae..0cf8f0e5a 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -144,11 +144,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self?.handleActivation() } - // Clear all notifications whenever we become active. - // When opening the app from a notification, - // AppDelegate.didReceiveLocalNotification will always - // be called _before_ we become active. - clearAllNotificationsAndRestoreBadgeCount() + /// Clear all notifications whenever we become active + /// + /// **Note:** It looks like when opening the app from a notification, `userNotificationCenter(didReceive)` is + /// no longer always called before we become active so we need to dispatch this to run on the next run loop + DispatchQueue.main.async { [weak self] in + self?.clearAllNotificationsAndRestoreBadgeCount() + } // On every activation, clear old temp directories. ClearOldTemporaryDirectories(); diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 478f3fa55..ca26e0347 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -34,7 +34,11 @@ public enum SyncPushTokensJob: JobExecutor { return } - guard !UIApplication.shared.isRegisteredForRemoteNotifications else { + // Push tokens don't normally change while the app is launched, so checking once during launch is + // usually sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications" and disabled + // "Background App Refresh" will not be able to obtain an APN token. Enabling those settings does not + // restart the app, so we check every activation for users who haven't yet registered. + guard job.behaviour != .recurringOnActive || !UIApplication.shared.isRegisteredForRemoteNotifications else { deferred(job) // Don't need to do anything if push notifications are already registered return } @@ -157,6 +161,7 @@ extension SyncPushTokensJob { failure(error) } + .retainUntilComplete() } } diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index e8beb16b1..92ffebb8a 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -339,7 +339,8 @@ public final class FullConversationCell: UITableViewCell { useFallbackPicture: ( cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil - ) + ), + showMultiAvatarForClosedGroup: true ) displayNameLabel.text = cellViewModel.displayName timestampLabel.text = cellViewModel.lastInteractionDate.formattedForDisplay diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 12682d06d..0a70ebeb9 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -178,11 +178,9 @@ enum _003_YDBToGRDBMigration: Migration { // value contains a HTTPS scheme so we get IP HTTP -> HTTPS for free as well) let processedOpenGroupServer: String = { // Check if the server is a Session-run one based on it's - guard - openGroup.server.contains(OpenGroupAPI.legacyDefaultServerIP) || - openGroup.server == OpenGroupAPI.defaultServer - .replacingOccurrences(of: "https://", with: "http://") - else { return openGroup.server } + guard OpenGroupManager.isSessionRunOpenGroup(server: openGroup.server) else { + return openGroup.server + } return OpenGroupAPI.defaultServer }() diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 4ec8ae8b9..43ce8b5ea 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -34,6 +34,21 @@ public enum GarbageCollectionJob: JobExecutor { .defaulting(to: Types.allCases) let timestampNow: TimeInterval = Date().timeIntervalSince1970 + /// Only do something if the job isn't the recurring one or it's been 23 hours since it last ran (23 hours so a user who opens the + /// app at about the same time every day will trigger the garbage collection) - since this runs when the app becomes active we + /// want to prevent it running to frequently (the app becomes active if a system alert, the notification center or the control panel + /// are shown) + let lastGarbageCollection: Date = UserDefaults.standard[.lastGarbageCollection] + .defaulting(to: Date.distantPast) + + guard + job.behaviour != .recurringOnActive || + Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60) + else { + deferred(job) + return + } + Storage.shared.writeAsync( updates: { db in /// Remove any expired controlMessageProcessRecords diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index c307e1a80..856003cc3 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -108,12 +108,57 @@ public final class OpenGroupManager: NSObject { // MARK: - Adding & Removing + private static func port(for server: String, serverUrl: URL) -> String { + if let port: Int = serverUrl.port { + return ":\(port)" + } + + let components: [String] = server.components(separatedBy: ":") + + guard + let port: String = components.last, + ( + port != components.first && + !port.starts(with: "//") + ) + else { return "" } + + return ":\(port)" + } + + public static func isSessionRunOpenGroup(server: String) -> Bool { + guard let serverUrl: URL = URL(string: server.lowercased()) else { return false } + + let serverPort: String = OpenGroupManager.port(for: server, serverUrl: serverUrl) + let serverHost: String = serverUrl.host + .defaulting( + to: server + .lowercased() + .replacingOccurrences(of: serverPort, with: "") + ) + let options: Set = Set([ + OpenGroupAPI.legacyDefaultServerIP, + OpenGroupAPI.defaultServer + .replacingOccurrences(of: "http://", with: "") + .replacingOccurrences(of: "https://", with: "") + ]) + + return options.contains(serverHost) + } + public func hasExistingOpenGroup(_ db: Database, roomToken: String, server: String, publicKey: String, dependencies: OGMDependencies = OGMDependencies()) -> Bool { guard let serverUrl: URL = URL(string: server.lowercased()) else { return false } - let serverHost: String = (serverUrl.host ?? server.lowercased()) - let serverPort: String = (serverUrl.port.map { ":\($0)" } ?? "") - let defaultServerHost: String = OpenGroupAPI.defaultServer.substring(from: "https://".count) + let serverPort: String = OpenGroupManager.port(for: server, serverUrl: serverUrl) + let serverHost: String = serverUrl.host + .defaulting( + to: server + .lowercased() + .replacingOccurrences(of: serverPort, with: "") + ) + let defaultServerHost: String = OpenGroupAPI.defaultServer + .replacingOccurrences(of: "http://", with: "") + .replacingOccurrences(of: "https://", with: "") var serverOptions: Set = Set([ server.lowercased(), "\(serverHost)\(serverPort)", @@ -121,12 +166,12 @@ public final class OpenGroupManager: NSObject { "https://\(serverHost)\(serverPort)" ]) - if serverHost == OpenGroupAPI.legacyDefaultServerIP { + // If the server is run by Session then include all configurations in case one of the alternate configurations + // was used + if OpenGroupManager.isSessionRunOpenGroup(server: server) { serverOptions.insert(defaultServerHost) serverOptions.insert("http://\(defaultServerHost)") - serverOptions.insert(OpenGroupAPI.defaultServer) - } - else if serverHost == defaultServerHost { + serverOptions.insert("https://\(defaultServerHost)") serverOptions.insert(OpenGroupAPI.legacyDefaultServerIP) serverOptions.insert("http://\(OpenGroupAPI.legacyDefaultServerIP)") serverOptions.insert("https://\(OpenGroupAPI.legacyDefaultServerIP)") @@ -158,7 +203,14 @@ public final class OpenGroupManager: NSObject { } // Store the open group information - let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: server) + let targetServer: String = { + guard OpenGroupManager.isSessionRunOpenGroup(server: server) else { + return server.lowercased() + } + + return OpenGroupAPI.defaultServer + }() + let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: targetServer) // Optionally try to insert a new version of the OpenGroup (it will fail if there is already an // inactive one but that won't matter as we then activate it @@ -167,14 +219,14 @@ public final class OpenGroupManager: NSObject { if (try? OpenGroup.exists(db, id: threadId)) == false { try? OpenGroup - .fetchOrCreate(db, server: server, roomToken: roomToken, publicKey: publicKey) + .fetchOrCreate(db, server: targetServer, roomToken: roomToken, publicKey: publicKey) .save(db) } // Set the group to active and reset the sequenceNumber (handle groups which have // been deactivated) _ = try? OpenGroup - .filter(id: OpenGroup.idFor(roomToken: roomToken, server: server)) + .filter(id: OpenGroup.idFor(roomToken: roomToken, server: targetServer)) .updateAll( db, OpenGroup.Columns.isActive.set(to: true), @@ -195,7 +247,7 @@ public final class OpenGroupManager: NSObject { .capabilitiesAndRoom( db, for: roomToken, - on: server, + on: targetServer, authenticated: false, using: dependencies ) @@ -206,7 +258,7 @@ public final class OpenGroupManager: NSObject { OpenGroupManager.handleCapabilities( db, capabilities: response.capabilities.data, - on: server + on: targetServer ) // Then the room @@ -215,7 +267,7 @@ public final class OpenGroupManager: NSObject { pollInfo: OpenGroupAPI.RoomPollInfo(room: response.room.data), publicKey: publicKey, for: roomToken, - on: server, + on: targetServer, dependencies: dependencies ) { seal.fulfill(()) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index a9e24c8f7..e2312383d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -433,8 +433,10 @@ extension MessageReceiver { publicKey: userPublicKey ) } - else { - // Re-add the removed member as a zombie + + // Re-add the removed member as a zombie (unless the admin left which disbands the + // group) + if !didAdminLeave { try GroupMember( groupId: id, profileId: sender, diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 5b4dca427..da8b9065b 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -344,8 +344,10 @@ public enum MessageReceiver { } // Download the profile picture if needed - db.afterNextTransactionCommit { _ in - ProfileManager.downloadAvatar(for: updatedProfile) + if updatedProfile.profilePictureUrl != profile.profilePictureUrl || updatedProfile.profileEncryptionKey != profile.profileEncryptionKey { + db.afterNextTransactionCommit { _ in + ProfileManager.downloadAvatar(for: updatedProfile) + } } } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 7aad64ac8..7770a65ae 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -113,41 +113,49 @@ public final class PushNotificationAPI : NSObject { request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] request.httpBody = body - let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { - OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) - .map2 { _, response in - guard let response: UpdateRegistrationResponse = try? response?.decoded(as: UpdateRegistrationResponse.self) else { - return SNLog("Couldn't register device token.") - } - guard response.body.code != 0 else { - return SNLog("Couldn't register device token due to error: \(response.body.message ?? "nil").") + var promises: [Promise] = [] + + promises.append( + attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) + .map2 { _, response -> Void in + guard let response: UpdateRegistrationResponse = try? response?.decoded(as: UpdateRegistrationResponse.self) else { + return SNLog("Couldn't register device token.") + } + guard response.body.code != 0 else { + return SNLog("Couldn't register device token due to error: \(response.body.message ?? "nil").") + } + + userDefaults[.deviceToken] = hexEncodedToken + userDefaults[.lastDeviceTokenUpload] = now + userDefaults[.isUsingFullAPNs] = true } - - userDefaults[.deviceToken] = hexEncodedToken - userDefaults[.lastDeviceTokenUpload] = now - userDefaults[.isUsingFullAPNs] = true - } - } - promise.catch2 { error in + } + ) + promises.first?.catch2 { error in SNLog("Couldn't register device token.") } // Subscribe to all closed groups - Storage.shared.read { db in - try ClosedGroup - .select(.threadId) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) - ) - .asRequest(of: String.self) - .fetchAll(db) - .forEach { closedGroupPublicKey in + promises.append( + contentsOf: Storage.shared + .read { db -> [String] in + try ClosedGroup + .select(.threadId) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) + ) + .asRequest(of: String.self) + .fetchAll(db) + } + .defaulting(to: []) + .map { closedGroupPublicKey -> Promise in performOperation(.subscribe, for: closedGroupPublicKey, publicKey: publicKey) } - } + ) - return promise + return when(fulfilled: promises) } @objc(registerWithToken:hexEncodedPublicKey:isForcedUpdate:) diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index e9132b51d..f32718c95 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -421,6 +421,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -433,6 +434,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -662,6 +664,7 @@ public extension SessionThreadViewModel { let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) @@ -698,6 +701,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -710,6 +714,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -851,6 +856,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -863,6 +869,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -1034,6 +1041,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -1046,6 +1054,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -1330,6 +1339,7 @@ public extension SessionThreadViewModel { let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) @@ -1378,6 +1388,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND @@ -1390,6 +1401,7 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) + JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE ( \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 170bee378..46a0564e2 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -83,9 +83,12 @@ public struct ProfileManager { // MARK: - File Paths public static let sharedDataProfileAvatarsDirPath: String = { - URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath()) + let path: String = URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath()) .appendingPathComponent("ProfileAvatars") .path + OWSFileSystem.ensureDirectoryExists(path) + + return path }() private static let profileAvatarsDirPath: String = { @@ -305,8 +308,8 @@ public struct ProfileManager { // Upload the avatar to the FileServer FileServerAPI .upload(encryptedAvatarData) - .done(on: queue) { fileId in - let downloadUrl: String = "\(FileServerAPI.server)/files/\(fileId)" + .done(on: queue) { fileUploadResponse in + let downloadUrl: String = "\(FileServerAPI.server)/files/\(fileUploadResponse.id)" UserDefaults.standard[.lastProfilePictureUpload] = Date() Storage.shared.writeAsync { db in diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index d0f84429f..bfb823411 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -421,6 +421,65 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - Adding & Removing + // MARK: - --isSessionRunOpenGroup + + context("when checking if an open group is run by session") { + it("returns false when it does not match one of Sessions servers with no scheme") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "test.test")) + .to(beFalse()) + } + + it("returns false when it does not match one of Sessions servers in http") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://test.test")) + .to(beFalse()) + } + + it("returns false when it does not match one of Sessions servers in https") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://test.test")) + .to(beFalse()) + } + + it("returns true when it matches Sessions SOGS IP") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS IP with http") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://116.203.70.33")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS IP with https") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://116.203.70.33")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS IP with a port") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33:80")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS domain") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS domain with http") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://open.getsession.org")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS domain with https") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://open.getsession.org")) + .to(beTrue()) + } + + it("returns true when it matches Sessions SOGS domain with a port") { + expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org:80")) + .to(beTrue()) + } + } + // MARK: - --hasExistingOpenGroup context("when checking it has an existing open group") { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index bb701f1be..1cb3dd274 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -82,16 +82,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension let interaction: Interaction = try? Interaction.fetchOne(db, id: interactionId), interaction.variant == .standardOutgoing { - let semaphore = DispatchSemaphore(value: 0) - let center = UNUserNotificationCenter.current() - center.getDeliveredNotifications { notifications in - let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == interaction.threadId }) - center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier })) - // Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() } + let semaphore = DispatchSemaphore(value: 0) + let center = UNUserNotificationCenter.current() + center.getDeliveredNotifications { notifications in + let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == interaction.threadId }) + center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier })) + // Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() } + } + semaphore.wait() } - semaphore.wait() - } case let unsendRequest as UnsendRequest: try MessageReceiver.handleUnsendRequest(db, message: unsendRequest) diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index b3427ebb3..9bea5687a 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -95,7 +95,8 @@ final class SimplifiedConversationCell: UITableViewCell { additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, - useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil) + useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil), + showMultiAvatarForClosedGroup: true ) displayNameLabel.text = cellViewModel.displayName } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index bda5ad482..b71cb0ef9 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -304,52 +304,52 @@ public final class SnodeAPI { }.defaulting(to: true) let snodePool: Set = SnodeAPI.snodePool - if hasInsufficientSnodes || hasSnodePoolExpired { - if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise } - - let promise: Promise> - if snodePool.count < minSnodePoolCount { - promise = getSnodePoolFromSeedNode() - } - else { - promise = getSnodePoolFromSnode().recover2 { _ in - getSnodePoolFromSeedNode() - } + guard hasInsufficientSnodes || hasSnodePoolExpired else { + return Promise.value(snodePool) + } + + if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise } + + let promise: Promise> + if snodePool.count < minSnodePoolCount { + promise = getSnodePoolFromSeedNode() + } + else { + promise = getSnodePoolFromSnode().recover2 { _ in + getSnodePoolFromSeedNode() } + } + + getSnodePoolPromise = promise + promise.map2 { snodePool -> Set in + guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed } - getSnodePoolPromise = promise - promise.map2 { snodePool -> Set in - guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed } - - return snodePool - } + return snodePool + } + + promise.then2 { snodePool -> Promise> in + let (promise, seal) = Promise>.pending() - promise.then2 { snodePool -> Promise> in - let (promise, seal) = Promise>.pending() - - Storage.shared.writeAsync( - updates: { db in - db[.lastSnodePoolRefreshDate] = now - setSnodePool(to: snodePool, db: db) - }, - completion: { _, _ in - seal.fulfill(snodePool) - } - ) - - return promise - } - promise.done2 { _ in - getSnodePoolPromise = nil - } - promise.catch2 { _ in - getSnodePoolPromise = nil - } + Storage.shared.writeAsync( + updates: { db in + db[.lastSnodePoolRefreshDate] = now + setSnodePool(to: snodePool, db: db) + }, + completion: { _, _ in + seal.fulfill(snodePool) + } + ) return promise } + promise.done2 { _ in + getSnodePoolPromise = nil + } + promise.catch2 { _ in + getSnodePoolPromise = nil + } - return Promise.value(snodePool) + return promise } public static func getSessionID(for onsName: String) -> Promise { diff --git a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift index f8a13021b..7e7fb370d 100644 --- a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -19,6 +19,14 @@ enum _002_SetupStandardJobs: Migration { variant: .syncPushTokens, behaviour: .recurringOnLaunch ).inserted(db) + + // Note: We actually need this job to run both onLaunch and onActive as the logic differs + // slightly and there are cases where a user might not be registered in 'onLaunch' but is + // in 'onActive' (see the `SyncPushTokensJob` for more info) + _ = try Job( + variant: .syncPushTokens, + behaviour: .recurringOnActive + ).inserted(db) } Storage.update(progress: 1, for: self, in: target) // In case this is the last migration diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index 815ea5317..8fbeee36c 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -43,6 +43,7 @@ public enum SNUserDefaults { case lastProfilePictureUpload case lastOpenGroupImageUpdate case lastOpen + case lastGarbageCollection } public enum Double: Swift.String { diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift index 08123780e..a9d57eb16 100644 --- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift +++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift @@ -77,7 +77,8 @@ public final class ProfilePictureView: UIView { useFallbackPicture: ( viewModel.threadVariant == .openGroup && viewModel.openGroupProfilePictureData == nil - ) + ), + showMultiAvatarForClosedGroup: true ) } @@ -87,7 +88,8 @@ public final class ProfilePictureView: UIView { additionalProfile: Profile? = nil, threadVariant: SessionThread.Variant, openGroupProfilePicture: UIImage? = nil, - useFallbackPicture: Bool = false + useFallbackPicture: Bool = false, + showMultiAvatarForClosedGroup: Bool = false ) { AssertIsOnMainThread() guard !useFallbackPicture else { @@ -125,36 +127,41 @@ public final class ProfilePictureView: UIView { ) } - // Calulate the sizes (and set the additional image content + // Calulate the sizes (and set the additional image content) let targetSize: CGFloat - if let additionalProfile: Profile = additionalProfile, openGroupProfilePicture == nil { - if self.size == 40 { - targetSize = 32 - } - else if self.size == Values.largeProfilePictureSize { - targetSize = 56 - } - else { - targetSize = Values.smallProfilePictureSize - } - - imageViewWidthConstraint.constant = targetSize - imageViewHeightConstraint.constant = targetSize - additionalImageViewWidthConstraint.constant = targetSize - additionalImageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = false - additionalImageView.image = getProfilePicture( - of: targetSize, - for: additionalProfile.id, - profile: additionalProfile - ).image - } - else { - targetSize = self.size - imageViewWidthConstraint.constant = targetSize - imageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = true - additionalImageView.image = nil + + switch (threadVariant, showMultiAvatarForClosedGroup) { + case (.closedGroup, true): + if self.size == 40 { + targetSize = 32 + } + else if self.size == Values.largeProfilePictureSize { + targetSize = 56 + } + else { + targetSize = Values.smallProfilePictureSize + } + + imageViewWidthConstraint.constant = targetSize + imageViewHeightConstraint.constant = targetSize + additionalImageViewWidthConstraint.constant = targetSize + additionalImageViewHeightConstraint.constant = targetSize + additionalImageView.isHidden = false + + if let additionalProfile: Profile = additionalProfile { + additionalImageView.image = getProfilePicture( + of: targetSize, + for: additionalProfile.id, + profile: additionalProfile + ).image + } + + default: + targetSize = self.size + imageViewWidthConstraint.constant = targetSize + imageViewHeightConstraint.constant = targetSize + additionalImageView.isHidden = true + additionalImageView.image = nil } // Set the image