Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Monal/Classes/ChannelMemberList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ struct ChannelMemberList: View {
ForEach(participants.keys, id: \.self) { participant_key in
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
Image(uiImage: MLImageManager.sharedInstance().getAvatarForNick(participant_key, inRoom: channel.contactJid, forAccount: account.accountID))
.resizable()
.frame(width: 35, height: 35, alignment: .center)
.padding(.vertical, 1)
.padding(.trailing, 8)
Text(participant_key)
Spacer()
Text(mucAffiliationToString(participants[participant_key]))
Expand Down
2 changes: 1 addition & 1 deletion Monal/Classes/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ class ChatViewMessage: ExyteChat.Message {
}
init(_ message: MLMessage) {
self.innerMessage = ObservableKVOWrapper(message)
let user = ExyteChat.User(id: message.senderID, name: message.contactDisplayName, avatarURL: nil, isCurrentUser: !message.inbound)
let user = ExyteChat.User(id: message.senderID, name: message.contactDisplayName, avatarData: message.senderAvatar.pngData()!, isCurrentUser: !message.inbound)
// We don't need to initialize the properties that we overrode with computed properties
super.init(id: message.id, user: user, createdAt: message.timestamp, text: "")

Expand Down
2 changes: 2 additions & 0 deletions Monal/Classes/DataLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ extern NSString* const kMessageTypeFiletransfer;

-(void) setAvatarHash:(NSString*) hash forContact:(NSString*) contact andAccount:(NSNumber*) accountID;
-(NSString*) getAvatarHashForContact:(NSString*) buddy andAccount:(NSNumber*) accountID;
-(NSString*) getAvatarHashForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID;
-(void) clearAvatarHashForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID;

-(BOOL) saveMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountID withComment:(NSString*) comment;
-(NSString*) loadMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountID;
Expand Down
18 changes: 18 additions & 0 deletions Monal/Classes/DataLayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,23 @@ -(NSString*) getAvatarHashForContact:(NSString*) buddy andAccount:(NSNumber*) ac
}];
}

-(NSString*) getAvatarHashForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID
{
return [self.db idReadTransaction:^{
NSString* hash = [self.db executeScalar:@"SELECT iconhash FROM muc_participants WHERE account_id=? AND room=? and room_nick=?;" andArguments:@[accountID, room, nick]];
if(!hash)
hash = @""; //hashes should never be nil
return hash;
}];
}

-(void) clearAvatarHashForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID
{
[self.db voidWriteTransaction:^{
[self.db executeNonQuery:@"UPDATE muc_participants SET iconhash='' WHERE account_id=? AND room=? AND room_nick=?;" andArguments:@[accountID, room, nick]];
}];
}

-(BOOL) isContactInList:(NSString*) buddy forAccount:(NSNumber*) accountID
{
return [self.db boolReadTransaction:^{
Expand Down Expand Up @@ -980,6 +997,7 @@ -(void) addParticipant:(NSDictionary*) participant toMuc:(NSString*) room forAcc
[self.db executeNonQuery:@"UPDATE muc_participants SET participant_jid=? WHERE account_id=? AND room=? AND room_nick=?;" andArguments:@[nilWrapper(participant[@"jid"]), accountID, room, participant[@"nick"]]];
[self.db executeNonQuery:@"UPDATE muc_participants SET affiliation=? WHERE account_id=? AND room=? AND room_nick=?;" andArguments:@[nilWrapper(participant[@"affiliation"]), accountID, room, participant[@"nick"]]];
[self.db executeNonQuery:@"UPDATE muc_participants SET role=? WHERE account_id=? AND room=? AND room_nick=?;" andArguments:@[nilWrapper(participant[@"role"]), accountID, room, participant[@"nick"]]];
[self.db executeNonQuery:@"UPDATE muc_participants SET iconhash=? WHERE account_id=? AND room=? AND room_nick=?;" andArguments:@[nilWrapper(participant[@"avatar_hash"]), accountID, room, participant[@"nick"]]];
}];
}

Expand Down
7 changes: 6 additions & 1 deletion Monal/Classes/DataLayerMigrations.m
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ FOREIGN KEY('account_id', 'archive_jid') REFERENCES 'buddylist'('account_id', 'b
//NOTE: next reconnect is now(!) due to the upgraded db version
[self updateDB:db withDataLayer:dataLayer toVersion:5.113 withBlock:^{
[db executeNonQuery:@"UPDATE buddylist SET iconhash='';"];
[[MLImageManager sharedInstance] removeAllIcons];
[[MLImageManager sharedInstance] removeAllContactIcons];
}];

// migrate account_id column in blocklistCache to integer
Expand Down Expand Up @@ -968,6 +968,11 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \
[db executeNonQuery:@"ALTER TABLE buddylist ADD COLUMN reached_mam_archive_top BOOL DEFAULT FALSE;"];
}];

// Allow storing the avatar hashes of MUC participants
[self updateDB:db withDataLayer:dataLayer toVersion:7.002 withBlock:^{
[db executeNonQuery:@"ALTER TABLE muc_participants ADD COLUMN iconhash VARCHAR(200) NULL DEFAULT NULL;"];
}];

//check if device id changed and invalidate state, if so
//but do so only for non-sandbox (e.g. non-development) installs
if(![[HelperTools defaultsDB] boolForKey:@"isSandboxAPNS"])
Expand Down
2 changes: 1 addition & 1 deletion Monal/Classes/HelperTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ void swizzle(Class c, SEL orig, SEL new);
+(UIView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler;
+(NSData*) resizeAvatarImage:(UIImage* _Nullable) image withCircularMask:(BOOL) circularMask toMaxBase64Size:(unsigned long) length;
+(double) report_memory;
+(UIColor*) generateColorFromJid:(NSString*) jid;
+(UIColor*) generateColorFromString:(NSString*) inputString;
+(NSString*) bytesToHuman:(int64_t) bytes;
+(NSString*) stringFromToken:(NSData*) tokenIn;
+(NSString* _Nullable) exportIPCDatabase;
Expand Down
10 changes: 5 additions & 5 deletions Monal/Classes/HelperTools.m
Original file line number Diff line number Diff line change
Expand Up @@ -1775,27 +1775,27 @@ +(double) report_memory
return 1.0; //dummy value
}

+(UIColor*) generateColorFromJid:(NSString*) jid
+(UIColor*) generateColorFromString:(NSString*) inputString
{
//cache generated colors
static NSMutableDictionary* cache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cache = [NSMutableDictionary new];
});
if(cache[jid] != nil)
return cache[jid];
if(cache[inputString] != nil)
return cache[inputString];

//XEP-0392 implementation
NSData* hash = [self sha1:[jid dataUsingEncoding:NSUTF8StringEncoding]];
NSData* hash = [self sha1:[inputString dataUsingEncoding:NSUTF8StringEncoding]];
uint16_t rawHue = CFSwapInt16LittleToHost(*(uint16_t*)[hash bytes]);
double hue = (rawHue / 65536.0) * 360.0;
double saturation = 100.0;
double lightness = 50.0;

double r, g, b;
hsluv2rgb(hue, saturation, lightness, &r, &g, &b);
return cache[jid] = [UIColor colorWithRed:r green:g blue:b alpha:1];
return cache[inputString] = [UIColor colorWithRed:r green:g blue:b alpha:1];
}

+(NSString*) bytesToHuman:(int64_t) bytes
Expand Down
8 changes: 6 additions & 2 deletions Monal/Classes/MLImageManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

@import UIKit;
@class MLContact;
@class MLMessage;

@interface MLImageManager : NSObject

Expand All @@ -25,19 +26,22 @@

+(MLImageManager* _Nonnull) sharedInstance;
-(void) cleanupHashes;
-(void) removeAllIcons;
-(void) removeAllContactIcons;

/**
Takes the string from the xmpp icon vcard info and stores it in an appropropriate place.
*/
-(void) setIconForContact:(MLContact* _Nonnull) contact WithData:(NSData* _Nullable) data ;
-(void) setIconForContact:(MLContact* _Nonnull) contact WithData:(NSData* _Nullable) data;
-(void) setAvatarForNick:(NSString* _Nonnull) nick inRoom:(NSString* _Nonnull) room forAccount:(NSNumber* _Nonnull) accountID WithData:(NSData* _Nullable) data;

/**
retrieves a uiimage for the icon. returns noicon.png if nothing is found. never returns nil.
*/
-(BOOL) hasIconForContact:(MLContact* _Nonnull) contact;
-(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact withCompletion:(void (^_Nullable)(UIImage *_Nullable))completion;
-(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact;
-(UIImage* _Nonnull) getAvatarForNick:(NSString* _Nonnull) nick inRoom:(NSString* _Nonnull) room forAccount:(NSNumber* _Nonnull) accountID;
-(UIImage* _Nonnull) getAvatarForMessage:(MLMessage* _Nonnull) message;
+(UIImage* _Nonnull) circularImage:(UIImage* _Nonnull) image;

-(void) saveBackgroundImageData:(NSData* _Nullable) data forContact:(MLContact* _Nullable) contact;
Expand Down
162 changes: 146 additions & 16 deletions Monal/Classes/MLImageManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ -(void) purgeCacheForContact:(NSString*) contact andAccount:(NSNumber*) accountI
[self resetCachedBackgroundImageForContact:[MLContact createContactFromJid:contact andAccountID:accountID]];
}

-(void) purgeCacheForNick:(NSString*) nick inRoom:(NSString*) room
{
[self.iconCache removeObjectForKey:[NSString stringWithFormat:@"%@_%@", room, nick]];
}

-(void) cleanupHashes
{
NSFileManager* fileManager = [NSFileManager defaultManager];
Expand Down Expand Up @@ -141,17 +146,51 @@ -(void) cleanupHashes
if(error)
DDLogError(@"Error deleting orphan avatar file: %@", error);
}

//clean up orphan hashes and orphan avatar files of muc participants
if(contact.isMuc)
{
NSArray* participants = [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:contact.contactJid forAccountID:contact.accountID];
writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"muc_participant_avatars"];
writablePath = [writablePath stringByAppendingPathComponent:contact.contactJid];
for(NSDictionary* participant in participants)
{
//ignore entries that are not in the muc_participants db table
BOOL online = ((NSNumber*) participant[@"online"]).boolValue;
if(!online)
continue;

writablePath = [writablePath stringByAppendingPathComponent:[self fileNameForNick:participant[@"room_nick"]]];
hasHash = participant[@"iconhash"] && ![@"" isEqualToString:participant[@"iconhash"]];

if(hasHash && ![fileManager isReadableFileAtPath:writablePath])
{
DDLogDebug(@"Deleting orphan hash '%@' of muc participant: %@/%@", participant[@"iconhash"], participant[@"room"], participant[@"room_nick"]);
//delete avatar hash from db if the file containing our image data vanished
[[DataLayer sharedInstance] clearAvatarHashForNick:participant[@"room_nick"] inRoom:participant[@"room"] forAccount:contact.accountID];
}

if(!hasHash && [fileManager isReadableFileAtPath:writablePath])
{
DDLogDebug(@"Deleting orphan avatar file '%@' of muc participant: %@/%@", writablePath, participant[@"room"], participant[@"room_nick"]);
NSError* error;
[fileManager removeItemAtPath:writablePath error:&error];
if(error)
DDLogError(@"Error deleting orphan avatar file: %@", error);
}
}
}
}
}

-(void) removeAllIcons
-(void) removeAllContactIcons
{
NSError* error;
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
[fileManager removeItemAtPath:writablePath error:&error];
if(error)
DDLogError(@"Got error while trying to delete all avatar files: %@", error);
DDLogError(@"Got error while trying to delete all contact avatar files: %@", error);
}

#pragma mark chat bubbles
Expand All @@ -175,19 +214,11 @@ -(UIImage*) outboundImage

#pragma mark user icons

-(UIImage*) generateDummyIconForContact:(MLContact*) contact
-(UIImage*) generatePlaceholderAvatarForString:(NSString*) inputString withInitial:(NSString*) initialCharacter
{
NSString* contactLetter;

if(contact.isSelfChat)
{
xmpp* account = contact.account;
contactLetter = [[[MLContact ownDisplayNameForAccount:account] substringToIndex:1] uppercaseString];
}
else
contactLetter = [[[contact contactDisplayName] substringToIndex:1] uppercaseString];
MLAssert(initialCharacter != nil && initialCharacter.length == 1, @"initialCharacter string does not represent a character");

UIColor* background = [HelperTools generateColorFromJid:contact.contactJid];
UIColor* background = [HelperTools generateColorFromString:inputString];
UIColor* foreground = [UIColor blackColor];
if(![background isLightColor])
foreground = [UIColor whiteColor];
Expand All @@ -210,20 +241,37 @@ -(UIImage*) generateDummyIconForContact:(MLContact*) contact
NSForegroundColorAttributeName: foreground,
NSParagraphStyleAttributeName: paragraphStyle
};
CGSize textSize = [contactLetter sizeWithAttributes:attributes];
CGSize textSize = [initialCharacter sizeWithAttributes:attributes];
CGRect textRect = CGRectMake(floorf((float)(renderer.format.bounds.size.width - textSize.width) / 2),
floorf((float)(renderer.format.bounds.size.height - textSize.height) / 2),
textSize.width,
textSize.height);
[contactLetter drawInRect:textRect withAttributes:attributes];
[initialCharacter drawInRect:textRect withAttributes:attributes];
}];
}

-(UIImage*) generateDummyIconForContact:(MLContact*) contact
{
NSString* initial = [[contact.contactDisplayNameWithoutSelfnotesPrefix substringToIndex:1] uppercaseString];
return [self generatePlaceholderAvatarForString:contact.contactJid withInitial:initial];
}

-(UIImage*) generatePlaceholderAvatarForNick:(NSString*) nick
{
NSString* initial = [[nick substringToIndex:1] uppercaseString];
return [self generatePlaceholderAvatarForString:nick withInitial:initial];
}

-(NSString*) fileNameforContact:(MLContact*) contact
{
return [NSString stringWithFormat:@"%@_%@.png", contact.accountID.stringValue, [contact.contactJid lowercaseString]];;
}

-(NSString*) fileNameForNick:(NSString*) nick
{
return [NSString stringWithFormat:@"%@.png", nick];
}

-(void) setIconForContact:(MLContact*) contact WithData:(NSData* _Nullable) data
{
//documents directory/buddyicons/account no/contact
Expand Down Expand Up @@ -255,7 +303,40 @@ -(void) setIconForContact:(MLContact*) contact WithData:(NSData* _Nullable) data

//remove from cache if its there
[self.iconCache removeObjectForKey:[NSString stringWithFormat:@"%@_%@", contact.accountID, contact]];

}

-(void) setAvatarForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID WithData:(NSData* _Nullable) data
{
//documents directory/muc_participant_avatars/accountID/room/nick

NSString* filename = [self fileNameForNick:nick];
NSFileManager* fileManager = [NSFileManager defaultManager];

NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"muc_participant_avatars"];
writablePath = [writablePath stringByAppendingPathComponent:accountID.stringValue];
writablePath = [writablePath stringByAppendingPathComponent:room];

NSError* error;
[fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:&error];
[HelperTools configureFileProtectionFor:writablePath];
writablePath = [writablePath stringByAppendingPathComponent:filename];

if([fileManager fileExistsAtPath:writablePath])
[fileManager removeItemAtPath:writablePath error:nil];

if(data)
{
if([data writeToFile:writablePath atomically:NO])
{
[HelperTools configureFileProtectionFor:writablePath];
DDLogVerbose(@"wrote image to file: %@", writablePath);
}
else
DDLogError(@"failed to write image to file: %@", writablePath);
}

//remove from cache if its there
[self purgeCacheForNick:nick inRoom:room];
}

-(BOOL) hasIconForContact:(MLContact*) contact
Expand Down Expand Up @@ -337,6 +418,55 @@ -(UIImage*) getIconForContact:(MLContact*) contact withCompletion:(void (^)(UIIm
return toreturn;
}

-(UIImage*) getAvatarForNick:(NSString*) nick inRoom:(NSString*) room forAccount:(NSNumber*) accountID
{
NSString* filename = [self fileNameForNick:nick];

UIImage* toreturn = nil;

NSString* cacheKey = [NSString stringWithFormat:@"%@_%@", room, nick];

//check cache
toreturn = [self.iconCache objectForKey:cacheKey];
if(!toreturn)
{
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"muc_participant_avatars"];
writablePath = [writablePath stringByAppendingPathComponent:accountID.stringValue];
writablePath = [writablePath stringByAppendingPathComponent:room];
writablePath = [writablePath stringByAppendingPathComponent:filename];

DDLogVerbose(@"Loading avatar image at: %@", writablePath);
UIImage* savedImage = [UIImage imageWithContentsOfFile:writablePath];
DDLogVerbose(@"Loaded avatar image: %@", toreturn);

if(savedImage)
toreturn = savedImage;
else
toreturn = [self generatePlaceholderAvatarForNick:nick];

//uiimage is cached if avaialable, but only if not in appex due to memory limits therein
if(toreturn && ![HelperTools isAppExtension])
[self.iconCache setObject:toreturn forKey:cacheKey];
}
MLAssert(toreturn != nil, @"Returned avatar image can't be nil!");
return toreturn;
}

-(UIImage*) getAvatarForMessage:(MLMessage*) message
{
// For 1-1 chats, or group participants that are in our roster, use avatars fetched with XEP-0084
if(!message.isMuc)
return message.contact.avatar;
else if(message.participantJid)
{
MLContact* contact = [MLContact createContactFromJid:message.participantJid andAccountID:message.accountID];
if(contact.isInRoster)
return contact.avatar;
}

// For public channel participants, or group participants not in our roster, use avatars fetched with vcard-temp
return [self getAvatarForNick:message.actualFrom inRoom:message.buddyName forAccount:message.accountID];
}

-(void) saveBackgroundImageData:(NSData* _Nullable) data forContact:(MLContact* _Nullable) contact
{
Expand Down
Loading
Loading