123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2014-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 <https://www.gnu.org/licenses/>.
- #import "BallotCreateViewController.h"
- #import "BallotCreateTableCell.h"
- #import "BallotChoice.h"
- #import "EntityManager.h"
- #import "Ballot.h"
- #import "MessageSender.h"
- #import "AppDelegate.h"
- #import "NaClCrypto.h"
- #import "ProtocolDefines.h"
- #import "MyIdentityStore.h"
- #import "RectUtil.h"
- #import "BallotCreateDetailViewController.h"
- #import "BallotManager.h"
- #import "ContactStore.h"
- #import "Contact.h"
- #import "AppGroup.h"
- #import "FeatureMask.h"
- #import "Utils.h"
- #define MIN_NUMBER_CHARACTERS 0
- #define MIN_NUMBER_CHOICES 2
- #define BALLOT_CREATE_TABLE_CELL_ID @"BallotCreateTableCellId"
- // note: the new ballot object is created on a temorary NSManagedObjectContext, on save it is moved to the main context and saved there
- // - the conversation object might get updated while editing the ballot
- // - if using the main context a unwanted save may occur while the ballot is in in invalid state
- @interface BallotCreateViewController () <UITableViewDelegate, UITableViewDataSource, BallotCreateTableCellDelegate>
- @property NSMutableArray *choices;
- @property EntityManager *entityManager;
- @property BOOL isNewBallot;
- @property Ballot *ballot;
- @property Conversation *conversation;
- @property BOOL didShowFeatureMaskAlert;
- @property (nonatomic, strong) NSIndexPath *indexPathForPicker;
- @property (nonatomic, strong) NSDate *lastSelectedDate;
- @property (nonatomic) BOOL lastPickerWithoutTime;
- @end
- @implementation BallotCreateViewController
- + (instancetype) ballotCreateViewControllerForConversation:(Conversation *)conversation {
- BallotCreateViewController *viewController = [self ballotCreateViewController];
-
- Conversation *ownContextConversation = (Conversation *)[viewController.entityManager.entityFetcher getManagedObjectById:conversation.objectID];
- viewController.conversation = ownContextConversation;
- viewController.ballot = [viewController newBallot];
- viewController.ballot.conversation = ownContextConversation;
- viewController.isNewBallot = YES;
-
- return viewController;
- }
- + (instancetype) ballotCreateViewControllerForBallot:(Ballot *)ballot {
- BallotCreateViewController *viewController = [self ballotCreateViewController];
- viewController.ballot = (Ballot *)[viewController.entityManager.entityFetcher getManagedObjectById:ballot.objectID];
- viewController.conversation = viewController.ballot.conversation;
- viewController.isNewBallot = NO;
-
- return viewController;
- }
- + (instancetype) ballotCreateViewController {
- UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Ballot" bundle:nil];
-
- BallotCreateViewController *viewController = (BallotCreateViewController *) [storyboard instantiateViewControllerWithIdentifier:@"BallotCreateViewController"];
-
- viewController.entityManager = [[EntityManager alloc] init];
- viewController.didShowFeatureMaskAlert = NO;
-
- return viewController;
- }
- -(void)dealloc {
- [self removeFromObserver];
- }
- - (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
-
- [self dismissPicker];
-
- if (![[AppDelegate sharedAppDelegate] active]) {
- [_entityManager rollback];
- }
- }
- - (void)viewDidLoad {
- [super viewDidLoad];
-
- _cancelButton.target = self;
- _cancelButton.action = @selector(cancelPressed);
- _sendButton.target = self;
- _sendButton.action = @selector(sendPressed);
- [_addButton addTarget:self action:@selector(addPressed) forControlEvents:UIControlEventTouchUpInside];
- _choiceTableView.delegate = self;
- _choiceTableView.dataSource = self;
-
- [self updateUIStrings];
-
- if (_isNewBallot == NO) {
- [self setOnlyEditing];
- }
- [self registerForKeyboardNotifications];
-
- [_titleTextView becomeFirstResponder];
-
- [self setupColors];
- }
- - (void)setupColors {
- self.view.backgroundColor = [Colors background];
- _buttonView.backgroundColor = [Colors backgroundDark];
- _titleTextView.backgroundColor = [Colors background];
- _choiceTableView.backgroundColor = [Colors background];
-
- _hairlineTop.backgroundColor = [Colors fontLight];
- _hairlineTop.frame = [RectUtil setHeightOf:_hairlineTop.frame height:0.5];
- [_addButton setTintColor:[Colors main]];
- [_optionsButton setTitleColor:[Colors main] forState:UIControlStateNormal];
- _titleTextView.textColor = [Colors fontNormal];
- _headerView.backgroundColor = [Colors background];
-
- [Colors updateKeyboardAppearanceFor:_titleTextView];
- }
- - (void)setOnlyEditing {
- _optionsButton.enabled = NO;
- _addButton.enabled = NO;
- }
-
- - (void)viewWillAppear:(BOOL)animated {
- [self updateContent];
-
- [self checkFeatureMasks];
-
- _indexPathForPicker = nil;
-
- [super viewWillAppear:animated];
- }
- - (void)updateUIStrings {
- if (_isNewBallot) {
- [_sendButton setTitle:NSLocalizedString(@"send", nil)];
- } else {
- [_sendButton setTitle:NSLocalizedString(@"save", nil)];
- }
-
- UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
- _titleTextView.attributedPlaceholder =[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"ballot_placeholder_title", @"Ballot", nil) attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:fontDescriptor.pointSize]}];
-
- [_optionsButton setTitle:NSLocalizedStringFromTable(@"ballot_options", @"Ballot", nil) forState:UIControlStateNormal];
-
- _addButton.accessibilityValue = NSLocalizedStringFromTable(@"ballot_add_choice", @"Ballot", nil);
- [self setTitle:NSLocalizedStringFromTable(@"ballot_create", @"Ballot", nil)];
- }
- - (void)updateContent {
- [_titleTextView setText: _ballot.title];
-
- NSArray *sortedChoices = [_ballot choicesSortedByOrder];
- if (_choices != nil) {
- for (BallotChoice *choice in _choices) {
- if ([sortedChoices containsObject: choice] == NO) {
- [choice.managedObjectContext deleteObject: choice];
- }
- }
- }
- _choices = [NSMutableArray arrayWithArray: sortedChoices];
-
- /* show at least two cells */
- for (NSInteger i=[_choices count]; i<MIN_NUMBER_CHOICES; i++) {
- [self addChoice];
- }
-
- if (_conversation == nil) {
- //ballot has no conversation
- self.sendButton.enabled = NO;
- }
-
- [_choiceTableView reloadData];
- }
- - (void)addChoice {
- BallotChoice *choice = [_entityManager.entityCreator ballotChoice];
- [_choices addObject: choice];
- }
- - (void)updateEntityObjects {
- _ballot.title = _titleTextView.text;
- NSSet *verifiedChoices = [self verifiedChoices];
- NSInteger i=0;
- for (BallotChoice *choice in _choices) {
- if ([verifiedChoices containsObject:choice]) {
- choice.ballot = _ballot;
- choice.orderPosition = [NSNumber numberWithInteger: i];
- i++;
- } else {
- [choice.managedObjectContext deleteObject: choice];
- }
- }
-
- _ballot.choices = verifiedChoices;
- }
- - (NSSet *)verifiedChoices {
- NSMutableSet *verifiedChoices = [NSMutableSet set];
- for (BallotChoice *choice in _choices) {
- if (choice.name && [choice.name length] > MIN_NUMBER_CHARACTERS) {
- [verifiedChoices addObject:choice];
- }
- }
-
- return verifiedChoices;
- }
- - (BOOL)isContentValid {
- if ([[self verifiedChoices] count] < MIN_NUMBER_CHOICES) {
- NSString *message = NSLocalizedStringFromTable(@"ballot_validation_not_enough_choices", @"Ballot", nil);
- [self showAlert: message];
- return NO;
- } else if (_titleTextView.text <= MIN_NUMBER_CHARACTERS) {
- NSString *message = NSLocalizedStringFromTable(@"ballot_validation_title_missing", @"Ballot", nil);
- [self showAlert: message];
- return NO;
- }
-
- return YES;
- }
- - (void)checkFeatureMasks {
- [FeatureMask checkFeatureMask:FEATURE_MASK_BALLOT forContacts:_ballot.participants onCompletion:^(NSArray *unsupportedContacts) {
- if ([unsupportedContacts count] > 0) {
- [self showFeatureMaskAlertForContacts: unsupportedContacts];
- }
- }];
- }
- - (void)showAlert:(NSString *)message {
- NSString *title = NSLocalizedStringFromTable(@"ballot_validation_error_title", @"Ballot", nil);
- [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:nil];
- }
- - (void)showFeatureMaskAlertForContacts:(NSArray *)contacts {
- NSString *messageFormat;
- if ([contacts count] == [_ballot.participants count]) {
- messageFormat = NSLocalizedStringFromTable(@"ballot_feature_level_error_message", @"Ballot", nil);
- } else {
- // show warning only once
- if (_didShowFeatureMaskAlert) {
- return;
- }
-
- messageFormat = NSLocalizedStringFromTable(@"ballot_feature_level_warning_message", @"Ballot", nil);
- }
-
- NSString *participantNames = [Utils stringFromContacts:contacts];
- NSString *message = [NSString stringWithFormat:messageFormat, participantNames];
-
- NSString *title = NSLocalizedStringFromTable(@"ballot_feature_level_warning_title", @"Ballot", nil);
- [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:nil];
- _didShowFeatureMaskAlert = YES;
- }
- - (Ballot *)newBallot {
- Ballot *ballot = [_entityManager.entityCreator ballot];
- ballot.id = [[NaClCrypto sharedCrypto] randomBytes:kBallotIdLen];
- ballot.createDate = [NSDate date];
- ballot.creatorId = [MyIdentityStore sharedMyIdentityStore].identity;
- NSUserDefaults *defaults = [AppGroup userDefaults];
- NSNumber *type = [defaults objectForKey:@"ballotLastType"];
- if (type) {
- ballot.type = type;
- }
- NSNumber *assessmentType = [defaults objectForKey:@"ballotLastAssessmentType"];
- if (assessmentType) {
- ballot.assessmentType = assessmentType;
- }
-
- return ballot;
- }
- - (void)addChoiceToTable {
- [_choiceTableView beginUpdates];
-
- NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[_choices count] inSection:0];
- [self addChoice];
- [_choiceTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
-
- [_choiceTableView endUpdates];
- }
- - (void)updateAvailableCells {
- NSSet *verifiedChoices = [self verifiedChoices];
- if ([verifiedChoices count] >= [_choices count]) {
- [self addChoiceToTable];
- }
- }
- - (void)dismissPicker {
- if (_indexPathForPicker) {
- BallotCreateTableCell *selectedCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
- [selectedCell showDatePicker:nil];
- _indexPathForPicker = nil;
- }
- }
- #pragma mark - button actions
- - (void)addPressed {
- [self addChoiceToTable];
-
- NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
- [self setFirstResponderAfterIndexPath: indexPath];
- }
- - (void)sendPressed {
- if ([self isContentValid] == NO) {
- return;
- }
-
- [self updateEntityObjects];
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults setObject:_ballot.type forKey:@"ballotLastType"];
- [defaults setObject:_ballot.assessmentType forKey:@"ballotLastAssessmentType"];
-
- [_entityManager performSyncBlockAndSafe:nil];
-
- [MessageSender sendCreateMessageForBallot:_ballot];
-
- [self.navigationController dismissViewControllerAnimated:YES completion:nil];
- }
- - (void)cancelPressed {
- [_entityManager rollback];
-
- [self.navigationController dismissViewControllerAnimated:YES completion:nil];
- }
- #pragma mark - table cell callback
- - (void)didUpdateCell:(BallotCreateTableCell *)cell {
- NSIndexPath *indexPath = [_choiceTableView indexPathForCell: cell];
-
- if ([cell.choiceTextField isFirstResponder]) {
- [self updateAvailableCells];
- [self setFirstResponderAfterIndexPath: indexPath];
- }
- }
- - (void)showPickerForCell:(BallotCreateTableCell *)cell {
- if (_indexPathForPicker) {
- BallotCreateTableCell *lastCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
- [lastCell showDatePicker:nil];
- }
-
- [_choiceTableView endEditing:YES];
- [_choiceTableView resignFirstResponder];
- [_titleTextView resignFirstResponder];
-
- if (_lastSelectedDate) {
- [cell setInputText:_lastSelectedDate allDay:_lastPickerWithoutTime];
- }
-
- [_choiceTableView beginUpdates];
- _indexPathForPicker = [_choiceTableView indexPathForCell:cell];
- [_choiceTableView endUpdates];
-
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
- }
- - (void)hidePickerForCell:(BallotCreateTableCell *)cell {
- _lastSelectedDate = cell.datePicker.date;
- _lastPickerWithoutTime = cell.allDaySwitch.on;
- [_choiceTableView beginUpdates];
- _indexPathForPicker = nil;
- [_choiceTableView endUpdates];
-
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
- }
- - (void)setFirstResponderAfterIndexPath:(NSIndexPath *)indexPath {
- NSInteger index = indexPath.row;
- while (index < [_choices count]) {
- NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
- BallotCreateTableCell *nextCell = (BallotCreateTableCell *)[_choiceTableView cellForRowAtIndexPath: newIndexPath];
- if ([nextCell.choiceTextField.text length] <= 0) {
- [nextCell.choiceTextField becomeFirstResponder];
-
- break;
- }
- index++;
- }
- }
- #pragma mark - table view data source / delegate
- - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
-
- [Colors updateTableViewCell:cell];
- }
- - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
- if (_indexPathForPicker && indexPath.section == _indexPathForPicker.section && indexPath.row == _indexPathForPicker.row) {
- if (@available(iOS 14.0, *)) {
- return 450.0;
- } else {
- return 300.0;
- }
- }
- return UITableViewAutomaticDimension;
- }
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- {
- return [_choices count];
- }
- - (UITableViewCell *)tableView: (UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- BallotCreateTableCell *cell = [tableView dequeueReusableCellWithIdentifier: BALLOT_CREATE_TABLE_CELL_ID];
- if (cell == nil) {
- //Fallback
- cell = [[BallotCreateTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: BALLOT_CREATE_TABLE_CELL_ID];
- }
-
- BallotChoice *choice = [_choices objectAtIndex: indexPath.row];
- cell.choice = choice;
- cell.delegate = self;
-
- if (_isNewBallot == NO) {
- cell.userInteractionEnabled = NO;
- }
-
- return cell;
- }
- - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
- if (editingStyle == UITableViewCellEditingStyleDelete) {
- BallotChoice *choice = [_choices objectAtIndex:indexPath.row];
- [[_entityManager entityDestroyer] deleteObjectWithObject:choice];
-
- [_choices removeObjectAtIndex: indexPath.row];
- [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
- }
- }
- - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (_isNewBallot) {
- return UITableViewCellEditingStyleDelete;
- } else {
- return UITableViewCellEditingStyleNone;
- }
- }
- - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
- [self dismissPicker];
- return YES;
- }
- - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
- BallotChoice *choiceToMove = [_choices objectAtIndex:sourceIndexPath.row];
- [_choices removeObjectAtIndex:sourceIndexPath.row];
- [_choices insertObject:choiceToMove atIndex:destinationIndexPath.row];
- }
- # pragma mark Keyboard Notifications
- - (void)registerForKeyboardNotifications {
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(keyboardWillShow:)
- name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(keyboardDidShow:)
- name:UIKeyboardDidShowNotification object:nil];
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(keyboardWillHide:)
- name:UIKeyboardWillHideNotification object:nil];
- }
- - (void)removeFromObserver {
- [[NSNotificationCenter defaultCenter] removeObserver: self];
- }
- - (void)keyboardWillShow: (NSNotification*) aNotification {
- NSDictionary* info = [aNotification userInfo];
-
- CGRect keyboardRect = [[info objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
- CGRect keyboardRectConverted = [_choiceTableView convertRect: keyboardRect fromView: nil];
- CGSize keyboardSize = keyboardRectConverted.size;
-
- UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0);
- _choiceTableView.contentInset = contentInsets;
- _choiceTableView.scrollIndicatorInsets = contentInsets;
- }
- - (void)keyboardDidShow:(NSNotification *)aNotification {
- if (_indexPathForPicker) {
- BallotCreateTableCell *lastCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
- if (@available(iOS 14.0, *)) {
- if (!lastCell.choiceTextField.isFirstResponder) {
- [_choiceTableView scrollToRowAtIndexPath:_indexPathForPicker atScrollPosition:UITableViewScrollPositionBottom animated:true];
- return;
- }
- }
- [lastCell showDatePicker:nil];
- }
- }
-
- - (void)keyboardWillHide:(NSNotification*)aNotification
- {
- _choiceTableView.contentInset = UIEdgeInsetsZero;
- _choiceTableView.scrollIndicatorInsets = UIEdgeInsetsZero;
- }
- #pragma mark - Navigation
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
- if ([segue.identifier isEqualToString:@"ballotOptionsSegue"]) {
- [self updateEntityObjects];
-
- BallotCreateDetailViewController *controller = (BallotCreateDetailViewController*)segue.destinationViewController;
- controller.ballot = _ballot;
- }
- }
- @end
|