TUIReplyMessageCell.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. //
  2. // TUIReplyMessageCell.m
  3. // TUIChat
  4. //
  5. // Created by harvy on 2021/11/11.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "TUIReplyMessageCell.h"
  9. #import <TIMCommon/NSString+TUIEmoji.h>
  10. #import <TUICore/TUICore.h>
  11. #import <TUICore/TUIDarkModel.h>
  12. #import <TUICore/TUIThemeManager.h>
  13. #import <TUICore/UIView+TUILayout.h>
  14. #import "TUIFileMessageCellData.h"
  15. #import "TUIImageMessageCellData.h"
  16. #import "TUILinkCellData.h"
  17. #import "TUIMergeMessageCellData.h"
  18. #import "TUIReplyMessageCellData.h"
  19. #import "TUITextMessageCellData.h"
  20. #import "TUIVideoMessageCellData.h"
  21. #import "TUIVoiceMessageCellData.h"
  22. #import "TUIFileReplyQuoteView.h"
  23. #import "TUIImageReplyQuoteView.h"
  24. #import "TUIMergeReplyQuoteView.h"
  25. #import "TUIReplyQuoteView.h"
  26. #import "TUITextReplyQuoteView.h"
  27. #import "TUIVideoReplyQuoteView.h"
  28. #import "TUIVoiceReplyQuoteView.h"
  29. @interface TUIReplyMessageCell () <UITextViewDelegate,TUITextViewDelegate>
  30. @property(nonatomic, strong) TUIReplyQuoteView *currentOriginView;
  31. @property(nonatomic, strong) NSMutableDictionary<NSString *, TUIReplyQuoteView *> *customOriginViewsCache;
  32. @end
  33. @implementation TUIReplyMessageCell
  34. - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
  35. if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
  36. [self setupViews];
  37. }
  38. return self;
  39. }
  40. - (void)setupViews {
  41. [self.quoteView addSubview:self.senderLabel];
  42. [self.quoteView addSubview:self.quoteBorderLine];
  43. [self.bubbleView addSubview:self.quoteView];
  44. [self.bubbleView addSubview:self.textView];
  45. self.bottomContainer = [[UIView alloc] init];
  46. [self.contentView addSubview:self.bottomContainer];
  47. }
  48. // Override
  49. - (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
  50. NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.replyData};
  51. [TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
  52. }
  53. - (void)fillWithData:(TUIReplyMessageCellData *)data {
  54. [super fillWithData:data];
  55. self.replyData = data;
  56. self.senderLabel.text = [NSString stringWithFormat:@"%@:", data.sender];
  57. self.textView.attributedText = [data.content getFormatEmojiStringWithFont:self.textView.font emojiLocations:self.replyData.emojiLocations];
  58. self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
  59. if (data.direction == MsgDirectionIncoming) {
  60. self.textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_recv_text_color", @"#000000");
  61. self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
  62. self.quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_bg_color", @"#4444440c");
  63. } else {
  64. self.textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_text_color", @"#000000");
  65. self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
  66. self.quoteView.backgroundColor = [UIColor colorWithRed:68 / 255.0 green:68 / 255.0 blue:68 / 255.0 alpha:0.05];
  67. }
  68. @weakify(self);
  69. [[RACObserve(data, originMessage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(V2TIMMessage *originMessage) {
  70. @strongify(self);
  71. // tell constraints they need updating
  72. [self setNeedsUpdateConstraints];
  73. // update constraints now so we can animate the change
  74. [self updateConstraintsIfNeeded];
  75. [self layoutIfNeeded];
  76. }];
  77. // tell constraints they need updating
  78. [self setNeedsUpdateConstraints];
  79. // update constraints now so we can animate the change
  80. [self updateConstraintsIfNeeded];
  81. [self layoutIfNeeded];
  82. }
  83. + (BOOL)requiresConstraintBasedLayout {
  84. return YES;
  85. }
  86. // this is Apple's recommended place for adding/updating constraints
  87. - (void)updateConstraints {
  88. [super updateConstraints];
  89. [self updateUI:self.replyData];
  90. [self layoutBottomContainer];
  91. }
  92. - (void)updateUI:(TUIReplyMessageCellData *)replyData {
  93. self.currentOriginView = [self getCustomOriginView:replyData.originCellData];
  94. [self hiddenAllCustomOriginViews:YES];
  95. self.currentOriginView.hidden = NO;
  96. replyData.quoteData.supportForReply = YES;
  97. replyData.quoteData.showRevokedOriginMessage = replyData.showRevokedOriginMessage;
  98. [self.currentOriginView fillWithData:replyData.quoteData];
  99. [self.quoteView mas_remakeConstraints:^(MASConstraintMaker *make) {
  100. make.leading.mas_equalTo(self.bubbleView).mas_offset(16);
  101. make.top.mas_equalTo(12);
  102. make.trailing.mas_lessThanOrEqualTo(self.bubbleView).mas_offset(-16);
  103. make.width.mas_greaterThanOrEqualTo(self.senderLabel);
  104. make.height.mas_equalTo(self.replyData.quoteSize.height);
  105. }];
  106. [self.quoteBorderLine mas_remakeConstraints:^(MASConstraintMaker *make) {
  107. make.leading.mas_equalTo(self.quoteView);
  108. make.top.mas_equalTo(self.quoteView);
  109. make.width.mas_equalTo(3);
  110. make.bottom.mas_equalTo(self.quoteView);
  111. }];
  112. [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
  113. make.leading.mas_equalTo(self.quoteView).mas_offset(4);
  114. make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(12);
  115. make.trailing.mas_lessThanOrEqualTo(self.quoteView).mas_offset(-4);;
  116. make.bottom.mas_equalTo(self.bubbleView).mas_offset(-4);
  117. }];
  118. BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
  119. if (hasRiskContent ) {
  120. [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
  121. make.leading.mas_equalTo(self.quoteView).mas_offset(4);
  122. make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(12);
  123. make.trailing.mas_lessThanOrEqualTo(self.quoteView).mas_offset(-4);
  124. make.size.mas_equalTo(self.replyData.replyContentSize);
  125. }];
  126. [self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
  127. make.top.mas_equalTo(self.textView.mas_bottom);
  128. make.width.mas_equalTo(self.bubbleView);
  129. make.bottom.mas_equalTo(self.container);
  130. }];
  131. }
  132. [self.senderLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
  133. make.leading.mas_equalTo(self.textView);
  134. make.top.mas_equalTo(3);
  135. make.size.mas_equalTo(self.replyData.senderSize);
  136. }];
  137. BOOL hideSenderLabel = (replyData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
  138. !replyData.showRevokedOriginMessage;
  139. if (hideSenderLabel) {
  140. self.senderLabel.hidden = YES;
  141. } else {
  142. self.senderLabel.hidden = NO;
  143. }
  144. [self.currentOriginView mas_remakeConstraints:^(MASConstraintMaker *make) {
  145. make.leading.mas_equalTo(self.senderLabel);
  146. if (hideSenderLabel) {
  147. make.centerY.mas_equalTo(self.quoteView);
  148. } else {
  149. make.top.mas_equalTo(self.senderLabel.mas_bottom).mas_offset(4);
  150. }
  151. // make.width.mas_greaterThanOrEqualTo(self.replyData.quotePlaceholderSize);
  152. make.trailing.mas_lessThanOrEqualTo(self.quoteView.mas_trailing);
  153. make.height.mas_equalTo(self.replyData.quotePlaceholderSize);
  154. }];
  155. }
  156. - (TUIReplyQuoteView *)getCustomOriginView:(TUIMessageCellData *)originCellData {
  157. NSString *reuseId = originCellData ? NSStringFromClass(originCellData.class) : NSStringFromClass(TUITextMessageCellData.class);
  158. TUIReplyQuoteView *view = nil;
  159. BOOL reuse = NO;
  160. BOOL hasRiskContent = originCellData.innerMessage.hasRiskContent;
  161. if (hasRiskContent) {
  162. reuseId = @"hasRiskContent";
  163. }
  164. if ([self.customOriginViewsCache.allKeys containsObject:reuseId]) {
  165. view = [self.customOriginViewsCache objectForKey:reuseId];
  166. reuse = YES;
  167. }
  168. if (hasRiskContent && view == nil){
  169. TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
  170. view = quoteView;
  171. }
  172. if (view == nil) {
  173. Class class = [originCellData getReplyQuoteViewClass];
  174. if (class) {
  175. view = [[class alloc] init];
  176. }
  177. }
  178. if (view == nil) {
  179. TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
  180. view = quoteView;
  181. }
  182. if ([view isKindOfClass:[TUITextReplyQuoteView class]]) {
  183. TUITextReplyQuoteView *quoteView = (TUITextReplyQuoteView *)view;
  184. if (self.replyData.direction == MsgDirectionIncoming) {
  185. quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
  186. } else {
  187. quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
  188. }
  189. } else if ([view isKindOfClass:[TUIMergeReplyQuoteView class]]) {
  190. TUIMergeReplyQuoteView *quoteView = (TUIMergeReplyQuoteView *)view;
  191. if (self.replyData.direction == MsgDirectionIncoming) {
  192. quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
  193. } else {
  194. quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
  195. }
  196. }
  197. if (!reuse) {
  198. [self.customOriginViewsCache setObject:view forKey:reuseId];
  199. [self.quoteView addSubview:view];
  200. }
  201. view.hidden = YES;
  202. return view;
  203. }
  204. - (void)hiddenAllCustomOriginViews:(BOOL)hidden {
  205. [self.customOriginViewsCache enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, TUIReplyQuoteView *_Nonnull obj, BOOL *_Nonnull stop) {
  206. obj.hidden = hidden;
  207. [obj reset];
  208. }];
  209. }
  210. - (void)layoutSubviews {
  211. [super layoutSubviews];
  212. }
  213. - (void)layoutBottomContainer {
  214. if (CGSizeEqualToSize(self.replyData.bottomContainerSize, CGSizeZero)) {
  215. return;
  216. }
  217. CGSize size = self.replyData.bottomContainerSize;
  218. CGFloat topMargin = self.bubbleView.mm_maxY + self.nameLabel.mm_h + 8;
  219. [self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
  220. make.top.mas_equalTo(self.bubbleView.mas_bottom).mas_offset(8);
  221. make.size.mas_equalTo(size);
  222. if (self.replyData.direction == MsgDirectionOutgoing) {
  223. make.trailing.mas_equalTo(self.container);
  224. }
  225. else {
  226. make.leading.mas_equalTo(self.container);
  227. }
  228. }];
  229. if (!self.messageModifyRepliesButton.hidden) {
  230. CGRect oldRect = self.messageModifyRepliesButton.frame;
  231. CGRect newRect = CGRectMake(oldRect.origin.x, CGRectGetMaxY(self.bottomContainer.frame), oldRect.size.width, oldRect.size.height);
  232. self.messageModifyRepliesButton.frame = newRect;
  233. }
  234. }
  235. - (UILabel *)senderLabel {
  236. if (_senderLabel == nil) {
  237. _senderLabel = [[UILabel alloc] init];
  238. _senderLabel.text = @"harvy:";
  239. _senderLabel.font = [UIFont boldSystemFontOfSize:12.0];
  240. _senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_sender_text_color", @"#888888");
  241. _senderLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
  242. }
  243. return _senderLabel;
  244. }
  245. - (UIView *)quoteView {
  246. if (_quoteView == nil) {
  247. _quoteView = [[UIView alloc] init];
  248. _quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_bg_color", @"#4444440c");
  249. }
  250. return _quoteView;
  251. }
  252. - (UIView *)quoteBorderLine {
  253. if (_quoteBorderLine == nil) {
  254. _quoteBorderLine = [[UIView alloc] init];
  255. _quoteBorderLine.backgroundColor = [UIColor colorWithRed:68 / 255.0 green:68 / 255.0 blue:68 / 255.0 alpha:0.1];
  256. }
  257. return _quoteBorderLine;
  258. }
  259. - (TUITextView *)textView {
  260. if (_textView == nil) {
  261. _textView = [[TUITextView alloc] init];
  262. _textView.font = [UIFont systemFontOfSize:16.0];
  263. _textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_text_color", @"#000000");
  264. _textView.backgroundColor = [UIColor clearColor];
  265. _textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
  266. _textView.textContainer.lineFragmentPadding = 0;
  267. _textView.scrollEnabled = NO;
  268. _textView.editable = NO;
  269. _textView.delegate = self;
  270. _textView.tuiTextViewDelegate = self;
  271. _textView.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
  272. }
  273. return _textView;
  274. }
  275. - (void)onLongPressTextViewMessage:(UITextView *)textView {
  276. if (self.delegate && [self.delegate respondsToSelector:@selector(onLongPressMessage:)]) {
  277. [self.delegate onLongPressMessage:self];
  278. }
  279. }
  280. - (NSMutableDictionary *)customOriginViewsCache {
  281. if (_customOriginViewsCache == nil) {
  282. _customOriginViewsCache = [[NSMutableDictionary alloc] init];
  283. }
  284. return _customOriginViewsCache;
  285. }
  286. - (void)textViewDidChangeSelection:(UITextView *)textView {
  287. NSAttributedString *selectedString = [textView.attributedText attributedSubstringFromRange:textView.selectedRange];
  288. if (self.selectAllContentContent && selectedString) {
  289. if (selectedString.length == textView.attributedText.length) {
  290. self.selectAllContentContent(YES);
  291. } else {
  292. self.selectAllContentContent(NO);
  293. }
  294. }
  295. if (selectedString.length > 0) {
  296. NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
  297. [attributedString appendAttributedString:selectedString];
  298. NSUInteger offsetLocation = 0;
  299. for (NSDictionary *emojiLocation in self.replyData.emojiLocations) {
  300. NSValue *key = emojiLocation.allKeys.firstObject;
  301. NSAttributedString *originStr = emojiLocation[key];
  302. NSRange currentRange = [key rangeValue];
  303. currentRange.location += offsetLocation;
  304. if (currentRange.location >= textView.selectedRange.location) {
  305. currentRange.location -= textView.selectedRange.location;
  306. if (currentRange.location + currentRange.length <= attributedString.length) {
  307. [attributedString replaceCharactersInRange:currentRange withAttributedString:originStr];
  308. offsetLocation += originStr.length - currentRange.length;
  309. }
  310. }
  311. }
  312. self.selectContent = attributedString.string;
  313. } else {
  314. self.selectContent = nil;
  315. }
  316. }
  317. #pragma mark - TUIMessageCellProtocol
  318. + (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
  319. NSAssert([data isKindOfClass:TUIReplyMessageCellData.class], @"data must be kind of TUIReplyMessageCellData");
  320. TUIReplyMessageCellData *replyCellData = (TUIReplyMessageCellData *)data;
  321. CGFloat height = [super getHeight:replyCellData withWidth:width];
  322. if (replyCellData.bottomContainerSize.height > 0) {
  323. height += replyCellData.bottomContainerSize.height + kScale375(6);
  324. }
  325. return height;
  326. }
  327. + (CGSize)getContentSize:(TUIMessageCellData *)data {
  328. NSAssert([data isKindOfClass:TUIReplyMessageCellData.class], @"data must be kind of TUIReplyMessageCellData");
  329. TUIReplyMessageCellData *replyCellData = (TUIReplyMessageCellData *)data;
  330. CGFloat height = 0;
  331. CGFloat quoteHeight = 0;
  332. CGFloat quoteWidth = 0;
  333. CGFloat quoteMinWidth = 100;
  334. CGFloat quoteMaxWidth = TReplyQuoteView_Max_Width;
  335. CGFloat quotePlaceHolderMarginWidth = 12;
  336. UIFont *font = [UIFont systemFontOfSize:16.0];
  337. // Calculate the size of label which displays the sender's displyname
  338. CGSize senderSize = [@"0" sizeWithAttributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}];
  339. CGRect senderRect = [replyCellData.sender boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
  340. options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
  341. attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
  342. context:nil];
  343. // Calculate the size of revoke string
  344. CGRect messageRevokeRect = CGRectZero;
  345. BOOL showRevokeStr = (replyCellData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
  346. !replyCellData.showRevokedOriginMessage;
  347. if (showRevokeStr) {
  348. NSString *msgRevokeStr = TIMCommonLocalizableString(TUIKitRepliesOriginMessageRevoke);
  349. messageRevokeRect = [msgRevokeStr boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
  350. options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
  351. attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
  352. context:nil];
  353. }
  354. // Calculate the size of customize quote placeholder view
  355. CGSize placeholderSize = [replyCellData quotePlaceholderSizeWithType:replyCellData.originMsgType data:replyCellData.quoteData];
  356. // Calculate the size of label which displays the content of replying the original message
  357. NSAttributedString *attributeString = [replyCellData.content getFormatEmojiStringWithFont:font emojiLocations:nil];
  358. CGRect replyContentRect = [attributeString boundingRectWithSize:CGSizeMake(quoteMaxWidth, CGFLOAT_MAX)
  359. options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
  360. context:nil];
  361. // Calculate the size of quote view base the content
  362. quoteWidth = senderRect.size.width;
  363. if (quoteWidth < placeholderSize.width) {
  364. quoteWidth = placeholderSize.width;
  365. }
  366. if (quoteWidth < replyContentRect.size.width) {
  367. quoteWidth = replyContentRect.size.width;
  368. }
  369. quoteWidth += quotePlaceHolderMarginWidth;
  370. BOOL lineSpacingChecked = NO ;
  371. if (quoteWidth > quoteMaxWidth) {
  372. quoteWidth = quoteMaxWidth;
  373. //line spacing
  374. lineSpacingChecked = YES;
  375. }
  376. if (quoteWidth < quoteMinWidth) {
  377. quoteWidth = quoteMinWidth;
  378. }
  379. if (showRevokeStr) {
  380. quoteWidth = MAX(quoteWidth, messageRevokeRect.size.width);
  381. }
  382. quoteHeight = 3 + senderRect.size.height + 4 + placeholderSize.height + 6;
  383. replyCellData.senderSize = CGSizeMake(quoteWidth, senderRect.size.height);
  384. replyCellData.quotePlaceholderSize = placeholderSize;
  385. replyCellData.replyContentSize = CGSizeMake(replyContentRect.size.width, replyContentRect.size.height);
  386. replyCellData.quoteSize = CGSizeMake(quoteWidth, quoteHeight);
  387. // cell
  388. // Calculate the height of cell
  389. height = 12 + quoteHeight + 12 + replyCellData.replyContentSize.height + 12;
  390. CGRect replyContentRect2 = [attributeString boundingRectWithSize:CGSizeMake(MAXFLOAT, [font lineHeight])
  391. options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
  392. context:nil];
  393. // Determine whether the width of the last line exceeds the position of the message status. If it exceeds, the message status will be wrapped.
  394. if (lineSpacingChecked) {
  395. if ((int)replyContentRect2.size.width % (int)quoteWidth == 0 ||
  396. (int)replyContentRect2.size.width % (int)quoteWidth + font.lineHeight > quoteWidth) {
  397. height += font.lineHeight;
  398. }
  399. }
  400. CGSize size = CGSizeMake(quoteWidth + TReplyQuoteView_Margin_Width, height);
  401. BOOL hasRiskContent = replyCellData.innerMessage.hasRiskContent;
  402. if (hasRiskContent) {
  403. size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
  404. size.height += kTUISecurityStrikeViewTopLineMargin;
  405. size.height += kTUISecurityStrikeViewTopLineToBottom;
  406. }
  407. return size;
  408. }
  409. @end