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