Fixed a few bugs found when testing strings

• Fixed an issue where creating a legacy group could be blocked by the legacy PN subscription failing (was part of the synchronous request)
• Fixed an issue where the code would incorrectly use profile data from incoming messages sent from the current user to update it's profile info
• Fixed an issue where saving media would fail silently if the user had rejected the OS permission
• Refactored a little code around profile changes to make things more readable
pull/1018/head
Morgan Pretty 7 months ago
parent 3a1f086d0c
commit e1aedb36da

@ -7673,7 +7673,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 478; CURRENT_PROJECT_VERSION = 482;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -7710,7 +7710,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.7.2; MARKETING_VERSION = 2.7.3;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = ( OTHER_CFLAGS = (
"-fobjc-arc-exceptions", "-fobjc-arc-exceptions",
@ -7751,7 +7751,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 478; CURRENT_PROJECT_VERSION = 482;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -7783,7 +7783,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.7.2; MARKETING_VERSION = 2.7.3;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = ( OTHER_CFLAGS = (
"-DNS_BLOCK_ASSERTIONS=1", "-DNS_BLOCK_ASSERTIONS=1",

@ -298,7 +298,7 @@ extension ConversationVC:
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Permissions.requestLibraryPermissionIfNeeded { [weak self, dependencies = viewModel.dependencies] in Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self, dependencies = viewModel.dependencies] in
DispatchQueue.main.async { DispatchQueue.main.async {
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
threadId: threadId, threadId: threadId,
@ -2326,30 +2326,35 @@ extension ConversationVC:
guard !mediaAttachments.isEmpty else { return } guard !mediaAttachments.isEmpty else { return }
mediaAttachments.forEach { attachment, originalFilePath in Permissions.requestLibraryPermissionIfNeeded(
PHPhotoLibrary.shared().performChanges( isSavingMedia: true,
{ presentingViewController: self
if attachment.isImage || attachment.isAnimated { ) { [weak self] in
PHAssetChangeRequest.creationRequestForAssetFromImage( mediaAttachments.forEach { attachment, originalFilePath in
atFileURL: URL(fileURLWithPath: originalFilePath) PHPhotoLibrary.shared().performChanges(
) {
} if attachment.isImage || attachment.isAnimated {
else if attachment.isVideo { PHAssetChangeRequest.creationRequestForAssetFromImage(
PHAssetChangeRequest.creationRequestForAssetFromVideo( atFileURL: URL(fileURLWithPath: originalFilePath)
atFileURL: URL(fileURLWithPath: originalFilePath) )
) }
} else if attachment.isVideo {
}, PHAssetChangeRequest.creationRequestForAssetFromVideo(
completionHandler: { _, _ in } atFileURL: URL(fileURLWithPath: originalFilePath)
) )
} }
},
// Send a 'media saved' notification if needed completionHandler: { _, _ in }
guard self.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { )
return }
// Send a 'media saved' notification if needed
guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else {
return
}
self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)))
} }
sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)))
} }
func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {

@ -154,7 +154,7 @@ class SendMediaNavigationController: UINavigationController {
} }
private func didTapMediaLibraryModeButton() { private func didTapMediaLibraryModeButton() {
Permissions.requestLibraryPermissionIfNeeded { [weak self] in Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.fadeTo(viewControllers: ((self?.mediaLibraryViewController).map { [$0] } ?? [])) self?.fadeTo(viewControllers: ((self?.mediaLibraryViewController).map { [$0] } ?? []))
} }

@ -116,7 +116,7 @@ struct DisplayNameScreen: View {
// Try to save the user name but ignore the result // Try to save the user name but ignore the result
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: .global(qos: .default), queue: .global(qos: .default),
profileName: displayName, displayNameUpdate: .currentUserUpdate(displayName),
using: dependencies using: dependencies
) )

@ -20,11 +20,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler( private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler(
onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) }, onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) },
onImageDataPicked: { [weak self] resultImageData in onImageDataPicked: { [weak self] resultImageData in
guard let oldDisplayName: String = self?.oldDisplayName else { return }
self?.updatedProfilePictureSelected( self?.updatedProfilePictureSelected(
name: oldDisplayName, displayPictureUpdate: .currentUserUploadImageData(resultImageData)
avatarUpdate: .uploadImageData(resultImageData)
) )
} }
) )
@ -205,10 +202,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
self?.setIsEditing(false) self?.setIsEditing(false)
self?.oldDisplayName = updatedNickname self?.oldDisplayName = updatedNickname
self?.updateProfile( self?.updateProfile(displayNameUpdate: .currentUserUpdate(updatedNickname))
name: updatedNickname,
avatarUpdate: .none
)
} }
] ]
} }
@ -527,8 +521,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
onConfirm: { modal in modal.close() }, onConfirm: { modal in modal.close() },
onCancel: { [weak self] modal in onCancel: { [weak self] modal in
self?.updateProfile( self?.updateProfile(
name: existingDisplayName, displayPictureUpdate: .currentUserRemove,
avatarUpdate: .remove,
onComplete: { [weak modal] in modal?.close() } onComplete: { [weak modal] in modal?.close() }
) )
}, },
@ -544,7 +537,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
self.transitionToScreen(modal, transitionType: .present) self.transitionToScreen(modal, transitionType: .present)
} }
fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) { fileprivate func updatedProfilePictureSelected(displayPictureUpdate: ProfileManager.DisplayPictureUpdate) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return } guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
self.editProfilePictureModal?.updateContent( self.editProfilePictureModal?.updateContent(
@ -552,8 +545,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
body: .image( body: .image(
placeholderData: UIImage(named: "profile_placeholder")?.pngData(), placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
valueData: { valueData: {
switch avatarUpdate { switch displayPictureUpdate {
case .uploadImageData(let imageData): return imageData case .currentUserUploadImageData(let imageData): return imageData
default: return nil default: return nil
} }
}(), }(),
@ -568,8 +561,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
confirmEnabled: true, confirmEnabled: true,
onConfirm: { [weak self] modal in onConfirm: { [weak self] modal in
self?.updateProfile( self?.updateProfile(
name: name, displayPictureUpdate: displayPictureUpdate,
avatarUpdate: avatarUpdate,
onComplete: { [weak modal] in modal?.close() } onComplete: { [weak modal] in modal?.close() }
) )
} }
@ -578,7 +570,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
} }
private func showPhotoLibraryForAvatar() { private func showPhotoLibraryForAvatar() {
Permissions.requestLibraryPermissionIfNeeded { [weak self] in Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
let picker: UIImagePickerController = UIImagePickerController() let picker: UIImagePickerController = UIImagePickerController()
picker.sourceType = .photoLibrary picker.sourceType = .photoLibrary
@ -591,15 +583,15 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
} }
fileprivate func updateProfile( fileprivate func updateProfile(
name: String, displayNameUpdate: ProfileManager.DisplayNameUpdate = .none,
avatarUpdate: ProfileManager.AvatarUpdate, displayPictureUpdate: ProfileManager.DisplayPictureUpdate = .none,
onComplete: (() -> ())? = nil onComplete: (() -> ())? = nil
) { ) {
let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: .global(qos: .default), queue: .global(qos: .default),
profileName: name, displayNameUpdate: displayNameUpdate,
avatarUpdate: avatarUpdate, displayPictureUpdate: displayPictureUpdate,
success: { db in success: { db in
// Wait for the database transaction to complete before updating the UI // Wait for the database transaction to complete before updating the UI
db.afterNextTransactionNested { _ in db.afterNextTransactionNested { _ in
@ -614,8 +606,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
DispatchQueue.main.async { DispatchQueue.main.async {
modalActivityIndicator.dismiss { modalActivityIndicator.dismiss {
let title: String = { let title: String = {
switch (avatarUpdate, error) { switch (displayPictureUpdate, error) {
case (.remove, _): return "update_profile_modal_remove_error_title".localized() case (.currentUserRemove, _): return "update_profile_modal_remove_error_title".localized()
case (_, .avatarUploadMaxFileSizeExceeded): case (_, .avatarUploadMaxFileSizeExceeded):
return "update_profile_modal_max_size_error_title".localized() return "update_profile_modal_max_size_error_title".localized()
@ -623,8 +615,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
} }
}() }()
let message: String? = { let message: String? = {
switch (avatarUpdate, error) { switch (displayPictureUpdate, error) {
case (.remove, _): return nil case (.currentUserRemove, _): return nil
case (_, .avatarUploadMaxFileSizeExceeded): case (_, .avatarUploadMaxFileSizeExceeded):
return "update_profile_modal_max_size_error_message".localized() return "update_profile_modal_max_size_error_message".localized()

@ -96,12 +96,14 @@ public enum Permissions {
} }
public static func requestLibraryPermissionIfNeeded( public static func requestLibraryPermissionIfNeeded(
isSavingMedia: Bool,
presentingViewController: UIViewController? = nil, presentingViewController: UIViewController? = nil,
onAuthorized: @escaping () -> Void onAuthorized: @escaping () -> Void
) { ) {
let authorizationStatus: PHAuthorizationStatus let authorizationStatus: PHAuthorizationStatus
if #available(iOS 14, *) { if #available(iOS 14, *) {
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) let targetPermission: PHAccessLevel = (isSavingMedia ? .addOnly : .readWrite)
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: targetPermission)
if authorizationStatus == .notDetermined { if authorizationStatus == .notDetermined {
// When the user chooses to select photos (which is the .limit status), // When the user chooses to select photos (which is the .limit status),
// the PHPhotoUI will present the picker view on the top of the front view. // the PHPhotoUI will present the picker view on the top of the front view.
@ -113,7 +115,7 @@ public enum Permissions {
// from showing when we request the photo library permission. // from showing when we request the photo library permission.
SessionEnvironment.shared?.isRequestingPermission = true SessionEnvironment.shared?.isRequestingPermission = true
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in PHPhotoLibrary.requestAuthorization(for: targetPermission) { status in
SessionEnvironment.shared?.isRequestingPermission = false SessionEnvironment.shared?.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) { if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized() onAuthorized()

@ -48,8 +48,7 @@ public enum UpdateProfilePictureJob: JobExecutor {
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: queue, queue: queue,
profileName: profile.name, displayPictureUpdate: (profilePictureData.map { .currentUserUploadImageData($0) } ?? .none),
avatarUpdate: (profilePictureData.map { .uploadImageData($0) } ?? .none),
success: { db in success: { db in
// Need to call the 'success' closure asynchronously on the queue to prevent a reentrancy // Need to call the 'success' closure asynchronously on the queue to prevent a reentrancy
// issue as it will write to the database and this closure is already called within // issue as it will write to the database and this closure is already called within

@ -42,11 +42,11 @@ internal extension LibSession {
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
db, db,
publicKey: userPublicKey, publicKey: userPublicKey,
name: profileName, displayNameUpdate: .currentUserUpdate(profileName),
avatarUpdate: { displayPictureUpdate: {
guard let profilePictureUrl: String = profilePictureUrl else { return .remove } guard let profilePictureUrl: String = profilePictureUrl else { return .currentUserRemove }
return .updateTo( return .currentUserUpdateTo(
url: profilePictureUrl, url: profilePictureUrl,
key: Data( key: Data(
libSessionVal: profilePic.key, libSessionVal: profilePic.key,

@ -241,8 +241,8 @@ extension MessageReceiver {
// Resubscribe for group push notifications // Resubscribe for group push notifications
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
PushNotificationAPI (try? PushNotificationAPI
.subscribeToLegacyGroups( .preparedSubscribeToLegacyGroups(
currentUserPublicKey: currentUserPublicKey, currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: try ClosedGroup legacyGroupIds: try ClosedGroup
.select(.threadId) .select(.threadId)
@ -253,8 +253,11 @@ extension MessageReceiver {
) )
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchSet(db) .fetchSet(db)
.inserting(groupPublicKey) // Insert the new key just to be sure .inserting(groupPublicKey), // Insert the new key just to be sure
) using: dependencies
))?
.send(using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete() .sinkUntilComplete()
} }

@ -31,14 +31,14 @@ extension MessageReceiver {
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
db, db,
publicKey: senderId, publicKey: senderId,
name: profile.displayName, displayNameUpdate: .contactUpdate(profile.displayName),
avatarUpdate: { displayPictureUpdate: {
guard guard
let profilePictureUrl: String = profile.profilePictureUrl, let profilePictureUrl: String = profile.profilePictureUrl,
let profileKey: Data = profile.profileKey let profileKey: Data = profile.profileKey
else { return .none } else { return .none }
return .updateTo( return .contactUpdateTo(
url: profilePictureUrl, url: profilePictureUrl,
key: profileKey, key: profileKey,
fileName: nil fileName: nil

@ -30,20 +30,20 @@ extension MessageReceiver {
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
db, db,
publicKey: sender, publicKey: sender,
name: profile.displayName, displayNameUpdate: .contactUpdate(profile.displayName),
blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, displayPictureUpdate: {
avatarUpdate: {
guard guard
let profilePictureUrl: String = profile.profilePictureUrl, let profilePictureUrl: String = profile.profilePictureUrl,
let profileKey: Data = profile.profileKey let profileKey: Data = profile.profileKey
else { return .remove } else { return .contactRemove }
return .updateTo( return .contactUpdateTo(
url: profilePictureUrl, url: profilePictureUrl,
key: profileKey, key: profileKey,
fileName: nil fileName: nil
) )
}(), }(),
blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests,
sentTimestamp: messageSentTimestamp, sentTimestamp: messageSentTimestamp,
using: dependencies using: dependencies
) )

@ -7,6 +7,11 @@ import SessionUtilitiesKit
import SessionSnodeKit import SessionSnodeKit
extension MessageSender { extension MessageSender {
typealias CreateGroupDatabaseResult = (
SessionThread,
[MessageSender.PreparedSendData],
Network.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>?
)
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:]) public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
public static func createClosedGroup( public static func createClosedGroup(
@ -15,7 +20,7 @@ extension MessageSender {
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<SessionThread, Error> { ) -> AnyPublisher<SessionThread, Error> {
dependencies.storage dependencies.storage
.writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData], Set<String>) in .writePublisher { db -> CreateGroupDatabaseResult in
// Generate the group's two keys // Generate the group's two keys
guard guard
let groupKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()), let groupKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()),
@ -108,42 +113,54 @@ extension MessageSender {
using: dependencies using: dependencies
) )
} }
let allActiveLegacyGroupIds: Set<String> = try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == userPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db)
.inserting(groupPublicKey) // Insert the new key just to be sure
return (userPublicKey, thread, memberSendData, allActiveLegacyGroupIds) // Prepare the notification subscription
var preparedNotificationSubscription: Network.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>?
if let token: String = dependencies.standardUserDefaults[.deviceToken] {
preparedNotificationSubscription = try? PushNotificationAPI
.preparedSubscribeToLegacyGroups(
token: token,
currentUserPublicKey: userPublicKey,
legacyGroupIds: try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == userPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db)
.inserting(groupPublicKey), // Insert the new key just to be sure,
using: dependencies
)
}
return (thread, memberSendData, preparedNotificationSubscription)
} }
.flatMap { userPublicKey, thread, memberSendData, allActiveLegacyGroupIds in .flatMap { thread, memberSendData, preparedNotificationSubscription -> AnyPublisher<(SessionThread, Network.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>?), Error> in
Publishers Publishers
.MergeMany( .MergeMany(
// Send a closed group update message to all members individually // Send a closed group update message to all members individually
memberSendData memberSendData.map { MessageSender.sendImmediate(data: $0, using: dependencies) }
.map { MessageSender.sendImmediate(data: $0, using: dependencies) }
.appending(
// Resubscribe to all legacy groups
PushNotificationAPI.subscribeToLegacyGroups(
currentUserPublicKey: userPublicKey,
legacyGroupIds: allActiveLegacyGroupIds
)
)
) )
.collect() .collect()
.map { _ in thread } .map { _ in (thread, preparedNotificationSubscription) }
.eraseToAnyPublisher()
} }
.handleEvents( .handleEvents(
receiveOutput: { thread in receiveOutput: { thread, preparedNotificationSubscription in
// Subscribe for push notifications (if PNs are enabled)
preparedNotificationSubscription?
.send(using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete()
// Start polling // Start polling
ClosedGroupPoller.shared.startIfNeeded(for: thread.id, using: dependencies) ClosedGroupPoller.shared.startIfNeeded(for: thread.id, using: dependencies)
} }
) )
.map { thread, _ -> SessionThread in thread }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

@ -2,7 +2,7 @@
import Foundation import Foundation
extension PushNotificationAPI { public extension PushNotificationAPI {
struct LegacyPushServerResponse: Codable { struct LegacyPushServerResponse: Codable {
let code: Int let code: Int
let message: String? let message: String?

@ -31,6 +31,11 @@ public enum PushNotificationAPI {
isForcedUpdate: Bool, isForcedUpdate: Bool,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> { ) -> AnyPublisher<Void, Error> {
typealias SubscribeAllPreparedRequests = (
SubscribeRequest,
String,
Network.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>?
)
let hexEncodedToken: String = token.toHexString() let hexEncodedToken: String = token.toHexString()
let oldToken: String? = dependencies.standardUserDefaults[.deviceToken] let oldToken: String? = dependencies.standardUserDefaults[.deviceToken]
let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload] let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload]
@ -52,7 +57,7 @@ public enum PushNotificationAPI {
// TODO: Need to generate requests for each updated group as well // TODO: Need to generate requests for each updated group as well
return dependencies.storage return dependencies.storage
.readPublisher(using: dependencies) { db -> (SubscribeRequest, String, Set<String>) in .readPublisher(using: dependencies) { db -> SubscribeAllPreparedRequests in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair throw SnodeAPIError.noKeyPair
} }
@ -74,22 +79,30 @@ public enum PushNotificationAPI {
ed25519PublicKey: userED25519KeyPair.publicKey, ed25519PublicKey: userED25519KeyPair.publicKey,
ed25519SecretKey: userED25519KeyPair.secretKey ed25519SecretKey: userED25519KeyPair.secretKey
) )
let preparedLegacyGroupRequest = try PushNotificationAPI
.preparedSubscribeToLegacyGroups(
forced: true,
token: hexEncodedToken,
currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db),
using: dependencies
)
return ( return (
request, request,
currentUserPublicKey, currentUserPublicKey,
try ClosedGroup preparedLegacyGroupRequest
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db)
) )
} }
.tryFlatMap { request, currentUserPublicKey, legacyGroupIds -> AnyPublisher<Void, Error> in .tryFlatMap { request, currentUserPublicKey, legacyGroupRequest -> AnyPublisher<Void, Error> in
Publishers Publishers
.MergeMany( .MergeMany(
[ [
@ -126,14 +139,12 @@ public enum PushNotificationAPI {
.map { _ in () } .map { _ in () }
.eraseToAnyPublisher(), .eraseToAnyPublisher(),
// FIXME: Remove this once legacy groups are deprecated // FIXME: Remove this once legacy groups are deprecated
PushNotificationAPI.subscribeToLegacyGroups( legacyGroupRequest?
forced: true, .send(using: dependencies)
token: hexEncodedToken, .map { _, _ in () }
currentUserPublicKey: currentUserPublicKey, .eraseToAnyPublisher()
legacyGroupIds: legacyGroupIds,
using: dependencies
)
] ]
.compactMap { $0 }
) )
.collect() .collect()
.map { _ in () } .map { _ in () }
@ -281,61 +292,52 @@ public enum PushNotificationAPI {
// MARK: - Legacy Groups // MARK: - Legacy Groups
// FIXME: Remove this once legacy groups are deprecated // FIXME: Remove this once legacy groups are deprecated
public static func subscribeToLegacyGroups( public static func preparedSubscribeToLegacyGroups(
forced: Bool = false, forced: Bool = false,
token: String? = nil, token: String? = nil,
currentUserPublicKey: String, currentUserPublicKey: String,
legacyGroupIds: Set<String>, legacyGroupIds: Set<String>,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies
) -> AnyPublisher<Void, Error> { ) throws -> Network.PreparedRequest<LegacyPushServerResponse>? {
let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs] let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs]
// Only continue if PNs are enabled and we have a device token // Only continue if PNs are enabled and we have a device token
guard guard
!legacyGroupIds.isEmpty,
(forced || isUsingFullAPNs), (forced || isUsingFullAPNs),
let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken]) let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken])
else { else { return nil }
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
do { return try PushNotificationAPI
return try PushNotificationAPI .prepareRequest(
.prepareRequest( request: Request(
request: Request( method: .post,
method: .post, endpoint: .legacyGroupsOnlySubscribe,
endpoint: .legacyGroupsOnlySubscribe, body: LegacyGroupOnlyRequest(
body: LegacyGroupOnlyRequest( token: deviceToken,
token: deviceToken, pubKey: currentUserPublicKey,
pubKey: currentUserPublicKey, device: "ios",
device: "ios", legacyGroupPublicKeys: legacyGroupIds
legacyGroupPublicKeys: legacyGroupIds
),
using: dependencies
), ),
responseType: LegacyPushServerResponse.self,
using: dependencies using: dependencies
) ),
.send(using: dependencies) responseType: LegacyPushServerResponse.self,
.retry(maxRetryCount, using: dependencies) retryCount: PushNotificationAPI.maxRetryCount,
.handleEvents( using: dependencies
receiveOutput: { _, response in )
guard response.code != 0 else { .handleEvents(
return SNLog("Couldn't subscribe for legacy groups due to error: \(response.message ?? "nil").") receiveOutput: { _, response in
} guard response.code != 0 else {
}, return Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups due to error: \(response.message ?? "nil").")
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Couldn't subscribe for legacy groups.")
}
} }
) },
.map { _ in () } receiveCompletion: { result in
.eraseToAnyPublisher() switch result {
} case .finished: break
catch { return Fail(error: error).eraseToAnyPublisher() } case .failure: Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups.")
}
}
)
} }
// FIXME: Remove this once legacy groups are deprecated // FIXME: Remove this once legacy groups are deprecated

@ -7,11 +7,21 @@ import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
public struct ProfileManager { public struct ProfileManager {
public enum AvatarUpdate { public enum DisplayNameUpdate {
case none case none
case remove case contactUpdate(String?)
case uploadImageData(Data) case currentUserUpdate(String?)
case updateTo(url: String, key: Data, fileName: String?) }
public enum DisplayPictureUpdate {
case none
case contactRemove
case contactUpdateTo(url: String, key: Data, fileName: String?)
case currentUserRemove
case currentUserUploadImageData(Data)
case currentUserUpdateTo(url: String, key: Data, fileName: String?)
} }
// The max bytes for a user's profile name, encoded in UTF8. // The max bytes for a user's profile name, encoded in UTF8.
@ -281,24 +291,27 @@ public struct ProfileManager {
public static func updateLocal( public static func updateLocal(
queue: DispatchQueue, queue: DispatchQueue,
profileName: String, displayNameUpdate: DisplayNameUpdate = .none,
avatarUpdate: AvatarUpdate = .none, displayPictureUpdate: DisplayPictureUpdate = .none,
success: ((Database) throws -> ())? = nil, success: ((Database) throws -> ())? = nil,
failure: ((ProfileManagerError) -> ())? = nil, failure: ((ProfileManagerError) -> ())? = nil,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) { ) {
let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
let isRemovingAvatar: Bool = { let isRemovingDisplayPicture: Bool = {
switch avatarUpdate { switch displayPictureUpdate {
case .remove: return true case .currentUserRemove: return true
default: return false default: return false
} }
}() }()
switch avatarUpdate { switch displayPictureUpdate {
case .none, .remove, .updateTo: case .contactRemove, .contactUpdateTo:
failure?(ProfileManagerError.invalidCall)
case .none, .currentUserRemove, .currentUserUpdateTo:
dependencies.storage.writeAsync { db in dependencies.storage.writeAsync { db in
if isRemovingAvatar { if isRemovingDisplayPicture {
let existingProfileUrl: String? = try Profile let existingProfileUrl: String? = try Profile
.filter(id: userPublicKey) .filter(id: userPublicKey)
.select(.profilePictureUrl) .select(.profilePictureUrl)
@ -324,8 +337,8 @@ public struct ProfileManager {
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
db, db,
publicKey: userPublicKey, publicKey: userPublicKey,
name: profileName, displayNameUpdate: displayNameUpdate,
avatarUpdate: avatarUpdate, displayPictureUpdate: displayPictureUpdate,
sentTimestamp: dependencies.dateNow.timeIntervalSince1970, sentTimestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies using: dependencies
) )
@ -334,7 +347,7 @@ public struct ProfileManager {
try success?(db) try success?(db)
} }
case .uploadImageData(let data): case .currentUserUploadImageData(let data):
prepareAndUploadAvatarImage( prepareAndUploadAvatarImage(
queue: queue, queue: queue,
imageData: data, imageData: data,
@ -343,8 +356,8 @@ public struct ProfileManager {
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
db, db,
publicKey: userPublicKey, publicKey: userPublicKey,
name: profileName, displayNameUpdate: displayNameUpdate,
avatarUpdate: .updateTo(url: downloadUrl, key: newProfileKey, fileName: fileName), displayPictureUpdate: .currentUserUpdateTo(url: downloadUrl, key: newProfileKey, fileName: fileName),
sentTimestamp: dependencies.dateNow.timeIntervalSince1970, sentTimestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies using: dependencies
) )
@ -494,9 +507,9 @@ public struct ProfileManager {
public static func updateProfileIfNeeded( public static func updateProfileIfNeeded(
_ db: Database, _ db: Database,
publicKey: String, publicKey: String,
name: String?, displayNameUpdate: DisplayNameUpdate = .none,
displayPictureUpdate: DisplayPictureUpdate,
blocksCommunityMessageRequests: Bool? = nil, blocksCommunityMessageRequests: Bool? = nil,
avatarUpdate: AvatarUpdate,
sentTimestamp: TimeInterval, sentTimestamp: TimeInterval,
calledFromConfigHandling: Bool = false, calledFromConfigHandling: Bool = false,
using dependencies: Dependencies using dependencies: Dependencies
@ -506,15 +519,21 @@ public struct ProfileManager {
var profileChanges: [ConfigColumnAssignment] = [] var profileChanges: [ConfigColumnAssignment] = []
// Name // Name
if let name: String = name, !name.isEmpty, name != profile.name { // FIXME: This 'lastNameUpdate' approach is buggy - we should have a timestamp on the ConvoInfoVolatile
if sentTimestamp > (profile.lastNameUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { switch (displayNameUpdate, isCurrentUser, (sentTimestamp > (profile.lastNameUpdate ?? 0))) {
case (.none, _, _): break
case (.currentUserUpdate(let name), true, _), (.contactUpdate(let name), false, true):
guard let name: String = name, !name.isEmpty, name != profile.name else { break }
profileChanges.append(Profile.Columns.name.set(to: name)) profileChanges.append(Profile.Columns.name.set(to: name))
profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp))
}
// Don't want profiles in messages to modify the current users profile info so ignore those cases
default: break
} }
// Blocks community message requets flag // Blocks community message requets flag (only update for other users)
if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > (profile.lastBlocksCommunityMessageRequests ?? 0) { if !isCurrentUser, let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > (profile.lastBlocksCommunityMessageRequests ?? 0) {
profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests))
profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp))
} }
@ -522,43 +541,49 @@ public struct ProfileManager {
// Profile picture & profile key // Profile picture & profile key
var avatarNeedsDownload: Bool = false var avatarNeedsDownload: Bool = false
var targetAvatarUrl: String? = nil var targetAvatarUrl: String? = nil
let shouldUpdateAvatar: Bool = (
(!isCurrentUser && (sentTimestamp > (profile.lastProfilePictureUpdate ?? 0))) || // Update other users
(isCurrentUser && calledFromConfigHandling) // Only update the current user via config messages
)
if sentTimestamp > (profile.lastProfilePictureUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { switch (displayPictureUpdate, isCurrentUser) {
switch avatarUpdate { case (.none, _): break
case .none: break case (.currentUserUploadImageData, _): preconditionFailure("Invalid options for this function")
case .uploadImageData: preconditionFailure("Invalid options for this function")
case (.contactRemove, false), (.currentUserRemove, true):
case .remove: profileChanges.append(Profile.Columns.profilePictureUrl.set(to: nil))
profileChanges.append(Profile.Columns.profilePictureUrl.set(to: nil)) profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: nil))
profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: nil)) profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil))
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil)) profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp))
profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp))
case (.contactUpdateTo(let url, let key, let fileName), false),
case .updateTo(let url, let key, let fileName): (.currentUserUpdateTo(let url, let key, let fileName), true):
if url != profile.profilePictureUrl { if url != profile.profilePictureUrl {
profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url)) profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url))
avatarNeedsDownload = true avatarNeedsDownload = true
targetAvatarUrl = url targetAvatarUrl = url
} }
if key != profile.profileEncryptionKey && key.count == ProfileManager.avatarAES256KeyByteLength { if key != profile.profileEncryptionKey && key.count == ProfileManager.avatarAES256KeyByteLength {
profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key)) profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key))
} }
// Profile filename (this isn't synchronized between devices) // Profile filename (this isn't synchronized between devices)
if let fileName: String = fileName { if let fileName: String = fileName {
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName)) profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName))
// If we have already downloaded the image then no need to download it again
avatarNeedsDownload = (
avatarNeedsDownload &&
!ProfileManager.hasProfileImageData(with: fileName)
)
}
// Update the 'lastProfilePictureUpdate' timestamp for either external or local changes // If we have already downloaded the image then no need to download it again
profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) avatarNeedsDownload = (
} avatarNeedsDownload &&
!ProfileManager.hasProfileImageData(with: fileName)
)
}
// Update the 'lastProfilePictureUpdate' timestamp for either external or local changes
profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp))
// Don't want profiles in messages to modify the current users profile info so ignore those cases
default: break
} }
// Persist any changes // Persist any changes

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation import Foundation
@ -17,7 +19,7 @@ public enum ProfileManagerError: LocalizedError {
case .avatarEncryptionFailed: return "Avatar encryption failed." case .avatarEncryptionFailed: return "Avatar encryption failed."
case .avatarUploadFailed: return "Avatar upload failed." case .avatarUploadFailed: return "Avatar upload failed."
case .avatarUploadMaxFileSizeExceeded: return "Maximum file size exceeded." case .avatarUploadMaxFileSizeExceeded: return "Maximum file size exceeded."
case .invalidCall: return "Attempted to remove avatar using the wrong method." case .invalidCall: return "Attempted to modify profile using the wrong method."
} }
} }
} }

Loading…
Cancel
Save