Enable profile pictures

This commit is contained in:
Maxim Shishmarev 2019-11-18 15:59:50 +11:00
parent 7aa4e83700
commit 859384afaf
12 changed files with 91 additions and 85 deletions

View File

@ -58,7 +58,7 @@ final class DisplayNameVC : OnboardingBaseViewController {
guard !displayName.isEmpty else {
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a display name", comment: ""))
}
guard displayName.allSatisfy({ "0"..."9" ~= $0 || "a"..."z" ~= $0 || "A"..."Z" ~= $0 || $0 == "_" }) else {
guard displayName.allSatisfy({ "0"..."9" ~= $0 || "a"..."z" ~= $0 || "A"..."Z" ~= $0 || $0 == "_" || $0 == " " }) else {
return OWSAlerts.showErrorAlert(message: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {

View File

@ -57,7 +57,9 @@ NS_ASSUME_NONNULL_BEGIN
handler:^(UIAlertAction *_Nonnull action) {
[self.delegate clearAvatar];
}];
[actionSheet addAction:clearAction];
// TODO: enable this once we support removing avatars (as opposed to replacing)
// [actionSheet addAction:clearAction];
}
[self.delegate.fromViewController presentAlert:actionSheet];

View File

@ -396,7 +396,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a display name", @"")];
}
NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ "];
if ([normalizedProfileName rangeOfCharacterFromSet:allowedCharacters.invertedSet].location != NSNotFound) {
return [OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", @"")];
}

View File

@ -1165,12 +1165,10 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}
NSString *_Nullable avatarUrlPathAtStart = userProfile.avatarUrlPath;
if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrlPath.length < 1) {
if (userProfile.avatarUrlPath.length < 1) {
return;
}
OWSAES256Key *profileKeyAtStart = userProfile.profileKey;
NSString *fileName = [self generateAvatarFilename];
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:fileName];
@ -1188,66 +1186,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
NSString *tempDirectory = OWSTemporaryDirectory();
NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName];
void (^completionHandler)(NSURLResponse *_Nonnull, NSURL *_Nullable, NSError *_Nullable) = ^(
NSURLResponse *_Nonnull response, NSURL *_Nullable filePathParam, NSError *_Nullable error) {
// Ensure disk IO and decryption occurs off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *_Nullable encryptedData = [NSData dataWithContentsOfFile:tempFilePath];
NSData *_Nullable decryptedData = encryptedData;
UIImage *_Nullable image = nil;
if (decryptedData) {
BOOL success = [decryptedData writeToFile:filePath atomically:YES];
if (success) {
image = [UIImage imageWithContentsOfFile:filePath];
}
}
@synchronized(self.currentAvatarDownloads)
{
[self.currentAvatarDownloads removeObject:userProfile.recipientId];
}
OWSUserProfile *latestUserProfile =
[OWSUserProfile getOrBuildUserProfileForRecipientId:userProfile.recipientId
dbConnection:self.dbConnection];
if (latestUserProfile.profileKey.keyData.length < 1
|| ![latestUserProfile.profileKey isEqual:userProfile.profileKey]) {
OWSLogWarn(@"Ignoring avatar download for obsolete user profile.");
} else if (![avatarUrlPathAtStart isEqualToString:latestUserProfile.avatarUrlPath]) {
OWSLogInfo(@"avatar url has changed during download");
if (latestUserProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:latestUserProfile];
}
} else if (error) {
OWSLogError(@"avatar download for %@ failed with error: %@", userProfile.recipientId, error);
} else if (!encryptedData) {
OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId);
} else if (!decryptedData) {
OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId);
} else if (!image) {
OWSLogError(
@"avatar image for %@ could not be loaded with error: %@", userProfile.recipientId, error);
} else {
[self updateProfileAvatarCache:image filename:fileName];
[latestUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:nil];
}
// If we're updating the profile that corresponds to our local number,
// update the local profile as well.
NSString *_Nullable localNumber = self.tsAccountManager.localNumber;
if (localNumber && [localNumber isEqualToString:userProfile.recipientId]) {
OWSUserProfile *localUserProfile = self.localUserProfile;
OWSAssertDebug(localUserProfile);
[localUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:nil];
[self updateProfileAvatarCache:image filename:fileName];
}
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
});
};
NSString *profilePictureURL = userProfile.avatarUrlPath;
NSError *serializationError;
@ -1261,15 +1199,49 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return;
}
__block *downloadTask = [self.avatarHTTPManager downloadTaskWithRequest:request
progress:^(NSProgress *_Nonnull downloadProgress) {
OWSLogVerbose(
@"Downloading avatar for %@ %f", userProfile.recipientId, downloadProgress.fractionCompleted);
NSURLSession* session = [NSURLSession sharedSession];
NSURLSessionTask* downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
@synchronized(self.currentAvatarDownloads)
{
[self.currentAvatarDownloads removeObject:userProfile.recipientId];
}
destination:^NSURL *_Nonnull(NSURL *_Nonnull targetPath, NSURLResponse *_Nonnull response) {
return [NSURL fileURLWithPath:tempFilePath];
if (error) {
OWSLogError(@"Dowload failed: %@", error);
return;
}
completionHandler:completionHandler];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
NSError *moveError;
if (![fileManager moveItemAtURL:location toURL:fileURL error:&moveError]) {
OWSLogError(@"MoveItemAtURL for avatar failed: %@", moveError);
return;
}
UIImage *image = [UIImage imageWithContentsOfFile:[fileURL path]];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateProfileAvatarCache:image filename:fileName];
OWSUserProfile *latestUserProfile =
[OWSUserProfile getOrBuildUserProfileForRecipientId:userProfile.recipientId
dbConnection:self.dbConnection];
[latestUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:^{
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification
object:nil];
}];
});
}
}];
[downloadTask resume];
});
}
@ -1334,8 +1306,11 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
{
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:contactID transaction:transaction];
NSString *oldProfilePictureURL = userProfile.avatarUrlPath;
[userProfile updateWithProfileName:displayName avatarUrlPath:profilePictureURL avatarFileName:@"" transaction:transaction completion:nil];
if (![oldProfilePictureURL isEqual:profilePictureURL]) {
// Note: we keep using the old file name until we have the new one
// (otherwise the profile picuture would disspear for a short time)
NSString *oldAvatarFileName = userProfile.avatarFileName;
[userProfile updateWithProfileName:displayName avatarUrlPath:profilePictureURL avatarFileName:oldAvatarFileName transaction:transaction completion:nil];
if (profilePictureURL && ![oldProfilePictureURL isEqual:profilePictureURL]) {
[self downloadAvatarForUserProfile:userProfile];
}
}

View File

@ -30,7 +30,7 @@ typedef void (^OWSAvatarDrawBlock)(CGContextRef context);
OWSAvatarBuilder *avatarBuilder;
if ([thread isKindOfClass:[TSContactThread class]]) {
TSContactThread *contactThread = (TSContactThread *)thread;
return [LKIdenticon generateIconWithString:contactThread.contactIdentifier size:((CGFloat)diameter)];
avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:contactThread.contactIdentifier colorName:contactThread.conversationColorName diameter:diameter];
} else if ([thread isKindOfClass:[TSGroupThread class]]) {
avatarBuilder = [[OWSGroupAvatarBuilder alloc] initWithThread:(TSGroupThread *)thread diameter:diameter];
} else {

View File

@ -247,7 +247,7 @@ message DataMessage {
// Loki: A custom message for our profile
message LokiProfile {
optional string displayName = 1;
optional string profilePicture = 2;
optional AttachmentPointer avatar = 2;
}
optional string body = 1;

View File

@ -10,6 +10,7 @@ public final class LokiStorageAPI : LokiDotNetAPI {
@objc public static let server = "https://file.lokinet.org"
// #endif
private static let deviceLinkType = "network.loki.messenger.devicemapping"
private static let attachmentType = "net.app.core.oembed"
// MARK: Database
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }

View File

@ -99,6 +99,9 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
let avatarUrl = value["avatar"] as? String ?? nil;
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
@ -125,7 +128,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
return LokiPublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
}
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: avatarUrl, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
@ -162,7 +165,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
throw Error.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, avatar: signedMessage.avatar, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
}
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiPublicChatMessage> in
if let error = error as? NetworkManagerError, error.statusCode == 401 {

View File

@ -5,6 +5,7 @@ public final class LokiPublicChatMessage : NSObject {
public let serverID: UInt64?
public let hexEncodedPublicKey: String
public let displayName: String
public let avatar: String?
public let body: String
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
public let timestamp: UInt64
@ -66,10 +67,11 @@ public final class LokiPublicChatMessage : NSObject {
}
// MARK: Initialization
public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?) {
public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, avatar: String?, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?) {
self.serverID = serverID
self.hexEncodedPublicKey = hexEncodedPublicKey
self.displayName = displayName
self.avatar = avatar
self.body = body
self.type = type
self.timestamp = timestamp
@ -79,7 +81,7 @@ public final class LokiPublicChatMessage : NSObject {
super.init()
}
@objc public convenience init(hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteeHexEncodedPublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, signatureData: Data?, signatureVersion: UInt64) {
@objc public convenience init(hexEncodedPublicKey: String, displayName: String, avatar: String?, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteeHexEncodedPublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, signatureData: Data?, signatureVersion: UInt64) {
let quote: Quote?
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteeHexEncodedPublicKey, let quotedMessageBody = quotedMessageBody {
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
@ -93,7 +95,7 @@ public final class LokiPublicChatMessage : NSObject {
} else {
signature = nil
}
self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature)
self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: avatar, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature)
}
// MARK: Crypto
@ -108,7 +110,7 @@ public final class LokiPublicChatMessage : NSObject {
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, avatar: avatar, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
}
internal func hasValidSignature() -> Bool {
@ -128,6 +130,11 @@ public final class LokiPublicChatMessage : NSObject {
value["sig"] = signature.data.toHexString()
value["sigver"] = signature.version
}
if let avatar = avatar {
value["avatar"] = avatar;
}
let annotation: JSON = [ "type" : type, "value" : value ]
let attachmentAnnotations: [JSON] = attachments.map { attachment in
let type: String

View File

@ -120,6 +120,14 @@ public final class LokiPublicChatPoller : NSObject {
signalLinkPreview.setImage(try! attachment.build())
dataMessage.setPreview([ try! signalLinkPreview.build() ])
}
let profile = SSKProtoDataMessageLokiProfile.builder()
if let avatar = message.avatar {
profile.setProfilePicture(avatar)
profile.setDisplayName(message.displayName)
dataMessage.setProfile(try! profile.build())
}
dataMessage.setTimestamp(message.timestamp)
dataMessage.setGroup(try! groupContext.build())
if let quote = message.quote {

View File

@ -1335,6 +1335,14 @@ NS_ASSUME_NONNULL_BEGIN
[newMemberIds addObjectsFromArray:oldGroupThread.groupModel.groupMemberIds];
}
NSString *hexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
TSContactThread *thread =
[TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
NSString *profilePictureURL = dataMessage.profile.profilePicture;
NSString *displayName = dataMessage.profile.displayName;
[self.profileManager updateProfileForContactWithID:thread.contactIdentifier displayName:displayName profilePictureURL:profilePictureURL with:transaction];
switch (dataMessage.group.type) {
case SSKProtoGroupContextTypeUpdate: {
// Ensures that the thread exists but doesn't update it.
@ -1536,8 +1544,9 @@ NS_ASSUME_NONNULL_BEGIN
if (rawProfilePictureURL != nil && rawProfilePictureURL.length > 0) {
profilePictureURL = rawProfilePictureURL;
}
[self.profileManager updateProfileForContactWithID:thread.contactIdentifier displayName:displayName profilePictureURL:profilePictureURL with:transaction];
// Loki: Parse Loki specific properties if needed
if (envelope.isPtpMessage) { incomingMessage.isP2P = YES; }

View File

@ -1201,6 +1201,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
if (displayName == nil) { displayName = @"Anonymous"; }
NSString *avatarUrl = SSKEnvironment.shared.profileManager.profilePictureURL;
TSQuotedMessage *quote = message.quotedMessage;
uint64_t quoteID = quote.timestamp;
NSString *quoteeHexEncodedPublicKey = quote.authorId;
@ -1211,7 +1212,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}];
}
NSString *body = (message.body != nil && message.body.length > 0) ? message.body : [NSString stringWithFormat:@"%@", @(message.timestamp)]; // Workaround for the fact that the back-end doesn't accept messages without a body
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:body type:LKPublicChatAPI.publicChatMessageType
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName avatar:avatarUrl body:body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0];
OWSLinkPreview *linkPreview = message.linkPreview;
if (linkPreview != nil) {