ChatBallotMessageCell.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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 "ChatBallotMessageCell.h"
  21. #import "BallotMessage.h"
  22. #import "UserSettings.h"
  23. #import "Ballot.h"
  24. #import "RectUtil.h"
  25. #import "BallotDispatcher.h"
  26. #import "ModalNavigationController.h"
  27. #import "UIImage+ColoredImage.h"
  28. #define ICON_IMAGE @"ActionBallot"
  29. @implementation ChatBallotMessageCell {
  30. UILabel *headerLabel;
  31. UILabel *nameLabel;
  32. UIImageView *icon;
  33. }
  34. + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
  35. CGSize sizeConstrains = CGSizeMake([ChatMessageCell maxContentWidthForTableWidth:tableWidth] - 25, CGFLOAT_MAX);
  36. NSString *header = [ChatBallotMessageCell headerForMessage:message];
  37. UIFont *headerFont = [ChatBallotMessageCell headerFont];
  38. CGSize headerSize = [header boundingRectWithSize:sizeConstrains options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : headerFont} context:nil].size;
  39. headerSize.height = ceilf(headerSize.height);
  40. NSString *text = [ChatBallotMessageCell displayTextForMessage:message];
  41. UIFont *textFont = [ChatMessageCell textFont];
  42. CGSize size = [text boundingRectWithSize:sizeConstrains options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : textFont} context:nil].size;
  43. size.height = ceilf(size.height);
  44. return MAX(size.height + headerSize.height, 34.0f);
  45. }
  46. - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent
  47. {
  48. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent];
  49. if (self) {
  50. icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:ICON_IMAGE]];
  51. icon.frame = CGRectMake(0.0, 0.0, 20.0, 20.0);
  52. [self.contentView addSubview:icon];
  53. headerLabel = [[UILabel alloc] init];
  54. headerLabel.clearsContextBeforeDrawing = NO;
  55. headerLabel.backgroundColor = [UIColor clearColor];
  56. headerLabel.numberOfLines = 1;
  57. headerLabel.lineBreakMode = NSLineBreakByWordWrapping;
  58. headerLabel.font = [ChatBallotMessageCell headerFont];
  59. [self.contentView addSubview:headerLabel];
  60. nameLabel = [[UILabel alloc] init];
  61. nameLabel.clearsContextBeforeDrawing = NO;
  62. nameLabel.backgroundColor = [UIColor clearColor];
  63. nameLabel.numberOfLines = 0;
  64. nameLabel.lineBreakMode = NSLineBreakByWordWrapping;
  65. nameLabel.font = [ChatMessageCell textFont];
  66. [self.contentView addSubview:nameLabel];
  67. UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(messageTapped:)];
  68. gestureRecognizer.delegate = self;
  69. self.msgBackground.userInteractionEnabled = YES;
  70. [self.msgBackground addGestureRecognizer:gestureRecognizer];
  71. self.accessibilityIdentifier = @"ballot_matrix_cell";
  72. }
  73. return self;
  74. }
  75. - (void)setupColors {
  76. [super setupColors];
  77. icon.image = [UIImage imageNamed:ICON_IMAGE inColor:[Colors fontNormal]];
  78. icon.accessibilityLabel = @"ballot_matrix_image";
  79. headerLabel.textColor = [Colors fontNormal];
  80. nameLabel.textColor = [Colors fontNormal];
  81. }
  82. - (void)dealloc {
  83. @try {
  84. [self.message removeObserver:self forKeyPath:@"ballot.modifyDate"];
  85. }
  86. @catch(NSException *e) {}
  87. }
  88. - (void)layoutSubviews {
  89. CGFloat messageTextWidth;
  90. if (@available(iOS 11.0, *)) {
  91. messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.safeAreaLayoutGuide.layoutFrame.size.width];
  92. } else {
  93. messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.frame.size.width];
  94. }
  95. CGSize textSize = [nameLabel.text boundingRectWithSize:CGSizeMake(messageTextWidth - 25, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [ChatMessageCell textFont]} context:nil].size;
  96. CGSize headerSize = [headerLabel.text boundingRectWithSize:CGSizeMake(messageTextWidth - 25, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [ChatMessageCell textFont]} context:nil].size;
  97. CGFloat width = ceilf(MAX(textSize.width, headerSize.width));
  98. CGFloat height = ceilf(textSize.height + headerSize.height);
  99. CGSize size = CGSizeMake(width + 25, MAX(34.0f, height));
  100. [self setBubbleContentSize:size];
  101. [super layoutSubviews];
  102. CGFloat headerY = 7.0;
  103. CGFloat textY = 7.0 + floorf(headerSize.height);
  104. CGFloat xOffsetText;
  105. CGFloat xOffsetIcon;
  106. if (self.message.isOwn.boolValue) {
  107. xOffsetText = self.contentView.frame.size.width - width - 20;
  108. xOffsetIcon = self.contentView.frame.size.width - width - 48;
  109. } else {
  110. xOffsetText = 46 + self.contentLeftOffset;
  111. xOffsetIcon = 18 + self.contentLeftOffset;
  112. }
  113. nameLabel.frame = CGRectMake(xOffsetText, textY, floorf(textSize.width+1), floorf(textSize.height+1));
  114. headerLabel.frame = CGRectMake(xOffsetText, headerY, floorf(headerSize.width+1), floorf(headerSize.height+1));
  115. icon.frame = [RectUtil setXPositionOf:icon.frame x:xOffsetIcon];
  116. icon.frame = [RectUtil rect:icon.frame centerVerticalIn:self.contentView.frame];
  117. }
  118. - (NSString *)accessibilityLabelForContent {
  119. return [NSString stringWithFormat:@"%@, %@", [ChatBallotMessageCell headerForMessage:self.message], [ChatBallotMessageCell displayTextForMessage:self.message]];
  120. }
  121. - (void)setMessage:(BaseMessage *)newMessage {
  122. @try {
  123. [self.message removeObserver:self forKeyPath:@"ballot.modifyDate"];
  124. }
  125. @catch(NSException *e) {}
  126. [super setMessage:newMessage];
  127. if (!self.chatVc.isOpenWithForceTouch) {
  128. [self.message addObserver:self forKeyPath:@"ballot.modifyDate" options:0 context:nil];
  129. }
  130. [self updateView];
  131. }
  132. - (void)updateView {
  133. if (self.message.isOwn.boolValue) {
  134. nameLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  135. icon.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  136. } else {
  137. nameLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  138. icon.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  139. }
  140. [self setNeedsLayout];
  141. nameLabel.text = [ChatBallotMessageCell displayTextForMessage:self.message];
  142. headerLabel.text = [ChatBallotMessageCell headerForMessage:self.message];
  143. }
  144. - (void)messageTapped:(id)sender {
  145. [self.chatVc ballotMessageTapped:(BallotMessage*)self.message];
  146. }
  147. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  148. if (action == @selector(copyMessage:) || action == @selector(shareMessage:)) {
  149. return NO;
  150. } else if (action == @selector(forwardMessage:)) {
  151. return NO;
  152. } else if (action == @selector(speakMessage:)) {
  153. return YES;
  154. }
  155. else {
  156. return [super canPerformAction:action withSender:sender];
  157. }
  158. }
  159. - (void)copyMessage:(UIMenuController *)menuController {
  160. }
  161. - (void)shareMessage:(UIMenuController *)menuController {
  162. }
  163. - (void)forwardMessage:(UIMenuController *)menuController {
  164. }
  165. - (void)speakMessage:(UIMenuController *)menuController {
  166. NSString *speakText = [NSString stringWithFormat:@"%@, %@", [ChatBallotMessageCell headerForMessage:self.message], [ChatBallotMessageCell displayTextForMessage:self.message]];;
  167. AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:speakText];
  168. AVSpeechSynthesizer *syn = [[AVSpeechSynthesizer alloc] init];
  169. [syn speakUtterance:utterance];
  170. }
  171. - (void)deleteMessage:(UIMenuController*)menuController {
  172. if (self.message.isOwn.boolValue && !self.message.sent.boolValue && !self.message.sendFailed.boolValue)
  173. return;
  174. else
  175. [super deleteMessage:menuController];
  176. }
  177. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  178. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  179. dispatch_async(dispatch_get_main_queue(), ^{
  180. if (object == self.message) {
  181. [UIView animateWithDuration:0.5 animations:^{
  182. [self updateView];
  183. }];
  184. }
  185. });
  186. }
  187. - (BOOL)highlightOccurencesOf:(NSString *)pattern {
  188. NSAttributedString *attributedString = [ChatMessageCell highlightedOccurencesOf:pattern inString:nameLabel.text];
  189. if (attributedString) {
  190. nameLabel.attributedText = attributedString;
  191. return YES;
  192. }
  193. return NO;
  194. }
  195. + (NSString*)displayTextForMessage:(BaseMessage*)message {
  196. return ((BallotMessage*)message).ballot.title;
  197. }
  198. + (NSString*)headerForMessage:(BaseMessage*)message {
  199. BallotMessage *ballotMessage = (BallotMessage*)message;
  200. NSString *key;
  201. if (ballotMessage.isClosed) {
  202. key = @"ballot_closed_cell_title";
  203. } else {
  204. key = @"ballot_new_cell_title";
  205. }
  206. NSString *title = NSLocalizedStringFromTable(key, @"Ballot", nil);
  207. Ballot *ballot = ballotMessage.ballot;
  208. if (ballot.isClosed) {
  209. return title;
  210. } else {
  211. if (ballot.isIntermediate || ballot.isOwn) {
  212. NSInteger countParticipants = ballot.participantCount;
  213. NSInteger countVotes = ballot.numberOfReceivedVotes;
  214. return [NSString stringWithFormat:@"%@ %li/%li", title, (long)countVotes, (long)countParticipants];
  215. } else {
  216. return title;
  217. }
  218. }
  219. }
  220. + (UIFont *)headerFont {
  221. CGFloat fontSize = roundf([UserSettings sharedUserSettings].chatFontSize * 0.9);
  222. return [UIFont italicSystemFontOfSize: fontSize];
  223. }
  224. - (UIViewController *)previewViewController {
  225. BallotMessage *ballotMessage = (BallotMessage*)self.message;
  226. UIViewController *viewController = [BallotDispatcher viewControllerForBallot:ballotMessage.ballot];
  227. ModalNavigationController *modalNav = [[ModalNavigationController alloc] initWithRootViewController:viewController];
  228. modalNav.showDoneButton = YES;
  229. return modalNav;
  230. }
  231. - (BOOL)performPlayActionForAccessibility {
  232. [self messageTapped:self];
  233. return YES;
  234. }
  235. @end