TUITextMessageCell.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. //
  2. // TUITextMessageCell.m
  3. // UIKit
  4. //
  5. // Created by annidyfeng on 2019/5/30.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "TUITextMessageCell.h"
  9. #import <TIMCommon/TIMCommonModel.h>
  10. #import <TIMCommon/TIMDefine.h>
  11. #import <TUICore/TUICore.h>
  12. #import <TUICore/TUIGlobalization.h>
  13. #import "TUIFaceView.h"
  14. #import <TUICore/UIColor+TUIHexColor.h>
  15. #ifndef CGFLOAT_CEIL
  16. #ifdef CGFLOAT_IS_DOUBLE
  17. #define CGFLOAT_CEIL(value) ceil(value)
  18. #else
  19. #define CGFLOAT_CEIL(value) ceilf(value)
  20. #endif
  21. #endif
  22. @interface TUITextMessageCell ()<TUITextViewDelegate>
  23. @end
  24. @implementation TUITextMessageCell
  25. - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
  26. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  27. if (self) {
  28. self.textView = [[TUITextView alloc] init];
  29. self.textView.backgroundColor = [UIColor clearColor];
  30. self.textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
  31. self.textView.textContainer.lineFragmentPadding = 0;
  32. self.textView.scrollEnabled = NO;
  33. self.textView.editable = NO;
  34. self.textView.delegate = self;
  35. self.textView.tuiTextViewDelegate = self;
  36. self.bubbleView.userInteractionEnabled = YES;
  37. [self.bubbleView addSubview:self.textView];
  38. self.bottomContainer = [[UIView alloc] init];
  39. [self.contentView addSubview:self.bottomContainer];
  40. self.voiceReadPoint = [[UIImageView alloc] init];
  41. self.voiceReadPoint.backgroundColor = [UIColor redColor];
  42. self.voiceReadPoint.frame = CGRectMake(0, 0, 5, 5);
  43. self.voiceReadPoint.hidden = YES;
  44. [self.voiceReadPoint.layer setCornerRadius:self.voiceReadPoint.frame.size.width / 2];
  45. [self.voiceReadPoint.layer setMasksToBounds:YES];
  46. [self.bubbleView addSubview:self.voiceReadPoint];
  47. }
  48. return self;
  49. }
  50. - (void)prepareForReuse {
  51. [super prepareForReuse];
  52. for (UIView *view in self.bottomContainer.subviews) {
  53. [view removeFromSuperview];
  54. }
  55. }
  56. - (void)onLongPressTextViewMessage:(UITextView *)textView {
  57. if (self.delegate && [self.delegate respondsToSelector:@selector(onLongPressMessage:)]) {
  58. [self.delegate onLongPressMessage:self];
  59. }
  60. }
  61. // Override
  62. - (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
  63. NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.textData};
  64. [TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
  65. }
  66. - (void)fillWithData:(TUITextMessageCellData *)data {
  67. // set data
  68. [super fillWithData:data];
  69. self.textData = data;
  70. self.selectContent = data.content;
  71. self.voiceReadPoint.hidden = !data.showUnreadPoint;
  72. self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
  73. UIColor *textColor = self.class.incommingTextColor;
  74. UIFont *textFont = self.class.incommingTextFont;
  75. if (data.direction == MsgDirectionIncoming) {
  76. textColor = self.class.incommingTextColor;
  77. textFont = self.class.incommingTextFont;
  78. } else {
  79. textColor = self.class.outgoingTextColor;
  80. textFont = self.class.outgoingTextFont;
  81. }
  82. self.textView.attributedText = [data getContentAttributedString:textFont];
  83. self.textView.textColor = textColor;
  84. self.textView.font = textFont;
  85. // tell constraints they need updating
  86. [self setNeedsUpdateConstraints];
  87. // update constraints now so we can animate the change
  88. [self updateConstraintsIfNeeded];
  89. [self layoutIfNeeded];
  90. }
  91. + (BOOL)requiresConstraintBasedLayout {
  92. return YES;
  93. }
  94. // this is Apple's recommended place for adding/updating constraints
  95. - (void)updateConstraints {
  96. [super updateConstraints];
  97. [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
  98. make.leading.mas_equalTo(self.bubbleView.mas_leading).mas_offset(self.textData.textOrigin.x);
  99. make.top.mas_equalTo(self.bubbleView.mas_top).mas_offset(self.textData.textOrigin.y);
  100. make.size.mas_equalTo(self.textData.textSize);
  101. }];
  102. if (self.voiceReadPoint.hidden == NO) {
  103. [self.voiceReadPoint mas_remakeConstraints:^(MASConstraintMaker *make) {
  104. make.top.mas_equalTo(self.bubbleView);
  105. make.leading.mas_equalTo(self.bubbleView.mas_trailing).mas_offset(1);
  106. make.size.mas_equalTo(CGSizeMake(5, 5));
  107. }];
  108. }
  109. BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
  110. if (hasRiskContent ) {
  111. [self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
  112. make.top.mas_equalTo(self.textView.mas_bottom);
  113. make.width.mas_equalTo(self.bubbleView);
  114. make.bottom.mas_equalTo(self.container).mas_offset(- self.messageData.messageContainerAppendSize.height);
  115. }];
  116. }
  117. [self layoutBottomContainer];
  118. }
  119. - (void)layoutBottomContainer {
  120. if (CGSizeEqualToSize(self.textData.bottomContainerSize, CGSizeZero)) {
  121. return;
  122. }
  123. CGSize size = self.textData.bottomContainerSize;
  124. [self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
  125. if (self.textData.direction == MsgDirectionIncoming) {
  126. make.leading.mas_equalTo(self.container.mas_leading);
  127. }
  128. else {
  129. make.trailing.mas_equalTo(self.container.mas_trailing);
  130. }
  131. make.top.mas_equalTo(self.container.mas_bottom).offset(6);
  132. make.size.mas_equalTo(size);
  133. }];
  134. CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
  135. if (!self.messageModifyRepliesButton.hidden) {
  136. [self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  137. if (self.textData.direction == MsgDirectionIncoming) {
  138. make.leading.mas_equalTo(self.container.mas_leading);
  139. } else {
  140. make.trailing.mas_equalTo(self.container.mas_trailing);
  141. }
  142. make.top.mas_equalTo(self.bottomContainer.mas_bottom);
  143. make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
  144. }];
  145. }
  146. }
  147. - (void)textViewDidChangeSelection:(UITextView *)textView {
  148. NSAttributedString *selectedString = [textView.attributedText attributedSubstringFromRange:textView.selectedRange];
  149. if (self.selectAllContentContent && selectedString.length > 0) {
  150. if (selectedString.length == textView.attributedText.length) {
  151. self.selectAllContentContent(YES);
  152. } else {
  153. self.selectAllContentContent(NO);
  154. }
  155. }
  156. if (selectedString.length > 0) {
  157. NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
  158. [attributedString appendAttributedString:selectedString];
  159. NSUInteger offsetLocation = 0;
  160. for (NSDictionary *emojiLocation in self.textData.emojiLocations) {
  161. NSValue *key = emojiLocation.allKeys.firstObject;
  162. NSAttributedString *originStr = emojiLocation[key];
  163. NSRange currentRange = [key rangeValue];
  164. /**
  165. * After each emoji is replaced, the length of the string will change, and the actual location of the emoji will also change accordingly.
  166. */
  167. currentRange.location += offsetLocation;
  168. if (currentRange.location >= textView.selectedRange.location) {
  169. currentRange.location -= textView.selectedRange.location;
  170. if (currentRange.location + currentRange.length <= attributedString.length) {
  171. [attributedString replaceCharactersInRange:currentRange withAttributedString:originStr];
  172. offsetLocation += originStr.length - currentRange.length;
  173. }
  174. }
  175. }
  176. self.selectContent = attributedString.string;
  177. } else {
  178. self.selectContent = nil;
  179. [[NSNotificationCenter defaultCenter] postNotificationName:@"kTUIChatPopMenuWillHideNotification" object:nil];
  180. }
  181. }
  182. #pragma mark - TUIMessageCellProtocol
  183. + (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
  184. return 60.f;
  185. }
  186. + (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
  187. CGFloat height = [super getHeight:data withWidth:width];
  188. if (data.bottomContainerSize.height > 0) {
  189. height += data.bottomContainerSize.height + kScale375(6);
  190. }
  191. return height;
  192. }
  193. static CGSize gMaxTextSize;
  194. + (void)setMaxTextSize:(CGSize)maxTextSz {
  195. gMaxTextSize = maxTextSz;
  196. }
  197. + (CGSize)getContentSize:(TUIMessageCellData *)data {
  198. NSAssert([data isKindOfClass:TUITextMessageCellData.class], @"data must be kind of TUITextMessageCellData");
  199. TUITextMessageCellData *textCellData = (TUITextMessageCellData *)data;
  200. NSAttributedString *attributeString = [textCellData getContentAttributedString:
  201. data.direction == MsgDirectionIncoming ? self.incommingTextFont : self.outgoingTextFont];
  202. if (CGSizeEqualToSize(gMaxTextSize, CGSizeZero)) {
  203. gMaxTextSize = CGSizeMake(TTextMessageCell_Text_Width_Max, MAXFLOAT);
  204. }
  205. CGSize contentSize = [textCellData getContentAttributedStringSize:attributeString maxTextSize:gMaxTextSize];
  206. textCellData.textSize = contentSize;
  207. CGPoint textOrigin = CGPointMake(textCellData.cellLayout.bubbleInsets.left,
  208. textCellData.cellLayout.bubbleInsets.top);
  209. textCellData.textOrigin = textOrigin;
  210. CGFloat height = contentSize.height;
  211. CGFloat width = contentSize.width;
  212. height += textCellData.cellLayout.bubbleInsets.top;
  213. height += textCellData.cellLayout.bubbleInsets.bottom;
  214. width += textCellData.cellLayout.bubbleInsets.left;
  215. width += textCellData.cellLayout.bubbleInsets.right;
  216. if (textCellData.direction == MsgDirectionIncoming) {
  217. height = MAX(height, TUIBubbleMessageCell.incommingBubble.size.height);
  218. } else {
  219. height = MAX(height, TUIBubbleMessageCell.outgoingBubble.size.height);
  220. }
  221. BOOL hasRiskContent = textCellData.innerMessage.hasRiskContent;
  222. if (hasRiskContent) {
  223. width = MAX(width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
  224. height += kTUISecurityStrikeViewTopLineMargin;
  225. height += kTUISecurityStrikeViewTopLineToBottom;
  226. }
  227. CGSize size = CGSizeMake(width, height);
  228. return size;
  229. }
  230. @end
  231. @implementation TUITextMessageCell (TUILayoutConfiguration)
  232. + (void)initialize {
  233. [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
  234. }
  235. static UIColor *gOutgoingTextColor;
  236. + (UIColor *)outgoingTextColor {
  237. if (!gOutgoingTextColor) {
  238. gOutgoingTextColor = TUIChatDynamicColor(@"chat_text_message_send_text_color", @"#000000");
  239. }
  240. return gOutgoingTextColor;
  241. }
  242. + (void)setOutgoingTextColor:(UIColor *)outgoingTextColor {
  243. gOutgoingTextColor = outgoingTextColor;
  244. }
  245. static UIFont *gOutgoingTextFont;
  246. + (UIFont *)outgoingTextFont {
  247. if (!gOutgoingTextFont) {
  248. gOutgoingTextFont = [UIFont systemFontOfSize:16];
  249. }
  250. return gOutgoingTextFont;
  251. }
  252. + (void)setOutgoingTextFont:(UIFont *)outgoingTextFont {
  253. gOutgoingTextFont = outgoingTextFont;
  254. }
  255. static UIColor *gIncommingTextColor;
  256. + (UIColor *)incommingTextColor {
  257. if (!gIncommingTextColor) {
  258. gIncommingTextColor = TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000");
  259. }
  260. return gIncommingTextColor;
  261. }
  262. + (void)setIncommingTextColor:(UIColor *)incommingTextColor {
  263. gIncommingTextColor = incommingTextColor;
  264. }
  265. static UIFont *gIncommingTextFont;
  266. + (UIFont *)incommingTextFont {
  267. if (!gIncommingTextFont) {
  268. gIncommingTextFont = [UIFont systemFontOfSize:16];
  269. }
  270. return gIncommingTextFont;
  271. }
  272. + (void)setIncommingTextFont:(UIFont *)incommingTextFont {
  273. gIncommingTextFont = incommingTextFont;
  274. }
  275. + (void)onThemeChanged {
  276. gOutgoingTextColor = nil;
  277. gIncommingTextColor = nil;
  278. }
  279. @end