484 lines
16 KiB
Objective-C
484 lines
16 KiB
Objective-C
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#import "NewGroupViewController.h"
|
|
#import "AvatarViewHelper.h"
|
|
#import "OWSNavigationController.h"
|
|
#import "Signal-Swift.h"
|
|
#import "SignalApp.h"
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
|
#import <SignalCoreKit/NSString+OWS.h>
|
|
#import <SignalCoreKit/Randomness.h>
|
|
#import <SignalMessaging/BlockListUIUtils.h>
|
|
#import <SignalMessaging/ContactTableViewCell.h>
|
|
#import <SignalMessaging/Environment.h>
|
|
#import <SignalMessaging/OWSContactsManager.h>
|
|
#import <SignalMessaging/OWSTableViewController.h>
|
|
#import <SignalMessaging/UIUtil.h>
|
|
#import <SignalMessaging/UIView+OWS.h>
|
|
#import <SignalMessaging/UIViewController+OWS.h>
|
|
#import <SignalServiceKit/FunctionalUtil.h>
|
|
#import <SignalServiceKit/OWSMessageSender.h>
|
|
#import <SignalServiceKit/SignalAccount.h>
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
#import <SignalServiceKit/TSGroupModel.h>
|
|
#import <SignalServiceKit/TSGroupThread.h>
|
|
#import <SignalServiceKit/TSOutgoingMessage.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
@interface NewGroupViewController () <UIImagePickerControllerDelegate,
|
|
UITextFieldDelegate,
|
|
AvatarViewHelperDelegate,
|
|
RecipientPickerDelegate,
|
|
UINavigationControllerDelegate,
|
|
OWSNavigationView>
|
|
|
|
@property (nonatomic, readonly) OWSMessageSender *messageSender;
|
|
@property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper;
|
|
|
|
@property (nonatomic, readonly) RecipientPickerViewController *recipientPicker;
|
|
@property (nonatomic, readonly) AvatarImageView *avatarView;
|
|
@property (nonatomic, readonly) UIImageView *cameraImageView;
|
|
@property (nonatomic, readonly) UITextField *groupNameTextField;
|
|
|
|
@property (nonatomic, readonly) NewGroupSeed *groupSeed;
|
|
|
|
@property (nonatomic, nullable) UIImage *groupAvatar;
|
|
@property (nonatomic) NSMutableSet<PickedRecipient *> *memberRecipients;
|
|
|
|
@property (nonatomic) BOOL hasUnsavedChanges;
|
|
@property (nonatomic) BOOL hasAppeared;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation NewGroupViewController
|
|
|
|
#pragma mark - Dependencies
|
|
|
|
- (SDSDatabaseStorage *)databaseStorage
|
|
{
|
|
return SDSDatabaseStorage.shared;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
[self commonInit];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
[self commonInit];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)commonInit
|
|
{
|
|
_groupSeed = [NewGroupSeed new];
|
|
|
|
_messageSender = SSKEnvironment.shared.messageSender;
|
|
_avatarViewHelper = [AvatarViewHelper new];
|
|
_avatarViewHelper.delegate = self;
|
|
|
|
self.memberRecipients = [NSMutableSet new];
|
|
}
|
|
|
|
#pragma mark - View Lifecycle
|
|
|
|
- (void)loadView
|
|
{
|
|
[super loadView];
|
|
|
|
self.title = [MessageStrings newGroupDefaultTitle];
|
|
|
|
self.view.backgroundColor = Theme.backgroundColor;
|
|
|
|
self.navigationItem.rightBarButtonItem =
|
|
[[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NEW_GROUP_CREATE_BUTTON",
|
|
@"The title for the 'create group' button.")
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(createNewGroup)
|
|
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"create")];
|
|
self.navigationItem.rightBarButtonItem.imageInsets = UIEdgeInsetsMake(0, -10, 0, 10);
|
|
self.navigationItem.rightBarButtonItem.accessibilityLabel
|
|
= NSLocalizedString(@"FINISH_GROUP_CREATION_LABEL", @"Accessibility label for finishing new group");
|
|
|
|
// First section.
|
|
|
|
UIView *firstSection = [self firstSectionHeader];
|
|
[self.view addSubview:firstSection];
|
|
[firstSection autoSetDimension:ALDimensionHeight toSize:100.f];
|
|
[firstSection autoPinWidthToSuperview];
|
|
[firstSection autoPinToTopLayoutGuideOfViewController:self withInset:0];
|
|
|
|
_recipientPicker = [RecipientPickerViewController new];
|
|
self.recipientPicker.delegate = self;
|
|
self.recipientPicker.shouldShowGroups = NO;
|
|
self.recipientPicker.allowsSelectingUnregisteredPhoneNumbers = NO;
|
|
self.recipientPicker.shouldShowAlphabetSlider = NO;
|
|
|
|
[self addChildViewController:self.recipientPicker];
|
|
[self.view addSubview:self.recipientPicker.view];
|
|
[self.recipientPicker.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
|
[self.recipientPicker.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
|
[self.recipientPicker.view autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:firstSection];
|
|
[self.recipientPicker.view autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
|
}
|
|
|
|
- (UIView *)firstSectionHeader
|
|
{
|
|
UIView *firstSectionHeader = [UIView new];
|
|
firstSectionHeader.userInteractionEnabled = YES;
|
|
[firstSectionHeader
|
|
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(headerWasTapped:)]];
|
|
firstSectionHeader.backgroundColor = [Theme backgroundColor];
|
|
UIView *threadInfoView = [UIView new];
|
|
[firstSectionHeader addSubview:threadInfoView];
|
|
[threadInfoView autoPinWidthToSuperviewWithMargin:16.f];
|
|
[threadInfoView autoPinHeightToSuperviewWithMargin:16.f];
|
|
|
|
AvatarImageView *avatarView = [AvatarImageView new];
|
|
_avatarView = avatarView;
|
|
|
|
[threadInfoView addSubview:avatarView];
|
|
[avatarView autoVCenterInSuperview];
|
|
[avatarView autoPinLeadingToSuperviewMargin];
|
|
[avatarView autoSetDimension:ALDimensionWidth toSize:kLargeAvatarSize];
|
|
[avatarView autoSetDimension:ALDimensionHeight toSize:kLargeAvatarSize];
|
|
|
|
UIImageView *cameraImageView = [UIImageView new];
|
|
[cameraImageView setTemplateImageName:@"camera-outline-24" tintColor:Theme.secondaryTextAndIconColor];
|
|
[threadInfoView addSubview:cameraImageView];
|
|
|
|
[cameraImageView autoSetDimensionsToSize:CGSizeMake(32, 32)];
|
|
cameraImageView.contentMode = UIViewContentModeCenter;
|
|
cameraImageView.backgroundColor = Theme.backgroundColor;
|
|
cameraImageView.layer.cornerRadius = 16;
|
|
cameraImageView.layer.shadowColor =
|
|
[(Theme.isDarkThemeEnabled ? Theme.darkThemeWashColor : Theme.primaryTextColor) CGColor];
|
|
cameraImageView.layer.shadowOffset = CGSizeMake(1, 1);
|
|
cameraImageView.layer.shadowOpacity = 0.5;
|
|
cameraImageView.layer.shadowRadius = 4;
|
|
|
|
[cameraImageView autoPinTrailingToEdgeOfView:avatarView];
|
|
[cameraImageView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:avatarView];
|
|
_cameraImageView = cameraImageView;
|
|
|
|
[self updateAvatarView];
|
|
|
|
[avatarView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
|
action:@selector(avatarTouched:)]];
|
|
avatarView.userInteractionEnabled = YES;
|
|
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, avatarView);
|
|
|
|
UITextField *groupNameTextField = [OWSTextField new];
|
|
_groupNameTextField = groupNameTextField;
|
|
groupNameTextField.textColor = Theme.primaryTextColor;
|
|
groupNameTextField.font = [UIFont ows_dynamicTypeTitle2Font];
|
|
groupNameTextField.attributedPlaceholder =
|
|
[[NSAttributedString alloc] initWithString:NSLocalizedString(@"NEW_GROUP_NAMEGROUP_REQUEST_DEFAULT",
|
|
@"Placeholder text for group name field")
|
|
attributes:@{
|
|
NSForegroundColorAttributeName : Theme.secondaryTextAndIconColor,
|
|
}];
|
|
groupNameTextField.delegate = self;
|
|
[groupNameTextField addTarget:self
|
|
action:@selector(groupNameDidChange:)
|
|
forControlEvents:UIControlEventEditingChanged];
|
|
[threadInfoView addSubview:groupNameTextField];
|
|
[groupNameTextField autoVCenterInSuperview];
|
|
[groupNameTextField autoPinTrailingToSuperviewMargin];
|
|
[groupNameTextField autoPinLeadingToTrailingEdgeOfView:avatarView offset:16.f];
|
|
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, groupNameTextField);
|
|
|
|
return firstSectionHeader;
|
|
}
|
|
|
|
- (void)headerWasTapped:(UIGestureRecognizer *)sender
|
|
{
|
|
if (sender.state == UIGestureRecognizerStateRecognized) {
|
|
[self.groupNameTextField becomeFirstResponder];
|
|
}
|
|
}
|
|
|
|
- (void)avatarTouched:(UIGestureRecognizer *)sender
|
|
{
|
|
if (sender.state == UIGestureRecognizerStateRecognized) {
|
|
[self showChangeAvatarUI];
|
|
}
|
|
}
|
|
|
|
- (void)removeRecipient:(PickedRecipient *)recipient
|
|
{
|
|
[self.memberRecipients removeObject:recipient];
|
|
self.recipientPicker.pickedRecipients = self.memberRecipients.allObjects;
|
|
}
|
|
|
|
- (void)addRecipient:(PickedRecipient *)recipient
|
|
{
|
|
[self.memberRecipients addObject:recipient];
|
|
self.hasUnsavedChanges = YES;
|
|
self.recipientPicker.pickedRecipients = self.memberRecipients.allObjects;
|
|
}
|
|
|
|
#pragma mark - Methods
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:animated];
|
|
|
|
if (self.navigationController.viewControllers.count == 1) {
|
|
self.navigationItem.leftBarButtonItem =
|
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
|
target:self
|
|
action:@selector(dismissPressed)];
|
|
}
|
|
}
|
|
|
|
- (void)dismissPressed
|
|
{
|
|
[self.groupNameTextField resignFirstResponder];
|
|
|
|
if (!self.hasUnsavedChanges) {
|
|
// If user made no changes, dismiss.
|
|
[self dismissViewControllerAnimated:YES completion:nil];
|
|
return;
|
|
}
|
|
|
|
__weak NewGroupViewController *weakSelf = self;
|
|
[OWSActionSheets showPendingChangesActionSheetWithDiscardAction:^{
|
|
[weakSelf dismissViewControllerAnimated:YES completion:nil];
|
|
}];
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
|
|
if (!self.hasAppeared) {
|
|
[self.groupNameTextField becomeFirstResponder];
|
|
self.hasAppeared = YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (void)createNewGroup
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
[self.groupNameTextField acceptAutocorrectSuggestion];
|
|
|
|
NSMutableArray<SignalServiceAddress *> *members = [[self.memberRecipients.allObjects map:^(PickedRecipient *recipient) {
|
|
OWSAssertDebug(recipient.address.isValid);
|
|
return recipient.address;
|
|
}] mutableCopy];
|
|
[members addObject:[self.recipientPicker.contactsViewHelper localAddress]];
|
|
|
|
NSString *groupName = self.groupNameTextField.text;
|
|
|
|
[self createGroupThreadWithName:groupName avatarImage:self.groupAvatar members:members newGroupSeed:self.groupSeed];
|
|
}
|
|
|
|
#pragma mark - Group Avatar
|
|
|
|
- (void)showChangeAvatarUI
|
|
{
|
|
[self.avatarViewHelper showChangeAvatarUI];
|
|
}
|
|
|
|
- (void)setGroupAvatar:(nullable UIImage *)groupAvatar
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
_groupAvatar = groupAvatar;
|
|
|
|
self.hasUnsavedChanges = YES;
|
|
|
|
[self updateAvatarView];
|
|
}
|
|
|
|
- (void)updateAvatarView
|
|
{
|
|
UIImage *_Nullable groupAvatar = self.groupAvatar;
|
|
self.cameraImageView.hidden = groupAvatar != nil;
|
|
|
|
if (!groupAvatar) {
|
|
NSData *groupId = self.groupSeed.possibleGroupId;
|
|
NSString *conversationColorName = [TSGroupThread defaultConversationColorNameForGroupId:groupId];
|
|
groupAvatar = [OWSGroupAvatarBuilder defaultAvatarForGroupId:groupId
|
|
conversationColorName:conversationColorName
|
|
diameter:kLargeAvatarSize];
|
|
}
|
|
|
|
self.avatarView.image = groupAvatar;
|
|
}
|
|
|
|
#pragma mark - Event Handling
|
|
|
|
- (void)backButtonPressed
|
|
{
|
|
[self.groupNameTextField resignFirstResponder];
|
|
|
|
if (!self.hasUnsavedChanges) {
|
|
// If user made no changes, return to conversation settings view.
|
|
[self.navigationController popViewControllerAnimated:YES];
|
|
return;
|
|
}
|
|
|
|
__weak NewGroupViewController *weakSelf = self;
|
|
[OWSActionSheets showPendingChangesActionSheetWithDiscardAction:^{
|
|
[weakSelf.navigationController popViewControllerAnimated:YES];
|
|
}];
|
|
}
|
|
|
|
- (void)groupNameDidChange:(id)sender
|
|
{
|
|
self.hasUnsavedChanges = YES;
|
|
}
|
|
|
|
#pragma mark - Text Field Delegate
|
|
|
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField
|
|
{
|
|
[self.groupNameTextField resignFirstResponder];
|
|
return NO;
|
|
}
|
|
#pragma mark - AvatarViewHelperDelegate
|
|
|
|
- (nullable NSString *)avatarActionSheetTitle
|
|
{
|
|
return NSLocalizedString(
|
|
@"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar");
|
|
}
|
|
|
|
- (void)avatarDidChange:(UIImage *)image
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(image);
|
|
|
|
self.groupAvatar = image;
|
|
}
|
|
|
|
- (UIViewController *)fromViewController
|
|
{
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)hasClearAvatarAction
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - RecipientPickerDelegate
|
|
|
|
- (void)recipientPicker:(RecipientPickerViewController *)recipientPickerViewController
|
|
didSelectRecipient:(PickedRecipient *)recipient
|
|
{
|
|
OWSAssertDebug(recipient.address.isValid);
|
|
|
|
__weak __typeof(self) weakSelf;
|
|
BOOL isCurrentMember = [self.memberRecipients containsObject:recipient];
|
|
BOOL isBlocked = [self.recipientPicker.contactsViewHelper isSignalServiceAddressBlocked:recipient.address];
|
|
if (isCurrentMember) {
|
|
[self removeRecipient:recipient];
|
|
} else if (isBlocked) {
|
|
[BlockListUIUtils showUnblockAddressActionSheet:recipient.address
|
|
fromViewController:self
|
|
blockingManager:self.recipientPicker.contactsViewHelper.blockingManager
|
|
contactsManager:self.recipientPicker.contactsViewHelper.contactsManager
|
|
completionBlock:^(BOOL isStillBlocked) {
|
|
if (!isStillBlocked) {
|
|
[weakSelf addRecipient:recipient];
|
|
[weakSelf.navigationController popToViewController:self animated:YES];
|
|
}
|
|
}];
|
|
} else {
|
|
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
|
presentAlertIfNecessaryWithAddress:recipient.address
|
|
confirmationText:NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_"
|
|
@"ADD_TO_GROUP_ACTION",
|
|
@"button title to confirm adding "
|
|
@"a recipient to a group when "
|
|
@"their safety "
|
|
@"number has recently changed")
|
|
contactsManager:self.recipientPicker.contactsViewHelper.contactsManager
|
|
completion:^(BOOL didConfirmIdentity) {
|
|
if (didConfirmIdentity) {
|
|
[weakSelf addRecipient:recipient];
|
|
[weakSelf.navigationController popToViewController:self animated:YES];
|
|
}
|
|
}];
|
|
if (didShowSNAlert) {
|
|
return;
|
|
}
|
|
|
|
[self addRecipient:recipient];
|
|
[self.navigationController popToViewController:self animated:YES];
|
|
}
|
|
}
|
|
|
|
- (BOOL)recipientPicker:(RecipientPickerViewController *)recipientPickerViewController
|
|
canSelectRecipient:(PickedRecipient *)recipient
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (nullable NSString *)recipientPicker:(RecipientPickerViewController *)recipientPickerViewController
|
|
accessoryMessageForRecipient:(PickedRecipient *)recipient
|
|
{
|
|
OWSAssertDebug(recipient.address.isValid);
|
|
|
|
BOOL isCurrentMember = [self.memberRecipients containsObject:recipient];
|
|
BOOL isBlocked = [self.recipientPicker.contactsViewHelper isSignalServiceAddressBlocked:recipient.address];
|
|
|
|
if (isCurrentMember) {
|
|
// In the "contacts" section, we label members as such when editing an existing
|
|
// group.
|
|
return NSLocalizedString(@"NEW_GROUP_MEMBER_LABEL", @"An indicator that a user is a member of the new group.");
|
|
} else if (isBlocked) {
|
|
return MessageStrings.conversationIsBlocked;
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (void)recipientPickerTableViewWillBeginDragging:(RecipientPickerViewController *)recipientPickerViewController
|
|
{
|
|
[self.groupNameTextField resignFirstResponder];
|
|
}
|
|
|
|
#pragma mark - OWSNavigationView
|
|
|
|
- (BOOL)shouldCancelNavigationBack
|
|
{
|
|
BOOL result = self.hasUnsavedChanges;
|
|
if (self.hasUnsavedChanges) {
|
|
[self backButtonPressed];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|