diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 8017c7617..cd5373c3f 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -348,7 +348,6 @@ [avatarView autoSetDimension:ALDimensionHeight toSize:kLargeAvatarSize]; avatarView.contactID = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; - /** if (!localProfileAvatarImage) { UIImage *cameraImage = [UIImage imageNamed:@"settings-avatar-camera"]; UIImageView *cameraImageView = [[UIImageView alloc] initWithImage:cameraImage]; @@ -356,7 +355,6 @@ [cameraImageView autoPinTrailingToEdgeOfView:avatarView]; [cameraImageView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:avatarView]; } - */ UIView *nameView = [UIView containerView]; [cell.contentView addSubview:nameView]; diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index cae7e38c6..63d9081b6 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -154,12 +154,10 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarRowTapped:)]]; avatarRow.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"avatarRow"); -// Loki: Disable setting the avatar -// [rows addObject:avatarRow]; + [rows addObject:avatarRow]; UILabel *avatarLabel = [UILabel new]; - avatarLabel.text = NSLocalizedString( - @"PROFILE_VIEW_PROFILE_AVATAR_FIELD", @"Label for the profile avatar field of the profile view."); + avatarLabel.text = NSLocalizedString(@"Profile Picture", @""); avatarLabel.textColor = Theme.primaryColor; avatarLabel.font = [UIFont ows_mediumFontWithSize:fontSizePoints]; [avatarRow addSubview:avatarLabel]; @@ -601,8 +599,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat - (nullable NSString *)avatarActionSheetTitle { - return NSLocalizedString( - @"PROFILE_VIEW_AVATAR_ACTIONSHEET_TITLE", @"Action Sheet title prompting the user for a profile avatar"); + return NSLocalizedString(@"Set Profile Picture", @""); } - (void)avatarDidChange:(UIImage *)image @@ -626,7 +623,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat - (NSString *)clearAvatarActionLabel { - return NSLocalizedString(@"PROFILE_VIEW_CLEAR_AVATAR", @"Label for action that clear's the user's profile avatar"); + return NSLocalizedString(@"Clear Profile Picture", @""); } - (void)clearAvatar diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 74254155f..dc96a2d99 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2650,3 +2650,6 @@ "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters" = "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters"; "Multi Device Limit Reached" = "Multi Device Limit Reached"; "It's currently not allowed to link more than one device." = "It's currently not allowed to link more than one device."; +"Profile Picture" = "Profile Picture"; +"Set Profile Picture" = "Set Profile Picture"; +"Clear Profile Picture" = "Clear Profile Picture"; diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 9b26be0b2..5893f4ec3 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -294,11 +294,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSUserProfile *userProfile = self.localUserProfile; OWSAssertDebug(userProfile); - // Loki: We don't support avatar uploads yet - OWSLogVerbose(@"Updating local profile on service with no avatar."); - tryToUpdateService(nil, nil); - - /* ========== Original code =============== if (avatarImage) { // If we have a new avatar image, we must first: // @@ -343,7 +338,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSLogVerbose(@"Updating local profile on service with no avatar."); tryToUpdateService(nil, nil); } - */ } - (void)writeAvatarToDisk:(UIImage *)avatar @@ -402,6 +396,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSAssertDebug(failureBlock); OWSAssertDebug(avatarData == nil || avatarData.length > 0); + /* // We want to clear the local user's profile avatar as soon as // we request the upload form, since that request clears our // avatar on the service. @@ -412,8 +407,26 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSUserProfile *userProfile = self.localUserProfile; [userProfile updateWithAvatarUrlPath:nil avatarFileName:nil dbConnection:self.dbConnection completion:nil]; }; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + */ + +// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + [[LKStorageAPI setProfilePicture:avatarData] + .thenOn(dispatch_get_main_queue(), ^(NSString *url) { + successBlock(url); + }) + .catchOn(dispatch_get_main_queue(), ^(id result) { + // There appears to be a bug in PromiseKit that sometimes causes catchOn + // to be invoked with the fulfilled promise's value as the error. The below + // is a quick and dirty workaround. + if ([result isKindOfClass:NSString.class]) { + successBlock(result); + } else { + failureBlock(result); + } + }) retainUntilComplete]; + + /* // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html TSRequest *formRequest = [OWSRequestFactory profileAvatarUploadFormRequest]; @@ -526,7 +539,8 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSLogError(@"Failed to get profile avatar upload form: %@", error); return failureBlock(error); }]; - }); + */ +// }); } - (void)updateServiceWithProfileName:(nullable NSString *)localProfileName diff --git a/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift b/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift index f0a03aec6..74a27d5d9 100644 --- a/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift @@ -131,4 +131,124 @@ public final class LokiStorageAPI : LokiDotNetAPI { public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> AnyPromise { return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKey)) } + + // MARK: Attachments (Public API) + public static func uploadAttachment(_ attachment: TSAttachmentStream, attachmentID: String) -> Promise { + return Promise() { seal in + getAuthToken(for: server).done { token in + // Encrypt the attachment + guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else { + print("[Loki] Couldn't read attachment data from disk.") + return seal.reject(Error.generic) + } + var encryptionKey = NSData() + var digest = NSData() + guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else { + print("[Loki] Couldn't encrypt attachment.") + return seal.reject(Error.encryptionFailed) + } + attachment.encryptionKey = encryptionKey as Data + attachment.digest = digest as Data + // Create the request + let url = "\(server)/files" + let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] + var error: NSError? + var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in + formData.appendPart(withFileData: encryptedAttachmentData, name: "content", fileName: UUID().uuidString, mimeType: "application/binary") + }, error: &error) + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let error = error { + print("[Loki] Couldn't upload attachment due to error: \(error).") + throw error + } + // Send the request + let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: { rawProgress in + // Broadcast progress updates + let progress = max(0.1, rawProgress.fractionCompleted) + let userInfo: [String:Any] = [ kAttachmentUploadProgressKey : progress, kAttachmentUploadAttachmentIDKey : attachmentID ] + DispatchQueue.main.async { + NotificationCenter.default.post(name: .attachmentUploadProgress, object: nil, userInfo: userInfo) + } + }, completionHandler: { response, responseObject, error in + if let error = error { + print("[Loki] Couldn't upload attachment due to error: \(error).") + return seal.reject(error) + } + let statusCode = (response as! HTTPURLResponse).statusCode + let isSuccessful = (200...299) ~= statusCode + guard isSuccessful else { + print("[Loki] Couldn't upload attachment.") + return seal.reject(Error.generic) + } + // Parse the server ID & download URL + guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else { + print("[Loki] Couldn't parse attachment from: \(responseObject).") + return seal.reject(Error.parsingFailed) + } + // Update the attachment + attachment.serverId = serverID + attachment.isUploaded = true + attachment.downloadURL = downloadURL + attachment.save() + return seal.fulfill(()) + }) + task.resume() + }.catch { error in + print("[Loki] Couldn't upload attachment.") + seal.reject(error) + } + } + } + + // MARK: Attachments (Public Obj-C API) + @objc(uploadAttachment:withID:) + public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, attachmentID: String) -> AnyPromise { + return AnyPromise.from(uploadAttachment(attachment, attachmentID: attachmentID)) + } + + // MARK: Profile Pictures (Public API) + public static func setProfilePicture(_ profilePicture: Data) -> Promise { + return Promise() { seal in + getAuthToken(for: server).done { token in + let url = "\(server)/files" + let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] + var error: NSError? + var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in + formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary") + }, error: &error) + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let error = error { + print("[Loki] Couldn't upload profile picture due to error: \(error).") + throw error + } + let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: nil, completionHandler: { response, responseObject, error in + if let error = error { + print("[Loki] Couldn't upload profile picture due to error: \(error).") + return seal.reject(error) + } + let statusCode = (response as! HTTPURLResponse).statusCode + let isSuccessful = (200...299) ~= statusCode + guard isSuccessful else { + print("[Loki] Couldn't upload profile picture.") + return seal.reject(Error.generic) + } + guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { + print("[Loki] Couldn't parse profile picture from: \(responseObject).") + return seal.reject(Error.parsingFailed) + } + return seal.fulfill(downloadURL) + }) + task.resume() + }.catch { error in + print("[Loki] Couldn't upload profile picture.") + seal.reject(error) + } + } + } + + // MARK: Profile Pictures (Public Obj-C API) + @objc(setProfilePicture:) + public static func objc_setProfilePicture(_ profilePicture: Data) -> AnyPromise { + return AnyPromise.from(setProfilePicture(profilePicture)) + } }