// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2013-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "GroupDetailsViewController.h" #import "Conversation.h" #import "Contact.h" #import "GroupMemberCell.h" #import "ContactPickerViewController.h" #import "ContactDetailsViewController.h" #import "ImageData.h" #import "UIDefines.h" #import "AvatarMaker.h" #import "AppDelegate.h" #import "AddMemberCell.h" #import "BundleUtil.h" #import "DeleteConversationAction.h" #import "FullscreenImageViewController.h" #import "EditGroupViewController.h" #import "PickGroupMembersViewController.h" #import "ModalNavigationController.h" #import "ModalPresenter.h" #import "UIImage+ColoredImage.h" #import "CreateGroupNavigationController.h" #import "UserSettings.h" #import "Threema-Swift.h" #import "MDMSetup.h" #import "BundleUtil.h" @interface GroupDetailsViewController () @property BOOL canExportChat; @end @implementation GroupDetailsViewController { NSArray *members; Contact *selectedContact; NSInteger includeMediaIndex; NSInteger withoutMediaIndex; } - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatedGroup:) name:kNotificationUpdatedGroup object:nil]; self.navigationController.interactivePopGestureRecognizer.enabled = YES; self.navigationController.interactivePopGestureRecognizer.delegate = nil; _nameLabel.font = [UIFont boldSystemFontOfSize: _nameLabel.font.pointSize]; UITapGestureRecognizer *disclosureTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedHeaderView)]; [_disclosureButton addGestureRecognizer:disclosureTapRecognizer]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedImage)]; [_imageView addGestureRecognizer:tapRecognizer]; [self setupColors]; } - (void)setupColors { [_nameLabel setTextColor:[Colors fontNormal]]; _nameLabel.shadowColor = nil; [_creatorLabel setTextColor:[Colors fontLight]]; _creatorLabel.shadowColor = nil; UIImage *disclosureImage = [self.disclosureButton.imageView.image imageWithTint:[Colors main]]; [self.disclosureButton setImage:disclosureImage forState:UIControlStateNormal]; if (@available(iOS 11.0, *)) { _imageView.accessibilityIgnoresInvertColors = true; } } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.view.alpha = 1.0; _canExportChat = [self canExportConversation]; [self reloadMembers]; [self.tableView reloadData]; [self updateHeaderView]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3]; CGFloat size = fontDescriptor.pointSize; _nameLabel.font = [UIFont boldSystemFontOfSize:size]; } - (void)setGroup:(GroupProxy *)newGroup { [_group.conversation removeObserver:self forKeyPath:@"members"]; [_group.conversation removeObserver:self forKeyPath:@"groupName"]; [_group.conversation removeObserver:self forKeyPath:@"groupImage"]; _group = newGroup; [_group.conversation addObserver:self forKeyPath:@"members" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]; [_group.conversation addObserver:self forKeyPath:@"groupName" options:0 context:nil]; [_group.conversation addObserver:self forKeyPath:@"groupImage" options:0 context:nil]; } - (void)dealloc { [_group.conversation removeObserver:self forKeyPath:@"members"]; [_group.conversation removeObserver:self forKeyPath:@"groupName"]; [_group.conversation removeObserver:self forKeyPath:@"groupImage"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)updateHeaderView { UIImage *avatarImage = [[AvatarMaker sharedAvatarMaker] avatarForConversation:_group.conversation size:_imageView.frame.size.width masked:NO]; _imageView.image = avatarImage; _imageView.contentMode = UIViewContentModeScaleAspectFill; _imageView.layer.masksToBounds = YES; _imageView.layer.cornerRadius = _imageView.bounds.size.width / 2; _nameLabel.text = _group.name; if ([_group isOwnGroup]) { _disclosureButton.hidden = NO; } else { _disclosureButton.hidden = YES; } _creatorLabel.text = [_group creatorString]; _headerView.accessibilityLabel = [NSString stringWithFormat:@"%@, %@", _nameLabel.text, _creatorLabel.text]; } - (void)reloadMembers { members = _group.sortedActiveMembers; } - (void)tappedHeaderView { [self showEditGroupVC]; } - (void)tappedImage { if (_group.conversation.groupImage) { UIImage *image = [UIImage imageWithData:_group.conversation.groupImage.data]; if (image != nil) { FullscreenImageViewController *imageController = [FullscreenImageViewController controllerForImage:image]; if (SYSTEM_IS_IPAD) { ModalNavigationController *nav = [[ModalNavigationController alloc] initWithRootViewController:imageController]; nav.showDoneButton = YES; nav.showFullScreenOnIPad = YES; [self presentViewController:nav animated:YES completion:nil]; } else { [self.navigationController pushViewController:imageController animated:YES]; } } else { [self showEditGroupVC]; } } else { [self showEditGroupVC]; } } - (void)showEditGroupVC { if ([_group isOwnGroup] == NO) { return; } UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"CreateGroup" bundle:nil]; EditGroupViewController *editVC = [storyboard instantiateViewControllerWithIdentifier:@"EditGroupViewController"]; editVC.group = _group; UINavigationController *navigationVC = [[UINavigationController alloc] initWithRootViewController:editVC]; [ModalPresenter present:navigationVC on:self fromRect:_headerView.frame inView:self.view]; } - (void)showEditGroupMembersVCFrom:(NSIndexPath *)indexPath { if ([_group isOwnGroup] == NO || [self canAddMember] == NO) { return; } UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"CreateGroup" bundle:nil]; PickGroupMembersViewController *pickMembersVC = [storyboard instantiateViewControllerWithIdentifier:@"PickGroupMembersViewController"]; pickMembersVC.group = _group; UINavigationController *navigationVC = [[UINavigationController alloc] initWithRootViewController:pickMembersVC]; UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; [ModalPresenter present:navigationVC on:self fromRect:cell.frame inView:self.view]; [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"ShowContact"]) { ContactDetailsViewController *detailsVc = (ContactDetailsViewController*)segue.destinationViewController; detailsVc.contact = selectedContact; } else if ([segue.identifier isEqualToString:@"ShowPushSetting"]) { NotificationSettingViewController *notificationSettingViewController = (NotificationSettingViewController *)segue.destinationViewController; notificationSettingViewController.identity = [NSString stringWithHexData:self.group.groupId]; notificationSettingViewController.isGroup = YES; notificationSettingViewController.conversation = _group.conversation; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"members"]) { dispatch_async(dispatch_get_main_queue(), ^{ [self reloadMembers]; [self updateHeaderView]; [self.tableView reloadData]; }); } else if ([keyPath isEqualToString:@"groupName"] || [keyPath isEqualToString:@"groupImage"]) { dispatch_async(dispatch_get_main_queue(), ^{ [self updateHeaderView]; }); } } - (void)syncMembers { [_group syncGroupInfoToAll]; [UIAlertTemplate showAlertWithOwner:self title:NSLocalizedString(@"group_sync_title", nil) message:NSLocalizedString(@"group_sync_message", nil) actionOk:nil]; [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES]; } - (void)exportChatWithMedia:(bool)withMedia { EntityManager *em = [[EntityManager alloc] init]; ConversationExporter *exporter = [[ConversationExporter alloc] initWithViewController:self conversation: self.group.conversation entityManager:em withMedia:withMedia]; [exporter exportGroupConversation]; } - (void)conversationActionFromRect:(CGRect)rect { UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"include_media_title", nil), kExportConversationMediaSizeLimit] message:nil preferredStyle:UIAlertControllerStyleActionSheet]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"include_media", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [self exportChatWithMedia:true]; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"without_media", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [self exportChatWithMedia:false]; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:1] animated:YES]; }]]; actionSheet.popoverPresentationController.sourceRect = rect; actionSheet.popoverPresentationController.sourceView = self.view; [self presentViewController:actionSheet animated:YES completion:nil]; } - (void)sendMessageAction { NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: _group.conversation, kKeyConversation, [NSNumber numberWithBool:YES], kKeyForceCompose, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationShowConversation object:nil userInfo:info]; } - (BOOL)canExportConversation { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:NO]; if (_group.conversation == nil || [mdmSetup disableExport]) { return NO; } return _group.conversation.messages.count > 0; } - (void)leaveGroup { NSString *title = [self alertMessage]; [UIAlertTemplate showAlertWithOwner:self title:title message:nil titleOk:[BundleUtil localizedStringForKey:@"leave"] actionOk:^(UIAlertAction * _Nonnull action) { [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:3] animated:YES]; [self processLeaveGroup]; } titleCancel:[BundleUtil localizedStringForKey:@"cancel"] actionCancel:^(UIAlertAction * _Nonnull action) { [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:3] animated:YES]; }]; } - (NSString *)alertMessage { NSString *message; if (_group.conversation.isGroup && _group.conversation.contact == nil) { message = NSLocalizedString(@"group_admin_delete_confirm", nil); } else { message = NSLocalizedString(@"group_delete_confirm", nil); } return message; } - (void)processLeaveGroup { [_group leaveGroup]; [self reloadMembers]; [self.tableView reloadData]; [UIAlertTemplate showAlertWithOwner:self title:@"" message:NSLocalizedString(@"group_member_self_left", nil) actionOk:nil]; } - (BOOL)canAddMember { return (_group.conversation.members.count < [[BundleUtil objectForInfoDictionaryKey:@"ThreemaMaxGroupMembers"] intValue]); } - (void)cloneGroup { [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:NO]; if ([mdmSetup disableCreateGroup]) { [UIAlertTemplate showAlertWithOwner:self title:@"" message:NSLocalizedString(@"disabled_by_device_policy", nil) actionOk:nil]; return; } NSString *title = NSLocalizedString(@"group_clone_title", nil); NSString *message = NSLocalizedString(@"group_clone_message", nil); UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"no", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { }]]; [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"yes", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"CreateGroup" bundle:nil]; CreateGroupNavigationController *navVC = (CreateGroupNavigationController *)[storyboard instantiateInitialViewController]; navVC.cloneGroupId = _group.groupId; [self presentViewController:navVC animated:YES completion:nil]; }]]; [self presentViewController:alertController animated:YES completion:nil]; } #pragma mark - Table view data source - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0 && (indexPath.row-1) == members.count) { [Colors updateTableViewCellBackground:cell]; [Colors setTextColor:[Colors main] inView:cell.contentView]; } else if (indexPath.section == 3 ) { // handle custom table cells [Colors updateTableViewCellBackground:cell]; [Colors setTextColor:[Colors red] inView:cell.contentView]; } else { [Colors updateTableViewCell:cell]; } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 4; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0) { NSInteger n = members.count + 2; if ([_group isOwnGroup]) { n += 2; /* me = creator */ } return n; } else if (section == 1) { if (_hideActionButtons) { return 0; } if (_canExportChat) { return 2; } return 1; } else if (section == 2) { if (_hideActionButtons || [_group isSelfMember] == false) { return 0; } return 1; } else if (section == 3) { if (_hideActionButtons || [_group isSelfMember] == false) { return 0; } return 1; } return 0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { if (indexPath.row == 0) { /* first = me */ GroupMemberCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GroupMemberCell"]; cell.accessoryType = UITableViewCellAccessoryNone; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.isSelfMember = YES; cell.contact = nil; if (![_group isSelfMember]) { cell.nameLabel.text = [BundleUtil localizedStringForKey:@"you are not a member"]; } return cell; } else if ((indexPath.row-1) < members.count) { GroupMemberCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GroupMemberCell"]; cell.isSelfMember = NO; cell.contact = [members objectAtIndex:indexPath.row-1]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.selectionStyle = UITableViewCellSelectionStyleBlue; return cell; } else if ((indexPath.row-1) == members.count && [_group isOwnGroup]) { BOOL canAddMember = [self canAddMember]; AddMemberCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AddMemberCell"]; cell.userInteractionEnabled = canAddMember; cell.plusImage.alpha = canAddMember ? 1.0 : 0.4; cell.addLabel.enabled = canAddMember; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } else if ((indexPath.row-1) == members.count && ![_group isOwnGroup]) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CloneCell"]; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } else if ((indexPath.row-1) == (members.count+1)) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SyncCell"]; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } else if ((indexPath.row-1) == (members.count+2)) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CloneCell"]; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } } else if (indexPath.section == 1) { NSString *cellIdentifier; if (indexPath.row == 0) { cellIdentifier = @"SendMessageCell"; } else { cellIdentifier = @"ExportConversationCell"; } UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } else if (indexPath.section == 2) { UITableViewCell *pushSettingCell = [tableView dequeueReusableCellWithIdentifier:@"PushSettingCell"]; pushSettingCell.textLabel.text = NSLocalizedString(@"pushSetting_title", @""); return pushSettingCell; } else if (indexPath.section == 3) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"LeaveGroupCell"]; cell.accessibilityTraits = UIAccessibilityTraitButton; return cell; } return nil; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { if (section == 0) { return 40.0; } return UITableViewAutomaticDimension; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if (section == 0) { return NSLocalizedString(@"members", nil); } return nil; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { if (indexPath.row != 0 && (indexPath.row-1) < members.count) { selectedContact = [members objectAtIndex:indexPath.row-1]; [self performSegueWithIdentifier:@"ShowContact" sender:self]; } else if ((indexPath.row-1) == members.count && [_group isOwnGroup]) { [self showEditGroupMembersVCFrom:indexPath]; } else if ((indexPath.row-1) == members.count && ![_group isOwnGroup]) { [self cloneGroup]; } else if ((indexPath.row-1) == (members.count+1)) { [self syncMembers]; } else if ((indexPath.row-1) == (members.count+2)) { [self cloneGroup]; } } else if (indexPath.section == 1) { if (indexPath.row == 0) { [self sendMessageAction]; } else { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; [self conversationActionFromRect:cell.frame]; } } else if (indexPath.section == 2) { } else if (indexPath.section == 3) { [self leaveGroup]; } } #pragma mark - Table view delegae - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0 && _group.isOwnGroup) { if (indexPath.row == 0) { /* first = me */ return NO; } else if ((indexPath.row-1) < members.count) { return YES; } } return NO; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { Contact *contact = [members objectAtIndex:indexPath.row-1]; [_group adminRemoveMember:contact]; [self reloadMembers]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; } } #pragma mark - Mail composer delegate - (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { [self dismissViewControllerAnimated:YES completion:nil]; } # pragma mark - preview actions - (NSArray> *)previewActionItems { NSMutableArray *actions = [NSMutableArray array]; NSString *sendMessageTitle = NSLocalizedString(@"send_message", nil); UIPreviewAction *sendMessageAction = [UIPreviewAction actionWithTitle:sendMessageTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) { [self sendMessageAction]; }]; [actions addObject:sendMessageAction]; if (_delegate) { NSString *leaveGroupTitle = NSLocalizedString(@"leave_group", nil); UIPreviewAction *leaveGroupAction = [UIPreviewAction actionWithTitle:leaveGroupTitle style:UIPreviewActionStyleDestructive handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) { [_delegate presentGroupDetails:self onCompletion:^(GroupDetailsViewController *groupDetailsViewController) { [groupDetailsViewController leaveGroup]; }]; }]; [actions addObject:leaveGroupAction]; } return actions; } #pragma mark - Notifications - (void)updatedGroup:(NSNotification*)notification { NSString *creatorString = notification.userInfo[@"creatorString"]; NSData *groupId = notification.userInfo[@"groupId"]; if ([_group.creatorString isEqualToString:creatorString] && _group.groupId == groupId) { dispatch_async(dispatch_get_main_queue(), ^{ [self reloadMembers]; [self updateHeaderView]; [self.tableView reloadData]; }); } } @end