// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "TSGroupThread.h" #import "TSAttachmentStream.h" #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN NSString *const TSGroupThreadAvatarChangedNotification = @"TSGroupThreadAvatarChangedNotification"; NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_NotificationKey_UniqueId"; @implementation TSGroupThread #define TSGroupThreadPrefix @"g" - (instancetype)initWithGroupModel:(TSGroupModel *)groupModel { NSString *uniqueIdentifier = [[self class] threadIdFromGroupId:groupModel.groupId]; self = [super initWithUniqueId:uniqueIdentifier]; if (!self) { return self; } _groupModel = groupModel; return self; } - (instancetype)initWithGroupId:(NSData *)groupId groupType:(GroupType)groupType { NSString *localNumber = [TSAccountManager localNumber]; TSGroupModel *groupModel = [[TSGroupModel alloc] initWithTitle:nil memberIds:@[ localNumber ] image:nil groupId:groupId groupType:groupType adminIds:@[ localNumber ]]; self = [self initWithGroupModel:groupModel]; if (!self) { return self; } return self; } + (nullable instancetype)threadWithGroupId:(NSData *)groupId transaction:(YapDatabaseReadTransaction *)transaction { return [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction]; } + (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId groupType:(GroupType)groupType transaction:(YapDatabaseReadWriteTransaction *)transaction { TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction]; if (!thread) { thread = [[self alloc] initWithGroupId:groupId groupType:groupType]; [thread saveWithTransaction:transaction]; } return thread; } + (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId groupType:(GroupType)groupType { __block TSGroupThread *thread; [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { thread = [self getOrCreateThreadWithGroupId:groupId groupType:groupType transaction:transaction]; }]; return thread; } + (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel transaction:(YapDatabaseReadWriteTransaction *)transaction { TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupModel.groupId] transaction:transaction]; if (!thread) { thread = [[TSGroupThread alloc] initWithGroupModel:groupModel]; [thread saveWithTransaction:transaction]; } return thread; } + (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel { __block TSGroupThread *thread; [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { thread = [self getOrCreateThreadWithGroupModel:groupModel transaction:transaction]; }]; return thread; } + (NSString *)threadIdFromGroupId:(NSData *)groupId { return [TSGroupThreadPrefix stringByAppendingString:[[LKGroupUtilities getDecodedGroupIDAsData:groupId] base64EncodedString]]; } + (NSData *)groupIdFromThreadId:(NSString *)threadId { return [NSData dataFromBase64String:[threadId substringWithRange:NSMakeRange(1, threadId.length - 1)]]; } - (NSArray *)recipientIdentifiers { if (self.isClosedGroup) { NSMutableArray *groupMemberIds = [self.groupModel.groupMemberIds mutableCopy]; if (groupMemberIds == nil) { return @[]; } [groupMemberIds removeObject:TSAccountManager.localNumber]; return [groupMemberIds copy]; } else { return @[ [LKGroupUtilities getDecodedGroupID:self.groupModel.groupId] ]; } } // @returns all threads to which the recipient is a member. // // @note If this becomes a hotspot we can extract into a YapDB View. // As is, the number of groups should be small (dozens, *maybe* hundreds), and we only enumerate them upon SN changes. + (NSArray *)groupThreadsWithRecipientId:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction { NSMutableArray *groupThreads = [NSMutableArray new]; [self enumerateCollectionObjectsWithTransaction:transaction usingBlock:^(id obj, BOOL *stop) { if ([obj isKindOfClass:[TSGroupThread class]]) { TSGroupThread *groupThread = (TSGroupThread *)obj; if ([groupThread.groupModel.groupMemberIds containsObject:recipientId]) { [groupThreads addObject:groupThread]; } } }]; return [groupThreads copy]; } - (BOOL)isGroupThread { return true; } - (BOOL)isClosedGroup { return (self.groupModel.groupType == closedGroup); } - (BOOL)isOpenGroup { return (self.groupModel.groupType == openGroup); } - (BOOL)isCurrentUserMemberInGroup { NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey]; return [self isUserMemberInGroup:userPublicKey]; } - (BOOL)isUserMemberInGroup:(NSString *)publicKey { if (publicKey == nil) { return NO; } return [self.groupModel.groupMemberIds containsObject:publicKey]; } - (BOOL)isUserAdminInGroup:(NSString *)publicKey { if (publicKey == nil) { return NO; } return [self.groupModel.groupAdminIds containsObject:publicKey]; } - (NSString *)name { // TODO sometimes groupName is set to the empty string. I'm hesitent to change // the semantics here until we have time to thouroughly test the fallout. // Instead, see the `groupNameOrDefault` which is appropriate for use when displaying // text corresponding to a group. return self.groupModel.groupName ?: self.class.defaultGroupName; } + (NSString *)defaultGroupName { return @"Group"; } - (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction { self.groupModel = newGroupModel; [self saveWithTransaction:transaction]; [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{ [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.groupThreadUpdated object:self.uniqueId]; }]; } - (void)setIsOnlyNotifyingForMentions:(BOOL)isOnlyNotifyingForMentions withTransaction:(YapDatabaseReadWriteTransaction *)transaction { self.isOnlyNotifyingForMentions = isOnlyNotifyingForMentions; [self saveWithTransaction:transaction]; [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{ [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.groupThreadUpdated object:self.uniqueId]; }]; } - (void)leaveGroupWithSneakyTransaction { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [self leaveGroupWithTransaction:transaction]; }]; } - (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { NSMutableSet *newGroupMemberIDs = [NSMutableSet setWithArray:self.groupModel.groupMemberIds]; NSString *userPublicKey = TSAccountManager.localNumber; if (userPublicKey == nil) { return; } [newGroupMemberIDs removeObject:userPublicKey]; self.groupModel.groupMemberIds = newGroupMemberIDs.allObjects; [self saveWithTransaction:transaction]; [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{ [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.groupThreadUpdated object:self.uniqueId]; }]; } #pragma mark - Avatar - (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self updateAvatarWithAttachmentStream:attachmentStream transaction:transaction]; }]; } - (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream transaction:(YapDatabaseReadWriteTransaction *)transaction { self.groupModel.groupImage = [attachmentStream thumbnailImageSmallSync]; [self saveWithTransaction:transaction]; [transaction addCompletionQueue:nil completionBlock:^{ [self fireAvatarChangedNotification]; }]; // Avatars are stored directly in the database, so there's no need // to keep the attachment around after assigning the image. [attachmentStream removeWithTransaction:transaction]; } - (void)fireAvatarChangedNotification { NSDictionary *userInfo = @{ TSGroupThread_NotificationKey_UniqueId : self.uniqueId }; [[NSNotificationCenter defaultCenter] postNotificationName:TSGroupThreadAvatarChangedNotification object:self.uniqueId userInfo:userInfo]; } @end NS_ASSUME_NONNULL_END