BallotVoteViewController.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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 "BallotVoteViewController.h"
  21. #import "Contact.h"
  22. #import "BallotVoteTableCell.h"
  23. #import "BallotChoice.h"
  24. #import "BallotResult.h"
  25. #import "MyIdentityStore.h"
  26. #import "EntityManager.h"
  27. #import "Ballot.h"
  28. #import "MessageSender.h"
  29. #import "BallotManager.h"
  30. #import "RectUtil.h"
  31. #import "BallotHeaderView.h"
  32. #import "BallotCreateViewController.h"
  33. #import "BallotResultViewController.h"
  34. #import "UIImage+ColoredImage.h"
  35. #import "PermissionChecker.h"
  36. #import "NibUtil.h"
  37. #define BALLOT_VOTE_TABLE_CELL_ID @"BallotVoteTableCellId"
  38. #define BALLOT_CLOSE_ACK_MESSAGE NSLocalizedStringFromTable(@"ballot_close_ack", @"Ballot", nil)
  39. @interface BallotVoteViewController () <UITableViewDelegate, UITableViewDataSource>
  40. @property NSArray *choices;
  41. @property EntityManager *entityManager;
  42. @property BallotManager *ballotManager;
  43. @property Ballot *ballot;
  44. @property BallotHeaderView *headerView;
  45. @end
  46. @implementation BallotVoteViewController
  47. + (instancetype) ballotVoteViewControllerForBallot:(Ballot *)ballot {
  48. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Ballot" bundle:nil];
  49. BallotVoteViewController *viewController = (BallotVoteViewController *) [storyboard instantiateViewControllerWithIdentifier:@"BallotVoteViewController"];
  50. EntityManager *entityManager = [[EntityManager alloc] init];
  51. viewController.entityManager = entityManager;
  52. viewController.ballotManager = [BallotManager ballotManagerWithEntityManager: entityManager];
  53. viewController.ballot = (Ballot *)[entityManager.entityFetcher getManagedObjectById:ballot.objectID];
  54. return viewController;
  55. }
  56. -(void)viewWillLayoutSubviews {
  57. if (_adminView.hidden && !_summaryView.hidden) {
  58. _summaryView.frame = [RectUtil setYPositionOf:_summaryView.frame y:CGRectGetMaxY(self.view.frame) - CGRectGetHeight(_summaryView.frame)];
  59. }
  60. CGFloat top = self.topLayoutGuide.length;
  61. _headerPlaceholderView.frame = [RectUtil setYPositionOf:_headerPlaceholderView.frame y:top];
  62. _choiceTableView.frame = [RectUtil setYPositionOf:_choiceTableView.frame y:CGRectGetMaxY(_headerPlaceholderView.frame)];
  63. CGFloat height;
  64. if (_summaryView.hidden) {
  65. height = self.view.bounds.size.height - CGRectGetMaxY(_headerPlaceholderView.frame);
  66. } else {
  67. height = CGRectGetMinY(_summaryView.frame) - CGRectGetMaxY(_headerPlaceholderView.frame);
  68. }
  69. _choiceTableView.frame = [RectUtil setHeightOf:_choiceTableView.frame height:height];
  70. }
  71. - (void)viewWillAppear:(BOOL)animated {
  72. [self updateContent];
  73. if (animated) {
  74. [_headerView bounceDetailView];
  75. }
  76. }
  77. - (void)viewDidLoad {
  78. [super viewDidLoad];
  79. _choiceTableView.delegate = self;
  80. _choiceTableView.dataSource = self;
  81. _cancelButton.target = self;
  82. _cancelButton.action = @selector(cancelPressed);
  83. _voteButton.target = self;
  84. _voteButton.action = @selector(votePressed);
  85. _adminView.hidden = ![_ballot canEdit];
  86. _summaryView.hidden = ![_ballot isIntermediate] && ![_ballot canEdit];
  87. if ([_ballot isIntermediate]) {
  88. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resultsTapped)];
  89. [_summaryView addGestureRecognizer:tapGesture];
  90. _summaryView.accessibilityTraits = UIAccessibilityTraitButton;
  91. _summaryView.isAccessibilityElement = YES;
  92. _detailsImage.hidden = NO;
  93. UIImage *tmpImage = [UIImage imageNamed:@"ArrowNext"];
  94. _detailsImage.image = [tmpImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  95. _detailsImage.tintColor = [UIColor whiteColor];
  96. } else {
  97. _detailsImage.hidden = YES;
  98. }
  99. [_ballotCloseButton addTarget:self action:@selector(ballotClosePressed) forControlEvents:UIControlEventTouchUpInside];
  100. [_ballotEditButton addTarget:self action:@selector(ballotEditPressed) forControlEvents:UIControlEventTouchUpInside];
  101. _headerView = (BallotHeaderView *)[NibUtil loadViewFromNibWithName:@"BallotHeaderView"];
  102. _headerView.ballot = _ballot;
  103. _headerView.frame = _headerPlaceholderView.bounds;
  104. [_headerPlaceholderView addSubview: _headerView];
  105. _voteButton.title = NSLocalizedStringFromTable(@"ballot_vote", @"Ballot", nil);
  106. [_ballotCloseButton setTitle:NSLocalizedStringFromTable(@"ballot_close", @"Ballot", nil) forState:UIControlStateNormal];
  107. [self setupColors];
  108. }
  109. - (void)setupColors {
  110. self.view.backgroundColor = [Colors background];
  111. _adminView.backgroundColor = [Colors backgroundDark];
  112. _summaryView.backgroundColor = [Colors backgroundInverted];
  113. _countVotesLabel.textColor = [Colors white];
  114. _choiceTableView.backgroundColor = [Colors background];
  115. [_ballotCloseButton setBackgroundColor:[Colors background]];
  116. [_ballotCloseButton setTitleColor:[Colors main] forState:UIControlStateNormal];
  117. _headerView.backgroundColor = [Colors background];
  118. }
  119. - (void)updateContent {
  120. _choices = [_ballot choicesSortedByOrder];
  121. [self setTitle:NSLocalizedStringFromTable(@"ballot", @"Ballot", nil)];
  122. if (_ballot.conversation == nil) {
  123. _voteButton.enabled = NO;
  124. }
  125. if (![[PermissionChecker permissionChecker] canSendIn:_ballot.conversation entityManager: nil]) {
  126. _voteButton.enabled = NO;
  127. }
  128. NSString *messageFormat = NSLocalizedStringFromTable(@"ballot_got_votes_count", @"Ballot", nil);
  129. NSInteger countParticipants = _ballot.participantCount;
  130. NSInteger countVotes = _ballot.numberOfReceivedVotes;
  131. NSString *message = [NSString stringWithFormat:messageFormat, countVotes, countParticipants];
  132. _countVotesLabel.text = message;
  133. _summaryView.accessibilityValue = message;
  134. }
  135. - (NSString *)choiceTextAt:(NSInteger)index {
  136. BallotChoice *choice = [_choices objectAtIndex: index];
  137. return choice.name;
  138. }
  139. - (NSString *)voteCountTextAt:(NSInteger)index {
  140. BallotChoice *choice = [_choices objectAtIndex: index];
  141. NSInteger count = [choice totalCountOfResultsTrue];
  142. return [NSString stringWithFormat:@"%li", (long)count];
  143. }
  144. - (void)setResult:(BOOL)value forChoiceAt:(NSInteger)index {
  145. if ([_ballot isMultipleChoice] == NO) {
  146. [self resetAllValues];
  147. }
  148. BallotChoice *choice = [_choices objectAtIndex:index];
  149. [_ballotManager updateChoice:choice withOwnResult: [NSNumber numberWithBool:value]];
  150. [self updateTable];
  151. }
  152. - (BOOL)resultForChoiceAt:(NSInteger)index {
  153. BallotChoice *choice = [_choices objectAtIndex:index];
  154. BallotResult *result = [choice getOwnResult];
  155. if (result) {
  156. return [result boolValue];
  157. }
  158. return NO;
  159. }
  160. - (void)resetAllValues {
  161. for (BallotChoice *choice in _choices) {
  162. [_ballotManager updateChoice:choice withOwnResult: [NSNumber numberWithBool: NO]];
  163. }
  164. }
  165. - (void)updateTable {
  166. for (NSInteger i=0; i<[_choices count]; i++) {
  167. NSIndexPath *index = [NSIndexPath indexPathForRow:i inSection:0];
  168. BallotVoteTableCell *cell = (BallotVoteTableCell *)[_choiceTableView cellForRowAtIndexPath: index];
  169. [self updateCheckmarkForCell:cell atIndexPath: index];
  170. [self updateAccessabilityLabelForCell:cell];
  171. }
  172. }
  173. - (void)updateCheckmarkForCell:(BallotVoteTableCell *)cell atIndexPath:(NSIndexPath *) indexPath {
  174. BOOL selected = [self resultForChoiceAt:indexPath.row];
  175. cell.checkmarkView.hidden = !selected;
  176. }
  177. - (void)checkBallotClosed {
  178. if ([_ballot isClosed]) {
  179. _voteButton.enabled = NO;
  180. _choiceTableView.userInteractionEnabled = NO;
  181. NSString *title = NSLocalizedStringFromTable(@"ballot_vote_ballot_closed_title", @"Ballot", nil);
  182. NSString *message = NSLocalizedStringFromTable(@"ballot_vote_ballot_closed_message", @"Ballot", nil);
  183. [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:nil];
  184. }
  185. }
  186. - (void)setDefaultResultsIfMissing {
  187. for (BallotChoice *choice in [_ballot choices]) {
  188. if ([choice getOwnResult] == nil) {
  189. // add default value
  190. [_ballotManager updateChoice:choice withOwnResult:[NSNumber numberWithBool:NO]];
  191. }
  192. }
  193. }
  194. - (void)showBallotCloseAcknowledgeAlert {
  195. NSString *title = NSLocalizedStringFromTable(@"ballot_close", @"Ballot", nil);
  196. NSString *message = BALLOT_CLOSE_ACK_MESSAGE;
  197. UIAlertController *ackAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
  198. [ackAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
  199. [self closeBallot];
  200. }]];
  201. [ackAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
  202. }]];
  203. [self presentViewController:ackAlert animated:YES completion:nil];
  204. }
  205. - (void)closeBallot {
  206. [_ballot setClosed];
  207. [_entityManager performSyncBlockAndSafe:nil];
  208. [MessageSender sendCreateMessageForBallot:_ballot];
  209. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  210. }
  211. #pragma mark - button actions
  212. - (void) votePressed {
  213. [self setDefaultResultsIfMissing];
  214. _ballot.modifyDate = [NSDate date];
  215. [_entityManager performSyncBlockAndSafe:nil];
  216. [MessageSender sendBallotVoteMessage:_ballot];
  217. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  218. }
  219. - (void) ballotClosePressed {
  220. [self showBallotCloseAcknowledgeAlert];
  221. }
  222. - (void)resultsTapped {
  223. BallotResultViewController *viewController = [BallotResultViewController ballotResultViewControllerForBallot: _ballot];
  224. [self.navigationController pushViewController:viewController animated:YES];
  225. }
  226. - (void) ballotEditPressed {
  227. BallotCreateViewController *viewController = [BallotCreateViewController ballotCreateViewControllerForBallot: _ballot];
  228. [self.navigationController pushViewController:viewController animated:YES];
  229. }
  230. - (void) cancelPressed {
  231. [_entityManager rollback];
  232. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  233. }
  234. #pragma mark - table view data source
  235. - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  236. [Colors updateTableViewCell:cell];
  237. }
  238. - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
  239. return 0.001;
  240. }
  241. - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  242. {
  243. return [_choices count];
  244. }
  245. -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  246. NSString *title = [self choiceTextAt:indexPath.row];
  247. CGRect rect = CGRectMake(0.0, 0.0, CGRectGetWidth(_choiceTableView.frame), CGFLOAT_MAX);
  248. CGFloat height = [BallotVoteTableCell calculateHeightFor:title inFrame:rect];
  249. return height;
  250. }
  251. - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  252. {
  253. NSString *title = [self choiceTextAt:indexPath.row];
  254. BallotVoteTableCell *cell = [tableView dequeueReusableCellWithIdentifier: BALLOT_VOTE_TABLE_CELL_ID];
  255. if (cell == nil) {
  256. cell = [[BallotVoteTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: BALLOT_VOTE_TABLE_CELL_ID];
  257. }
  258. [cell.choiceLabel setText: title];
  259. [cell.choiceLabel sizeToFit];
  260. cell.frame = [RectUtil setHeightOf:cell.frame height:cell.choiceLabel.bounds.size.height];
  261. [self updateCheckmarkForCell:cell atIndexPath: indexPath];
  262. if ([_ballot isIntermediate]) {
  263. NSString *voteCount = [self voteCountTextAt:indexPath.row];
  264. [cell.voteCountLabel setText: voteCount];
  265. } else {
  266. cell.voteCountLabel.hidden = YES;
  267. }
  268. [self updateAccessabilityLabelForCell:cell];
  269. return cell;
  270. }
  271. - (void)updateAccessabilityLabelForCell:(BallotVoteTableCell *)cell {
  272. NSString *votesCountFormat = NSLocalizedStringFromTable(@"ballot_votes_count", @"Ballot", nil);
  273. NSString *selected = cell.checkmarkView.hidden ? NSLocalizedStringFromTable(@"ballot_vote_not_selected", @"Ballot", nil) : NSLocalizedStringFromTable(@"ballot_vote_selected", @"Ballot", nil);
  274. if (_ballot.isIntermediate) {
  275. NSString *votesCount = [NSString stringWithFormat:votesCountFormat, cell.voteCountLabel.text];
  276. cell.accessibilityLabel = [NSString stringWithFormat:@"%@, %@, %@", cell.choiceLabel.text, votesCount, selected];
  277. } else {
  278. cell.accessibilityLabel = [NSString stringWithFormat:@"%@, %@", cell.choiceLabel.text, selected];
  279. }
  280. }
  281. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  282. BOOL toggleValue = [self resultForChoiceAt:indexPath.row];
  283. [self setResult:!toggleValue forChoiceAt:indexPath.row];
  284. [tableView deselectRowAtIndexPath:indexPath animated: YES];
  285. }
  286. @end