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
This commit is contained in:
Morgan Pretty 2022-07-04 17:36:48 +10:00
parent fe2e2510bb
commit 34fea96db3
18 changed files with 316 additions and 140 deletions

View File

@ -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();

View File

@ -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()
}
}

View File

@ -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

View File

@ -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
}()

View File

@ -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

View File

@ -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<String> = 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<String> = 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(())

View File

@ -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,

View File

@ -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)
}
}
}
}

View File

@ -113,41 +113,49 @@ public final class PushNotificationAPI : NSObject {
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
let promise: Promise<Void> = 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.")
var promises: [Promise<Void>] = []
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
}
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
}
}
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<Void> in
performOperation(.subscribe, for: closedGroupPublicKey, publicKey: publicKey)
}
}
)
return promise
return when(fulfilled: promises)
}
@objc(registerWithToken:hexEncodedPublicKey:isForcedUpdate:)

View File

@ -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<ClosedGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = 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<ClosedGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = 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

View File

@ -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

View File

@ -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") {

View File

@ -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)

View File

@ -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
}

View File

@ -304,52 +304,52 @@ public final class SnodeAPI {
}.defaulting(to: true)
let snodePool: Set<Snode> = SnodeAPI.snodePool
if hasInsufficientSnodes || hasSnodePoolExpired {
if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise }
let promise: Promise<Set<Snode>>
if snodePool.count < minSnodePoolCount {
promise = getSnodePoolFromSeedNode()
guard hasInsufficientSnodes || hasSnodePoolExpired else {
return Promise.value(snodePool)
}
if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise }
let promise: Promise<Set<Snode>>
if snodePool.count < minSnodePoolCount {
promise = getSnodePoolFromSeedNode()
}
else {
promise = getSnodePoolFromSnode().recover2 { _ in
getSnodePoolFromSeedNode()
}
else {
promise = getSnodePoolFromSnode().recover2 { _ in
getSnodePoolFromSeedNode()
}
getSnodePoolPromise = promise
promise.map2 { snodePool -> Set<Snode> in
guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed }
return snodePool
}
promise.then2 { snodePool -> Promise<Set<Snode>> in
let (promise, seal) = Promise<Set<Snode>>.pending()
Storage.shared.writeAsync(
updates: { db in
db[.lastSnodePoolRefreshDate] = now
setSnodePool(to: snodePool, db: db)
},
completion: { _, _ in
seal.fulfill(snodePool)
}
}
getSnodePoolPromise = promise
promise.map2 { snodePool -> Set<Snode> in
guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed }
return snodePool
}
promise.then2 { snodePool -> Promise<Set<Snode>> in
let (promise, seal) = Promise<Set<Snode>>.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
}
)
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<String> {

View File

@ -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

View File

@ -43,6 +43,7 @@ public enum SNUserDefaults {
case lastProfilePictureUpload
case lastOpenGroupImageUpdate
case lastOpen
case lastGarbageCollection
}
public enum Double: Swift.String {

View File

@ -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