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:
parent
fe2e2510bb
commit
34fea96db3
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -43,6 +43,7 @@ public enum SNUserDefaults {
|
|||
case lastProfilePictureUpload
|
||||
case lastOpenGroupImageUpdate
|
||||
case lastOpen
|
||||
case lastGarbageCollection
|
||||
}
|
||||
|
||||
public enum Double: Swift.String {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue