BallotCreateViewController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2014-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "BallotCreateViewController.h"
  21. #import "BallotCreateTableCell.h"
  22. #import "BallotChoice.h"
  23. #import "EntityManager.h"
  24. #import "Ballot.h"
  25. #import "MessageSender.h"
  26. #import "AppDelegate.h"
  27. #import "NaClCrypto.h"
  28. #import "ProtocolDefines.h"
  29. #import "MyIdentityStore.h"
  30. #import "RectUtil.h"
  31. #import "BallotCreateDetailViewController.h"
  32. #import "BallotManager.h"
  33. #import "ContactStore.h"
  34. #import "Contact.h"
  35. #import "AppGroup.h"
  36. #import "FeatureMask.h"
  37. #import "Utils.h"
  38. #define MIN_NUMBER_CHARACTERS 0
  39. #define MIN_NUMBER_CHOICES 2
  40. #define BALLOT_CREATE_TABLE_CELL_ID @"BallotCreateTableCellId"
  41. // note: the new ballot object is created on a temorary NSManagedObjectContext, on save it is moved to the main context and saved there
  42. // - the conversation object might get updated while editing the ballot
  43. // - if using the main context a unwanted save may occur while the ballot is in in invalid state
  44. @interface BallotCreateViewController () <UITableViewDelegate, UITableViewDataSource, BallotCreateTableCellDelegate>
  45. @property NSMutableArray *choices;
  46. @property EntityManager *entityManager;
  47. @property BOOL isNewBallot;
  48. @property Ballot *ballot;
  49. @property Conversation *conversation;
  50. @property BOOL didShowFeatureMaskAlert;
  51. @property (nonatomic, strong) NSIndexPath *indexPathForPicker;
  52. @property (nonatomic, strong) NSDate *lastSelectedDate;
  53. @property (nonatomic) BOOL lastPickerWithoutTime;
  54. @end
  55. @implementation BallotCreateViewController
  56. + (instancetype) ballotCreateViewControllerForConversation:(Conversation *)conversation {
  57. BallotCreateViewController *viewController = [self ballotCreateViewController];
  58. Conversation *ownContextConversation = (Conversation *)[viewController.entityManager.entityFetcher getManagedObjectById:conversation.objectID];
  59. viewController.conversation = ownContextConversation;
  60. viewController.ballot = [viewController newBallot];
  61. viewController.ballot.conversation = ownContextConversation;
  62. viewController.isNewBallot = YES;
  63. return viewController;
  64. }
  65. + (instancetype) ballotCreateViewControllerForBallot:(Ballot *)ballot {
  66. BallotCreateViewController *viewController = [self ballotCreateViewController];
  67. viewController.ballot = (Ballot *)[viewController.entityManager.entityFetcher getManagedObjectById:ballot.objectID];
  68. viewController.conversation = viewController.ballot.conversation;
  69. viewController.isNewBallot = NO;
  70. return viewController;
  71. }
  72. + (instancetype) ballotCreateViewController {
  73. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Ballot" bundle:nil];
  74. BallotCreateViewController *viewController = (BallotCreateViewController *) [storyboard instantiateViewControllerWithIdentifier:@"BallotCreateViewController"];
  75. viewController.entityManager = [[EntityManager alloc] init];
  76. viewController.didShowFeatureMaskAlert = NO;
  77. return viewController;
  78. }
  79. -(void)dealloc {
  80. [self removeFromObserver];
  81. }
  82. - (void)viewWillDisappear:(BOOL)animated {
  83. [super viewWillDisappear:animated];
  84. [self dismissPicker];
  85. if (![[AppDelegate sharedAppDelegate] active]) {
  86. [_entityManager rollback];
  87. }
  88. }
  89. - (void)viewDidLoad {
  90. [super viewDidLoad];
  91. _cancelButton.target = self;
  92. _cancelButton.action = @selector(cancelPressed);
  93. _sendButton.target = self;
  94. _sendButton.action = @selector(sendPressed);
  95. [_addButton addTarget:self action:@selector(addPressed) forControlEvents:UIControlEventTouchUpInside];
  96. _choiceTableView.delegate = self;
  97. _choiceTableView.dataSource = self;
  98. [self updateUIStrings];
  99. if (_isNewBallot == NO) {
  100. [self setOnlyEditing];
  101. }
  102. [self registerForKeyboardNotifications];
  103. [_titleTextView becomeFirstResponder];
  104. [self setupColors];
  105. }
  106. - (void)setupColors {
  107. self.view.backgroundColor = [Colors background];
  108. _buttonView.backgroundColor = [Colors backgroundDark];
  109. _titleTextView.backgroundColor = [Colors background];
  110. _choiceTableView.backgroundColor = [Colors background];
  111. _hairlineTop.backgroundColor = [Colors fontLight];
  112. _hairlineTop.frame = [RectUtil setHeightOf:_hairlineTop.frame height:0.5];
  113. [_addButton setTintColor:[Colors main]];
  114. [_optionsButton setTitleColor:[Colors main] forState:UIControlStateNormal];
  115. _titleTextView.textColor = [Colors fontNormal];
  116. _headerView.backgroundColor = [Colors background];
  117. [Colors updateKeyboardAppearanceFor:_titleTextView];
  118. }
  119. - (void)setOnlyEditing {
  120. _optionsButton.enabled = NO;
  121. _addButton.enabled = NO;
  122. }
  123. - (void)viewWillAppear:(BOOL)animated {
  124. [self updateContent];
  125. [self checkFeatureMasks];
  126. _indexPathForPicker = nil;
  127. [super viewWillAppear:animated];
  128. }
  129. - (void)updateUIStrings {
  130. if (_isNewBallot) {
  131. [_sendButton setTitle:NSLocalizedString(@"send", nil)];
  132. } else {
  133. [_sendButton setTitle:NSLocalizedString(@"save", nil)];
  134. }
  135. UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
  136. _titleTextView.attributedPlaceholder =[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"ballot_placeholder_title", @"Ballot", nil) attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:fontDescriptor.pointSize]}];
  137. [_optionsButton setTitle:NSLocalizedStringFromTable(@"ballot_options", @"Ballot", nil) forState:UIControlStateNormal];
  138. _addButton.accessibilityValue = NSLocalizedStringFromTable(@"ballot_add_choice", @"Ballot", nil);
  139. [self setTitle:NSLocalizedStringFromTable(@"ballot_create", @"Ballot", nil)];
  140. }
  141. - (void)updateContent {
  142. [_titleTextView setText: _ballot.title];
  143. NSArray *sortedChoices = [_ballot choicesSortedByOrder];
  144. if (_choices != nil) {
  145. for (BallotChoice *choice in _choices) {
  146. if ([sortedChoices containsObject: choice] == NO) {
  147. [choice.managedObjectContext deleteObject: choice];
  148. }
  149. }
  150. }
  151. _choices = [NSMutableArray arrayWithArray: sortedChoices];
  152. /* show at least two cells */
  153. for (NSInteger i=[_choices count]; i<MIN_NUMBER_CHOICES; i++) {
  154. [self addChoice];
  155. }
  156. if (_conversation == nil) {
  157. //ballot has no conversation
  158. self.sendButton.enabled = NO;
  159. }
  160. [_choiceTableView reloadData];
  161. }
  162. - (void)addChoice {
  163. BallotChoice *choice = [_entityManager.entityCreator ballotChoice];
  164. [_choices addObject: choice];
  165. }
  166. - (void)updateEntityObjects {
  167. _ballot.title = _titleTextView.text;
  168. NSSet *verifiedChoices = [self verifiedChoices];
  169. NSInteger i=0;
  170. for (BallotChoice *choice in _choices) {
  171. if ([verifiedChoices containsObject:choice]) {
  172. choice.ballot = _ballot;
  173. choice.orderPosition = [NSNumber numberWithInteger: i];
  174. i++;
  175. } else {
  176. [choice.managedObjectContext deleteObject: choice];
  177. }
  178. }
  179. _ballot.choices = verifiedChoices;
  180. }
  181. - (NSSet *)verifiedChoices {
  182. NSMutableSet *verifiedChoices = [NSMutableSet set];
  183. for (BallotChoice *choice in _choices) {
  184. if (choice.name && [choice.name length] > MIN_NUMBER_CHARACTERS) {
  185. [verifiedChoices addObject:choice];
  186. }
  187. }
  188. return verifiedChoices;
  189. }
  190. - (BOOL)isContentValid {
  191. if ([[self verifiedChoices] count] < MIN_NUMBER_CHOICES) {
  192. NSString *message = NSLocalizedStringFromTable(@"ballot_validation_not_enough_choices", @"Ballot", nil);
  193. [self showAlert: message];
  194. return NO;
  195. } else if (_titleTextView.text <= MIN_NUMBER_CHARACTERS) {
  196. NSString *message = NSLocalizedStringFromTable(@"ballot_validation_title_missing", @"Ballot", nil);
  197. [self showAlert: message];
  198. return NO;
  199. }
  200. return YES;
  201. }
  202. - (void)checkFeatureMasks {
  203. [FeatureMask checkFeatureMask:FEATURE_MASK_BALLOT forContacts:_ballot.participants onCompletion:^(NSArray *unsupportedContacts) {
  204. if ([unsupportedContacts count] > 0) {
  205. [self showFeatureMaskAlertForContacts: unsupportedContacts];
  206. }
  207. }];
  208. }
  209. - (void)showAlert:(NSString *)message {
  210. NSString *title = NSLocalizedStringFromTable(@"ballot_validation_error_title", @"Ballot", nil);
  211. [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:nil];
  212. }
  213. - (void)showFeatureMaskAlertForContacts:(NSArray *)contacts {
  214. NSString *messageFormat;
  215. if ([contacts count] == [_ballot.participants count]) {
  216. messageFormat = NSLocalizedStringFromTable(@"ballot_feature_level_error_message", @"Ballot", nil);
  217. } else {
  218. // show warning only once
  219. if (_didShowFeatureMaskAlert) {
  220. return;
  221. }
  222. messageFormat = NSLocalizedStringFromTable(@"ballot_feature_level_warning_message", @"Ballot", nil);
  223. }
  224. NSString *participantNames = [Utils stringFromContacts:contacts];
  225. NSString *message = [NSString stringWithFormat:messageFormat, participantNames];
  226. NSString *title = NSLocalizedStringFromTable(@"ballot_feature_level_warning_title", @"Ballot", nil);
  227. [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:nil];
  228. _didShowFeatureMaskAlert = YES;
  229. }
  230. - (Ballot *)newBallot {
  231. Ballot *ballot = [_entityManager.entityCreator ballot];
  232. ballot.id = [[NaClCrypto sharedCrypto] randomBytes:kBallotIdLen];
  233. ballot.createDate = [NSDate date];
  234. ballot.creatorId = [MyIdentityStore sharedMyIdentityStore].identity;
  235. NSUserDefaults *defaults = [AppGroup userDefaults];
  236. NSNumber *type = [defaults objectForKey:@"ballotLastType"];
  237. if (type) {
  238. ballot.type = type;
  239. }
  240. NSNumber *assessmentType = [defaults objectForKey:@"ballotLastAssessmentType"];
  241. if (assessmentType) {
  242. ballot.assessmentType = assessmentType;
  243. }
  244. return ballot;
  245. }
  246. - (void)addChoiceToTable {
  247. [_choiceTableView beginUpdates];
  248. NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[_choices count] inSection:0];
  249. [self addChoice];
  250. [_choiceTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
  251. [_choiceTableView endUpdates];
  252. }
  253. - (void)updateAvailableCells {
  254. NSSet *verifiedChoices = [self verifiedChoices];
  255. if ([verifiedChoices count] >= [_choices count]) {
  256. [self addChoiceToTable];
  257. }
  258. }
  259. - (void)dismissPicker {
  260. if (_indexPathForPicker) {
  261. BallotCreateTableCell *selectedCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
  262. [selectedCell showDatePicker:nil];
  263. _indexPathForPicker = nil;
  264. }
  265. }
  266. #pragma mark - button actions
  267. - (void)addPressed {
  268. [self addChoiceToTable];
  269. NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  270. [self setFirstResponderAfterIndexPath: indexPath];
  271. }
  272. - (void)sendPressed {
  273. if ([self isContentValid] == NO) {
  274. return;
  275. }
  276. [self updateEntityObjects];
  277. NSUserDefaults *defaults = [AppGroup userDefaults];
  278. [defaults setObject:_ballot.type forKey:@"ballotLastType"];
  279. [defaults setObject:_ballot.assessmentType forKey:@"ballotLastAssessmentType"];
  280. [_entityManager performSyncBlockAndSafe:nil];
  281. [MessageSender sendCreateMessageForBallot:_ballot];
  282. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  283. }
  284. - (void)cancelPressed {
  285. [_entityManager rollback];
  286. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  287. }
  288. #pragma mark - table cell callback
  289. - (void)didUpdateCell:(BallotCreateTableCell *)cell {
  290. NSIndexPath *indexPath = [_choiceTableView indexPathForCell: cell];
  291. if ([cell.choiceTextField isFirstResponder]) {
  292. [self updateAvailableCells];
  293. [self setFirstResponderAfterIndexPath: indexPath];
  294. }
  295. }
  296. - (void)showPickerForCell:(BallotCreateTableCell *)cell {
  297. if (_indexPathForPicker) {
  298. BallotCreateTableCell *lastCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
  299. [lastCell showDatePicker:nil];
  300. }
  301. [_choiceTableView endEditing:YES];
  302. [_choiceTableView resignFirstResponder];
  303. [_titleTextView resignFirstResponder];
  304. if (_lastSelectedDate) {
  305. [cell setInputText:_lastSelectedDate allDay:_lastPickerWithoutTime];
  306. }
  307. [_choiceTableView beginUpdates];
  308. _indexPathForPicker = [_choiceTableView indexPathForCell:cell];
  309. [_choiceTableView endUpdates];
  310. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
  311. }
  312. - (void)hidePickerForCell:(BallotCreateTableCell *)cell {
  313. _lastSelectedDate = cell.datePicker.date;
  314. _lastPickerWithoutTime = cell.allDaySwitch.on;
  315. [_choiceTableView beginUpdates];
  316. _indexPathForPicker = nil;
  317. [_choiceTableView endUpdates];
  318. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
  319. }
  320. - (void)setFirstResponderAfterIndexPath:(NSIndexPath *)indexPath {
  321. NSInteger index = indexPath.row;
  322. while (index < [_choices count]) {
  323. NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
  324. BallotCreateTableCell *nextCell = (BallotCreateTableCell *)[_choiceTableView cellForRowAtIndexPath: newIndexPath];
  325. if ([nextCell.choiceTextField.text length] <= 0) {
  326. [nextCell.choiceTextField becomeFirstResponder];
  327. break;
  328. }
  329. index++;
  330. }
  331. }
  332. #pragma mark - table view data source / delegate
  333. - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  334. [Colors updateTableViewCell:cell];
  335. }
  336. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  337. if (_indexPathForPicker && indexPath.section == _indexPathForPicker.section && indexPath.row == _indexPathForPicker.row) {
  338. if (@available(iOS 14.0, *)) {
  339. return 450.0;
  340. } else {
  341. return 300.0;
  342. }
  343. }
  344. return UITableViewAutomaticDimension;
  345. }
  346. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  347. {
  348. return [_choices count];
  349. }
  350. - (UITableViewCell *)tableView: (UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  351. {
  352. BallotCreateTableCell *cell = [tableView dequeueReusableCellWithIdentifier: BALLOT_CREATE_TABLE_CELL_ID];
  353. if (cell == nil) {
  354. //Fallback
  355. cell = [[BallotCreateTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: BALLOT_CREATE_TABLE_CELL_ID];
  356. }
  357. BallotChoice *choice = [_choices objectAtIndex: indexPath.row];
  358. cell.choice = choice;
  359. cell.delegate = self;
  360. if (_isNewBallot == NO) {
  361. cell.userInteractionEnabled = NO;
  362. }
  363. return cell;
  364. }
  365. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
  366. if (editingStyle == UITableViewCellEditingStyleDelete) {
  367. BallotChoice *choice = [_choices objectAtIndex:indexPath.row];
  368. [[_entityManager entityDestroyer] deleteObjectWithObject:choice];
  369. [_choices removeObjectAtIndex: indexPath.row];
  370. [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
  371. }
  372. }
  373. - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
  374. {
  375. if (_isNewBallot) {
  376. return UITableViewCellEditingStyleDelete;
  377. } else {
  378. return UITableViewCellEditingStyleNone;
  379. }
  380. }
  381. - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
  382. [self dismissPicker];
  383. return YES;
  384. }
  385. - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
  386. BallotChoice *choiceToMove = [_choices objectAtIndex:sourceIndexPath.row];
  387. [_choices removeObjectAtIndex:sourceIndexPath.row];
  388. [_choices insertObject:choiceToMove atIndex:destinationIndexPath.row];
  389. }
  390. # pragma mark Keyboard Notifications
  391. - (void)registerForKeyboardNotifications {
  392. [[NSNotificationCenter defaultCenter] addObserver:self
  393. selector:@selector(keyboardWillShow:)
  394. name:UIKeyboardWillShowNotification object:nil];
  395. [[NSNotificationCenter defaultCenter] addObserver:self
  396. selector:@selector(keyboardDidShow:)
  397. name:UIKeyboardDidShowNotification object:nil];
  398. [[NSNotificationCenter defaultCenter] addObserver:self
  399. selector:@selector(keyboardWillHide:)
  400. name:UIKeyboardWillHideNotification object:nil];
  401. }
  402. - (void)removeFromObserver {
  403. [[NSNotificationCenter defaultCenter] removeObserver: self];
  404. }
  405. - (void)keyboardWillShow: (NSNotification*) aNotification {
  406. NSDictionary* info = [aNotification userInfo];
  407. CGRect keyboardRect = [[info objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
  408. CGRect keyboardRectConverted = [_choiceTableView convertRect: keyboardRect fromView: nil];
  409. CGSize keyboardSize = keyboardRectConverted.size;
  410. UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0);
  411. _choiceTableView.contentInset = contentInsets;
  412. _choiceTableView.scrollIndicatorInsets = contentInsets;
  413. }
  414. - (void)keyboardDidShow:(NSNotification *)aNotification {
  415. if (_indexPathForPicker) {
  416. BallotCreateTableCell *lastCell = [_choiceTableView cellForRowAtIndexPath:_indexPathForPicker];
  417. if (@available(iOS 14.0, *)) {
  418. if (!lastCell.choiceTextField.isFirstResponder) {
  419. [_choiceTableView scrollToRowAtIndexPath:_indexPathForPicker atScrollPosition:UITableViewScrollPositionBottom animated:true];
  420. return;
  421. }
  422. }
  423. [lastCell showDatePicker:nil];
  424. }
  425. }
  426. - (void)keyboardWillHide:(NSNotification*)aNotification
  427. {
  428. _choiceTableView.contentInset = UIEdgeInsetsZero;
  429. _choiceTableView.scrollIndicatorInsets = UIEdgeInsetsZero;
  430. }
  431. #pragma mark - Navigation
  432. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  433. if ([segue.identifier isEqualToString:@"ballotOptionsSegue"]) {
  434. [self updateEntityObjects];
  435. BallotCreateDetailViewController *controller = (BallotCreateDetailViewController*)segue.destinationViewController;
  436. controller.ballot = _ballot;
  437. }
  438. }
  439. @end