OWSMessageBubbleView.m 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598
  1. //
  2. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
  3. //
  4. #import "OWSMessageBubbleView.h"
  5. #import "AttachmentUploadView.h"
  6. #import "ConversationViewItem.h"
  7. #import "OWSAudioMessageView.h"
  8. #import "OWSBubbleShapeView.h"
  9. #import "OWSBubbleView.h"
  10. #import "OWSContactShareButtonsView.h"
  11. #import "OWSContactShareView.h"
  12. #import "OWSGenericAttachmentView.h"
  13. #import "OWSLabel.h"
  14. #import "OWSMessageFooterView.h"
  15. #import "OWSMessageTextView.h"
  16. #import "OWSQuotedMessageView.h"
  17. #import "Signal-Swift.h"
  18. #import "UIColor+OWS.h"
  19. #import <SignalMessaging/UIView+OWS.h>
  20. NS_ASSUME_NONNULL_BEGIN
  21. @interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
  22. @property (nonatomic) OWSBubbleView *bubbleView;
  23. @property (nonatomic) UIStackView *stackView;
  24. @property (nonatomic) UILabel *senderNameLabel;
  25. @property (nonatomic) UIView *senderNameContainer;
  26. @property (nonatomic) OWSMessageTextView *bodyTextView;
  27. @property (nonatomic, nullable) UIView *quotedMessageView;
  28. @property (nonatomic, nullable) UIView *bodyMediaView;
  29. @property (nonatomic) LinkPreviewView *linkPreviewView;
  30. // Should lazy-load expensive view contents (images, etc.).
  31. // Should do nothing if view is already loaded.
  32. @property (nonatomic, nullable) dispatch_block_t loadCellContentBlock;
  33. // Should unload all expensive view contents (images, etc.).
  34. @property (nonatomic, nullable) dispatch_block_t unloadCellContentBlock;
  35. @property (nonatomic, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
  36. @property (nonatomic) OWSMessageFooterView *footerView;
  37. @property (nonatomic, nullable) OWSContactShareButtonsView *contactShareButtonsView;
  38. @end
  39. #pragma mark -
  40. @implementation OWSMessageBubbleView
  41. #pragma mark - Dependencies
  42. - (OWSAttachmentDownloads *)attachmentDownloads
  43. {
  44. return SSKEnvironment.shared.attachmentDownloads;
  45. }
  46. #pragma mark -
  47. - (instancetype)initWithFrame:(CGRect)frame
  48. {
  49. self = [super initWithFrame:frame];
  50. if (!self) {
  51. return self;
  52. }
  53. [self commontInit];
  54. return self;
  55. }
  56. - (void)commontInit
  57. {
  58. // Ensure only called once.
  59. OWSAssertDebug(!self.bodyTextView);
  60. _viewConstraints = [NSMutableArray new];
  61. self.layoutMargins = UIEdgeInsetsZero;
  62. self.userInteractionEnabled = YES;
  63. self.bubbleView = [OWSBubbleView new];
  64. self.bubbleView.layoutMargins = UIEdgeInsetsZero;
  65. [self addSubview:self.bubbleView];
  66. [self.bubbleView autoPinEdgesToSuperviewEdges];
  67. self.stackView = [UIStackView new];
  68. self.stackView.axis = UILayoutConstraintAxisVertical;
  69. self.senderNameLabel = [OWSLabel new];
  70. self.senderNameContainer = [UIView new];
  71. self.senderNameContainer.layoutMargins = UIEdgeInsetsMake(0, 0, self.senderNameBottomSpacing, 0);
  72. [self.senderNameContainer addSubview:self.senderNameLabel];
  73. [self.senderNameLabel ows_autoPinToSuperviewMargins];
  74. self.bodyTextView = [self newTextView];
  75. self.bodyTextView.hidden = YES;
  76. self.linkPreviewView = [[LinkPreviewView alloc] initWithDraftDelegate:nil];
  77. self.footerView = [OWSMessageFooterView new];
  78. }
  79. - (OWSMessageTextView *)newTextView
  80. {
  81. OWSMessageTextView *textView = [OWSMessageTextView new];
  82. textView.backgroundColor = [UIColor clearColor];
  83. textView.opaque = NO;
  84. textView.editable = NO;
  85. textView.selectable = YES;
  86. textView.textContainerInset = UIEdgeInsetsZero;
  87. textView.contentInset = UIEdgeInsetsZero;
  88. textView.textContainer.lineFragmentPadding = 0;
  89. textView.scrollEnabled = NO;
  90. return textView;
  91. }
  92. - (UIFont *)textMessageFont
  93. {
  94. OWSAssertDebug(DisplayableText.kMaxJumbomojiCount == 5);
  95. CGFloat basePointSize = UIFont.ows_dynamicTypeBodyFont.pointSize;
  96. switch (self.displayableBodyText.jumbomojiCount) {
  97. case 0:
  98. break;
  99. case 1:
  100. return [UIFont ows_regularFontWithSize:basePointSize + 30.f];
  101. case 2:
  102. return [UIFont ows_regularFontWithSize:basePointSize + 24.f];
  103. case 3:
  104. case 4:
  105. case 5:
  106. return [UIFont ows_regularFontWithSize:basePointSize + 18.f];
  107. default:
  108. OWSFailDebug(@"Unexpected jumbomoji count: %zd", self.displayableBodyText.jumbomojiCount);
  109. break;
  110. }
  111. return UIFont.ows_dynamicTypeBodyFont;
  112. }
  113. #pragma mark - Convenience Accessors
  114. - (OWSMessageCellType)cellType
  115. {
  116. return self.viewItem.messageCellType;
  117. }
  118. - (BOOL)hasBodyText
  119. {
  120. // This should always be valid for the appropriate cell types.
  121. OWSAssertDebug(self.viewItem);
  122. return self.viewItem.hasBodyText;
  123. }
  124. - (nullable DisplayableText *)displayableBodyText
  125. {
  126. // This should always be valid for the appropriate cell types.
  127. OWSAssertDebug(self.viewItem.displayableBodyText);
  128. return self.viewItem.displayableBodyText;
  129. }
  130. - (TSMessage *)message
  131. {
  132. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  133. return (TSMessage *)self.viewItem.interaction;
  134. }
  135. - (BOOL)isQuotedReply
  136. {
  137. // This should always be valid for the appropriate cell types.
  138. OWSAssertDebug(self.viewItem);
  139. return self.viewItem.isQuotedReply;
  140. }
  141. - (BOOL)hasQuotedText
  142. {
  143. // This should always be valid for the appropriate cell types.
  144. OWSAssertDebug(self.viewItem);
  145. return self.viewItem.hasQuotedText;
  146. }
  147. - (BOOL)isIncoming
  148. {
  149. return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage;
  150. }
  151. - (BOOL)isOutgoing
  152. {
  153. return self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage;
  154. }
  155. #pragma mark - Load
  156. - (void)configureViews
  157. {
  158. OWSAssertDebug(self.conversationStyle);
  159. OWSAssertDebug(self.viewItem);
  160. OWSAssertDebug(self.viewItem.interaction);
  161. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  162. NSValue *_Nullable quotedMessageSize = [self quotedMessageSize];
  163. NSValue *_Nullable bodyMediaSize = [self bodyMediaSize];
  164. NSValue *_Nullable bodyTextSize = [self bodyTextSize];
  165. [self.bubbleView addSubview:self.stackView];
  166. [self.viewConstraints addObjectsFromArray:[self.stackView autoPinEdgesToSuperviewEdges]];
  167. NSMutableArray<UIView *> *textViews = [NSMutableArray new];
  168. if (self.shouldShowSenderName) {
  169. [self configureSenderNameLabel];
  170. [textViews addObject:self.senderNameContainer];
  171. }
  172. if (self.isQuotedReply) {
  173. // Flush any pending "text" subviews.
  174. BOOL isFirstSubview = ![self insertAnyTextViewsIntoStackView:textViews];
  175. [textViews removeAllObjects];
  176. if (isFirstSubview) {
  177. UIView *spacerView = [UIView containerView];
  178. [spacerView autoSetDimension:ALDimensionHeight toSize:self.quotedReplyTopMargin];
  179. [spacerView setCompressionResistanceHigh];
  180. [self.stackView addArrangedSubview:spacerView];
  181. }
  182. DisplayableText *_Nullable displayableQuotedText
  183. = (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
  184. OWSQuotedMessageView *quotedMessageView =
  185. [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
  186. displayableQuotedText:displayableQuotedText
  187. conversationStyle:self.conversationStyle
  188. isOutgoing:self.isOutgoing
  189. sharpCorners:self.sharpCornersForQuotedMessage];
  190. quotedMessageView.delegate = self;
  191. self.quotedMessageView = quotedMessageView;
  192. [quotedMessageView createContents];
  193. [self.stackView addArrangedSubview:quotedMessageView];
  194. OWSAssertDebug(quotedMessageSize);
  195. [self.viewConstraints addObject:[quotedMessageView autoSetDimension:ALDimensionHeight
  196. toSize:quotedMessageSize.CGSizeValue.height]];
  197. }
  198. UIView *_Nullable bodyMediaView = nil;
  199. switch (self.cellType) {
  200. case OWSMessageCellType_Unknown:
  201. case OWSMessageCellType_TextOnlyMessage:
  202. break;
  203. case OWSMessageCellType_Audio:
  204. bodyMediaView = [self loadViewForAudio];
  205. break;
  206. case OWSMessageCellType_GenericAttachment:
  207. bodyMediaView = [self loadViewForGenericAttachment];
  208. break;
  209. case OWSMessageCellType_ContactShare:
  210. bodyMediaView = [self loadViewForContactShare];
  211. break;
  212. case OWSMessageCellType_MediaMessage:
  213. bodyMediaView = [self loadViewForMediaAlbum];
  214. break;
  215. case OWSMessageCellType_OversizeTextDownloading:
  216. bodyMediaView = [self loadViewForOversizeTextDownload];
  217. break;
  218. }
  219. if (bodyMediaView) {
  220. OWSAssertDebug(self.loadCellContentBlock);
  221. OWSAssertDebug(self.unloadCellContentBlock);
  222. bodyMediaView.clipsToBounds = YES;
  223. self.bodyMediaView = bodyMediaView;
  224. bodyMediaView.userInteractionEnabled = NO;
  225. if (self.hasFullWidthMediaView) {
  226. // Flush any pending "text" subviews.
  227. [self insertAnyTextViewsIntoStackView:textViews];
  228. [textViews removeAllObjects];
  229. if (self.isQuotedReply) {
  230. UIView *spacerView = [UIView containerView];
  231. [spacerView autoSetDimension:ALDimensionHeight toSize:self.bodyMediaQuotedReplyVSpacing];
  232. [spacerView setCompressionResistanceHigh];
  233. [self.stackView addArrangedSubview:spacerView];
  234. }
  235. if (self.hasBodyMediaWithThumbnail) {
  236. [self.stackView addArrangedSubview:bodyMediaView];
  237. } else {
  238. OWSAssertDebug(self.cellType == OWSMessageCellType_ContactShare);
  239. if (self.contactShareHasSpacerTop) {
  240. UIView *spacerView = [UIView containerView];
  241. [spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
  242. [spacerView setCompressionResistanceHigh];
  243. [self.stackView addArrangedSubview:spacerView];
  244. }
  245. [self.stackView addArrangedSubview:bodyMediaView];
  246. if (self.contactShareHasSpacerBottom) {
  247. UIView *spacerView = [UIView containerView];
  248. [spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
  249. [spacerView setCompressionResistanceHigh];
  250. [self.stackView addArrangedSubview:spacerView];
  251. }
  252. }
  253. } else {
  254. [textViews addObject:bodyMediaView];
  255. }
  256. }
  257. if (self.viewItem.linkPreview) {
  258. if (self.isQuotedReply) {
  259. UIView *spacerView = [UIView containerView];
  260. [spacerView autoSetDimension:ALDimensionHeight toSize:self.bodyMediaQuotedReplyVSpacing];
  261. [spacerView setCompressionResistanceHigh];
  262. [self.stackView addArrangedSubview:spacerView];
  263. }
  264. self.linkPreviewView.state = self.linkPreviewState;
  265. [self.stackView addArrangedSubview:self.linkPreviewView];
  266. [self.linkPreviewView addBorderViewsWithBubbleView:self.bubbleView];
  267. }
  268. // We render malformed messages as "empty text" messages,
  269. // so create a text view if there is no body media view.
  270. if (self.hasBodyText || !bodyMediaView) {
  271. [self configureBodyTextView];
  272. [textViews addObject:self.bodyTextView];
  273. OWSAssertDebug(bodyTextSize);
  274. [self.viewConstraints addObjectsFromArray:@[
  275. [self.bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextSize.CGSizeValue.height],
  276. ]];
  277. UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary];
  278. if (tapForMoreLabel) {
  279. [textViews addObject:tapForMoreLabel];
  280. [self.viewConstraints addObjectsFromArray:@[
  281. [tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight],
  282. ]];
  283. }
  284. }
  285. BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && bodyMediaView && !self.hasBodyText);
  286. if (self.viewItem.shouldHideFooter) {
  287. // Do nothing.
  288. } else if (shouldFooterOverlayMedia) {
  289. OWSAssertDebug(bodyMediaView);
  290. CGFloat maxGradientHeight = 40.f;
  291. CAGradientLayer *gradientLayer = [CAGradientLayer new];
  292. gradientLayer.colors = @[
  293. (id)[UIColor colorWithWhite:0.f alpha:0.f].CGColor,
  294. (id)[UIColor colorWithWhite:0.f alpha:0.4f].CGColor,
  295. ];
  296. OWSLayerView *gradientView =
  297. [[OWSLayerView alloc] initWithFrame:CGRectZero
  298. layoutCallback:^(UIView *layerView) {
  299. CGRect layerFrame = layerView.bounds;
  300. layerFrame.size.height = MIN(maxGradientHeight, layerView.height);
  301. layerFrame.origin.y = layerView.height - layerFrame.size.height;
  302. gradientLayer.frame = layerFrame;
  303. }];
  304. [gradientView.layer addSublayer:gradientLayer];
  305. [bodyMediaView addSubview:gradientView];
  306. [self.viewConstraints addObjectsFromArray:[gradientView ows_autoPinToSuperviewEdges]];
  307. [self.footerView configureWithConversationViewItem:self.viewItem
  308. isOverlayingMedia:YES
  309. conversationStyle:self.conversationStyle
  310. isIncoming:self.isIncoming];
  311. [bodyMediaView addSubview:self.footerView];
  312. bodyMediaView.layoutMargins = UIEdgeInsetsZero;
  313. [self.viewConstraints addObjectsFromArray:@[
  314. [self.footerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
  315. [self.footerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
  316. [self.footerView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual],
  317. [self.footerView autoPinBottomToSuperviewMarginWithInset:self.conversationStyle.textInsetBottom],
  318. ]];
  319. } else {
  320. [self.footerView configureWithConversationViewItem:self.viewItem
  321. isOverlayingMedia:NO
  322. conversationStyle:self.conversationStyle
  323. isIncoming:self.isIncoming];
  324. [textViews addObject:self.footerView];
  325. }
  326. [self insertAnyTextViewsIntoStackView:textViews];
  327. CGSize bubbleSize = [self measureSize];
  328. [self.viewConstraints addObjectsFromArray:@[
  329. [self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width],
  330. ]];
  331. if (bodyMediaView) {
  332. OWSAssertDebug(bodyMediaSize);
  333. [self.viewConstraints
  334. addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.CGSizeValue.height]];
  335. }
  336. [self insertContactShareButtonsIfNecessary];
  337. [self updateBubbleColor];
  338. [self configureBubbleRounding];
  339. }
  340. - (void)insertContactShareButtonsIfNecessary
  341. {
  342. if (self.cellType != OWSMessageCellType_ContactShare) {
  343. return;
  344. }
  345. if (![OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
  346. return;
  347. }
  348. OWSAssertDebug(self.viewItem.contactShare);
  349. OWSContactShareButtonsView *buttonsView =
  350. [[OWSContactShareButtonsView alloc] initWithContactShare:self.viewItem.contactShare delegate:self];
  351. NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
  352. OWSAssertDebug(actionButtonsSize);
  353. [self.viewConstraints addObjectsFromArray:@[
  354. [buttonsView autoSetDimension:ALDimensionHeight toSize:actionButtonsSize.CGSizeValue.height],
  355. ]];
  356. // The "contact share" view casts a shadow "downward" onto adjacent views,
  357. // so we use a "proxy" view to take its place within the v-stack
  358. // view and then insert the "contact share" view above its proxy so that
  359. // it floats above the other content of the bubble view.
  360. UIView *proxyView = [UIView new];
  361. [self.stackView addArrangedSubview:proxyView];
  362. OWSBubbleShapeView *shadowView = [[OWSBubbleShapeView alloc] initShadow];
  363. OWSBubbleShapeView *clipView = [[OWSBubbleShapeView alloc] initClip];
  364. [self addSubview:shadowView];
  365. [self addSubview:clipView];
  366. [self.viewConstraints addObjectsFromArray:[shadowView autoPinToEdgesOfView:proxyView]];
  367. [self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]];
  368. [clipView addSubview:buttonsView];
  369. [self.viewConstraints addObjectsFromArray:[buttonsView ows_autoPinToSuperviewEdges]];
  370. [self.bubbleView addPartnerView:shadowView];
  371. [self.bubbleView addPartnerView:clipView];
  372. // Prevent the layer from animating changes.
  373. [CATransaction begin];
  374. [CATransaction setDisableActions:YES];
  375. OWSAssertDebug(buttonsView.backgroundColor);
  376. shadowView.fillColor = buttonsView.backgroundColor;
  377. shadowView.layer.shadowColor = Theme.boldColor.CGColor;
  378. shadowView.layer.shadowOpacity = 0.12f;
  379. shadowView.layer.shadowOffset = CGSizeZero;
  380. shadowView.layer.shadowRadius = 1.f;
  381. [CATransaction commit];
  382. }
  383. - (BOOL)contactShareHasSpacerTop
  384. {
  385. return (self.cellType == OWSMessageCellType_ContactShare && (self.isQuotedReply || !self.shouldShowSenderName));
  386. }
  387. - (BOOL)contactShareHasSpacerBottom
  388. {
  389. return (self.cellType == OWSMessageCellType_ContactShare && !self.hasBottomFooter);
  390. }
  391. - (CGFloat)contactShareVSpacing
  392. {
  393. return 12.f;
  394. }
  395. - (CGFloat)senderNameBottomSpacing
  396. {
  397. return 2.f;
  398. }
  399. - (OWSDirectionalRectCorner)sharpCorners
  400. {
  401. OWSDirectionalRectCorner sharpCorners = 0;
  402. if (!self.viewItem.isFirstInCluster) {
  403. sharpCorners = sharpCorners
  404. | (self.isIncoming ? OWSDirectionalRectCornerTopLeading : OWSDirectionalRectCornerTopTrailing);
  405. }
  406. if (!self.viewItem.isLastInCluster) {
  407. sharpCorners = sharpCorners
  408. | (self.isIncoming ? OWSDirectionalRectCornerBottomLeading : OWSDirectionalRectCornerBottomTrailing);
  409. }
  410. return sharpCorners;
  411. }
  412. - (OWSDirectionalRectCorner)sharpCornersForQuotedMessage
  413. {
  414. if (self.viewItem.senderName) {
  415. return OWSDirectionalRectCornerAllCorners;
  416. } else {
  417. return self.sharpCorners | OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing;
  418. }
  419. }
  420. - (void)configureBubbleRounding
  421. {
  422. self.bubbleView.sharpCorners = self.sharpCorners;
  423. }
  424. - (void)updateBubbleColor
  425. {
  426. BOOL hasOnlyBodyMediaView = (self.hasBodyMediaWithThumbnail && self.stackView.subviews.count == 1);
  427. if (!hasOnlyBodyMediaView) {
  428. self.bubbleView.bubbleColor = self.bubbleColor;
  429. } else {
  430. // Media-only messages should have no background color; they will fill the bubble's bounds
  431. // and we don't want artifacts at the edges.
  432. self.bubbleView.bubbleColor = nil;
  433. }
  434. }
  435. - (UIColor *)bubbleColor
  436. {
  437. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  438. TSMessage *message = (TSMessage *)self.viewItem.interaction;
  439. return [self.conversationStyle bubbleColorWithMessage:message];
  440. }
  441. - (BOOL)hasBodyMediaWithThumbnail
  442. {
  443. switch (self.cellType) {
  444. case OWSMessageCellType_Unknown:
  445. case OWSMessageCellType_TextOnlyMessage:
  446. case OWSMessageCellType_Audio:
  447. case OWSMessageCellType_GenericAttachment:
  448. case OWSMessageCellType_ContactShare:
  449. case OWSMessageCellType_OversizeTextDownloading:
  450. return NO;
  451. case OWSMessageCellType_MediaMessage:
  452. return YES;
  453. }
  454. }
  455. - (BOOL)hasBodyMediaView {
  456. switch (self.cellType) {
  457. case OWSMessageCellType_Unknown:
  458. case OWSMessageCellType_TextOnlyMessage:
  459. return NO;
  460. case OWSMessageCellType_Audio:
  461. case OWSMessageCellType_GenericAttachment:
  462. case OWSMessageCellType_ContactShare:
  463. case OWSMessageCellType_MediaMessage:
  464. case OWSMessageCellType_OversizeTextDownloading:
  465. return YES;
  466. }
  467. }
  468. - (BOOL)hasFullWidthMediaView
  469. {
  470. return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_ContactShare
  471. || self.cellType == OWSMessageCellType_MediaMessage);
  472. }
  473. - (BOOL)canFooterOverlayMedia
  474. {
  475. return self.hasBodyMediaWithThumbnail;
  476. }
  477. - (BOOL)hasBottomFooter
  478. {
  479. BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && self.hasBodyMediaView && !self.hasBodyText);
  480. if (self.viewItem.shouldHideFooter) {
  481. return NO;
  482. } else if (shouldFooterOverlayMedia) {
  483. return NO;
  484. } else {
  485. return YES;
  486. }
  487. }
  488. - (BOOL)insertAnyTextViewsIntoStackView:(NSArray<UIView *> *)textViews
  489. {
  490. if (textViews.count < 1) {
  491. return NO;
  492. }
  493. UIStackView *textStackView = [[UIStackView alloc] initWithArrangedSubviews:textViews];
  494. textStackView.axis = UILayoutConstraintAxisVertical;
  495. textStackView.spacing = self.textViewVSpacing;
  496. textStackView.layoutMarginsRelativeArrangement = YES;
  497. textStackView.layoutMargins = UIEdgeInsetsMake(self.conversationStyle.textInsetTop,
  498. self.conversationStyle.textInsetHorizontal,
  499. self.conversationStyle.textInsetBottom,
  500. self.conversationStyle.textInsetHorizontal);
  501. [self.stackView addArrangedSubview:textStackView];
  502. return YES;
  503. }
  504. - (CGFloat)textViewVSpacing
  505. {
  506. return 2.f;
  507. }
  508. - (CGFloat)bodyMediaQuotedReplyVSpacing
  509. {
  510. return 6.f;
  511. }
  512. - (CGFloat)quotedReplyTopMargin
  513. {
  514. return 6.f;
  515. }
  516. - (nullable LinkPreviewSent *)linkPreviewState
  517. {
  518. if (!self.viewItem.linkPreview) {
  519. return nil;
  520. }
  521. return [[LinkPreviewSent alloc] initWithLinkPreview:self.viewItem.linkPreview
  522. imageAttachment:self.viewItem.linkPreviewAttachment
  523. conversationStyle:self.conversationStyle];
  524. }
  525. #pragma mark - Load / Unload
  526. - (void)loadContent
  527. {
  528. if (self.loadCellContentBlock) {
  529. self.loadCellContentBlock();
  530. }
  531. }
  532. - (void)unloadContent
  533. {
  534. if (self.unloadCellContentBlock) {
  535. self.unloadCellContentBlock();
  536. }
  537. }
  538. #pragma mark - Subviews
  539. - (void)configureBodyTextView
  540. {
  541. OWSAssertDebug(self.hasBodyText);
  542. BOOL shouldIgnoreEvents = NO;
  543. if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
  544. // Ignore taps on links in outgoing messages that haven't been sent yet, as
  545. // this interferes with "tap to retry".
  546. TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
  547. shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSent;
  548. }
  549. [self.class loadForTextDisplay:self.bodyTextView
  550. displayableText:self.displayableBodyText
  551. searchText:self.delegate.lastSearchedText
  552. textColor:self.bodyTextColor
  553. font:self.textMessageFont
  554. shouldIgnoreEvents:shouldIgnoreEvents];
  555. }
  556. + (void)loadForTextDisplay:(OWSMessageTextView *)textView
  557. displayableText:(DisplayableText *)displayableText
  558. searchText:(nullable NSString *)searchText
  559. textColor:(UIColor *)textColor
  560. font:(UIFont *)font
  561. shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
  562. {
  563. textView.hidden = NO;
  564. textView.textColor = textColor;
  565. // Honor dynamic type in the message bodies.
  566. textView.font = font;
  567. textView.linkTextAttributes = @{
  568. NSForegroundColorAttributeName : textColor,
  569. NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid)
  570. };
  571. textView.shouldIgnoreEvents = shouldIgnoreEvents;
  572. NSString *text = displayableText.displayText;
  573. NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc]
  574. initWithString:text
  575. attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
  576. if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
  577. NSError *error;
  578. NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:searchText
  579. options:NSRegularExpressionCaseInsensitive
  580. error:&error];
  581. OWSAssertDebug(error == nil);
  582. for (NSTextCheckingResult *match in
  583. [regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
  584. OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
  585. [attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
  586. [attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
  587. }
  588. }
  589. [textView ensureShouldLinkifyText:displayableText.shouldAllowLinkification];
  590. // For perf, set text last. Otherwise changing font/color is more expensive.
  591. // We use attributedText even when we're not highlighting searched text to esnure any lingering
  592. // attributes are reset.
  593. textView.attributedText = attributedText;
  594. }
  595. - (BOOL)shouldShowSenderName
  596. {
  597. return self.viewItem.senderName.length > 0;
  598. }
  599. - (void)configureSenderNameLabel
  600. {
  601. OWSAssertDebug(self.senderNameLabel);
  602. OWSAssertDebug(self.shouldShowSenderName);
  603. self.senderNameLabel.textColor = self.bodyTextColor;
  604. self.senderNameLabel.font = OWSMessageBubbleView.senderNameFont;
  605. self.senderNameLabel.attributedText = self.viewItem.senderName;
  606. self.senderNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
  607. }
  608. + (UIFont *)senderNameFont
  609. {
  610. return UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
  611. }
  612. + (NSDictionary *)senderNamePrimaryAttributes
  613. {
  614. return @{
  615. NSFontAttributeName : self.senderNameFont,
  616. NSForegroundColorAttributeName : ConversationStyle.bubbleTextColorIncoming,
  617. };
  618. }
  619. + (NSDictionary *)senderNameSecondaryAttributes
  620. {
  621. return @{
  622. NSFontAttributeName : self.senderNameFont.ows_italic,
  623. NSForegroundColorAttributeName : ConversationStyle.bubbleTextColorIncoming,
  624. };
  625. }
  626. - (BOOL)hasTapForMore
  627. {
  628. if (!self.hasBodyText) {
  629. return NO;
  630. } else if (!self.displayableBodyText.isTextTruncated) {
  631. return NO;
  632. } else {
  633. return YES;
  634. }
  635. }
  636. - (nullable UIView *)createTapForMoreLabelIfNecessary
  637. {
  638. if (!self.hasTapForMore) {
  639. return nil;
  640. }
  641. UILabel *tapForMoreLabel = [UILabel new];
  642. tapForMoreLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_OVERSIZE_TEXT_TAP_FOR_MORE",
  643. @"Indicator on truncated text messages that they can be tapped to see the entire text message.");
  644. tapForMoreLabel.font = [self tapForMoreFont];
  645. tapForMoreLabel.textColor = [self.bodyTextColor colorWithAlphaComponent:0.85];
  646. tapForMoreLabel.textAlignment = [tapForMoreLabel textAlignmentUnnatural];
  647. return tapForMoreLabel;
  648. }
  649. - (UIView *)loadViewForMediaAlbum
  650. {
  651. OWSAssertDebug(self.viewItem.mediaAlbumItems);
  652. OWSMediaAlbumCellView *albumView =
  653. [[OWSMediaAlbumCellView alloc] initWithMediaCache:self.cellMediaCache
  654. items:self.viewItem.mediaAlbumItems
  655. isOutgoing:self.isOutgoing
  656. maxMessageWidth:self.conversationStyle.maxMessageWidth];
  657. self.loadCellContentBlock = ^{
  658. [albumView loadMedia];
  659. };
  660. self.unloadCellContentBlock = ^{
  661. [albumView unloadMedia];
  662. };
  663. // Only apply "inner shadow" for single media, not albums.
  664. if (albumView.itemViews.count == 1) {
  665. UIView *itemView = albumView.itemViews.firstObject;
  666. OWSBubbleShapeView *innerShadowView = [[OWSBubbleShapeView alloc]
  667. initInnerShadowWithColor:(Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_blackColor)
  668. radius:0.5f
  669. opacity:0.15f];
  670. [itemView addSubview:innerShadowView];
  671. [self.bubbleView addPartnerView:innerShadowView];
  672. [self.viewConstraints addObjectsFromArray:[innerShadowView ows_autoPinToSuperviewEdges]];
  673. }
  674. return albumView;
  675. }
  676. - (UIView *)loadViewForAudio
  677. {
  678. TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);
  679. OWSAssertDebug(attachment);
  680. OWSAssertDebug([attachment isAudio]);
  681. OWSAudioMessageView *audioMessageView = [[OWSAudioMessageView alloc] initWithAttachment:attachment
  682. isIncoming:self.isIncoming
  683. viewItem:self.viewItem
  684. conversationStyle:self.conversationStyle];
  685. self.viewItem.lastAudioMessageView = audioMessageView;
  686. [audioMessageView createContents];
  687. [self addProgressViewsIfNecessary:audioMessageView];
  688. self.loadCellContentBlock = ^{
  689. // Do nothing.
  690. };
  691. self.unloadCellContentBlock = ^{
  692. // Do nothing.
  693. };
  694. return audioMessageView;
  695. }
  696. - (UIView *)loadViewForGenericAttachment
  697. {
  698. TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);
  699. OWSAssertDebug(attachment);
  700. OWSGenericAttachmentView *attachmentView =
  701. [[OWSGenericAttachmentView alloc] initWithAttachment:attachment isIncoming:self.isIncoming];
  702. [attachmentView createContentsWithConversationStyle:self.conversationStyle];
  703. [self addProgressViewsIfNecessary:attachmentView];
  704. self.loadCellContentBlock = ^{
  705. // Do nothing.
  706. };
  707. self.unloadCellContentBlock = ^{
  708. // Do nothing.
  709. };
  710. return attachmentView;
  711. }
  712. - (UIView *)loadViewForContactShare
  713. {
  714. OWSAssertDebug(self.viewItem.contactShare);
  715. OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare
  716. isIncoming:self.isIncoming
  717. conversationStyle:self.conversationStyle];
  718. [contactShareView createContents];
  719. // TODO: Should we change appearance if contact avatar is uploading?
  720. self.loadCellContentBlock = ^{
  721. // Do nothing.
  722. };
  723. self.unloadCellContentBlock = ^{
  724. // Do nothing.
  725. };
  726. return contactShareView;
  727. }
  728. - (UIView *)loadViewForOversizeTextDownload
  729. {
  730. // We can use an empty view. The progress views will display download
  731. // progress or tap-to-retry UI.
  732. UIView *attachmentView = [UIView new];
  733. [self addProgressViewsIfNecessary:attachmentView];
  734. self.loadCellContentBlock = ^{
  735. // Do nothing.
  736. };
  737. self.unloadCellContentBlock = ^{
  738. // Do nothing.
  739. };
  740. return attachmentView;
  741. }
  742. - (void)addProgressViewsIfNecessary:(UIView *)bodyMediaView
  743. {
  744. if (self.viewItem.attachmentStream) {
  745. [self addUploadViewIfNecessary:bodyMediaView];
  746. } else if (self.viewItem.attachmentPointer) {
  747. [self addDownloadViewIfNecessary:bodyMediaView];
  748. }
  749. }
  750. - (void)addUploadViewIfNecessary:(UIView *)bodyMediaView
  751. {
  752. OWSAssertDebug(self.viewItem.attachmentStream);
  753. if (!self.isOutgoing) {
  754. return;
  755. }
  756. if (self.viewItem.attachmentStream.isUploaded) {
  757. return;
  758. }
  759. AttachmentUploadView *uploadView = [[AttachmentUploadView alloc] initWithAttachment:self.viewItem.attachmentStream];
  760. [self.bubbleView addSubview:uploadView];
  761. [uploadView autoPinEdgesToSuperviewEdges];
  762. [uploadView setContentHuggingLow];
  763. [uploadView setCompressionResistanceLow];
  764. }
  765. - (void)addDownloadViewIfNecessary:(UIView *)bodyMediaView
  766. {
  767. OWSAssertDebug(self.viewItem.attachmentPointer);
  768. switch (self.viewItem.attachmentPointer.state) {
  769. case TSAttachmentPointerStateFailed:
  770. [self addTapToRetryView:bodyMediaView];
  771. return;
  772. case TSAttachmentPointerStateEnqueued:
  773. case TSAttachmentPointerStateDownloading:
  774. break;
  775. }
  776. switch (self.viewItem.attachmentPointer.pointerType) {
  777. case TSAttachmentPointerTypeRestoring:
  778. // TODO: Show "restoring" indicator and possibly progress.
  779. return;
  780. case TSAttachmentPointerTypeUnknown:
  781. case TSAttachmentPointerTypeIncoming:
  782. break;
  783. }
  784. NSString *_Nullable uniqueId = self.viewItem.attachmentPointer.uniqueId;
  785. if (uniqueId.length < 1) {
  786. OWSFailDebug(@"Missing uniqueId.");
  787. return;
  788. }
  789. if ([self.attachmentDownloads downloadProgressForAttachmentId:uniqueId] == nil) {
  790. OWSFailDebug(@"Missing download progress.");
  791. return;
  792. }
  793. UIView *overlayView = [UIView new];
  794. overlayView.backgroundColor = [self.bubbleColor colorWithAlphaComponent:0.5];
  795. [bodyMediaView addSubview:overlayView];
  796. [overlayView autoPinEdgesToSuperviewEdges];
  797. [overlayView setContentHuggingLow];
  798. [overlayView setCompressionResistanceLow];
  799. MediaDownloadView *downloadView =
  800. [[MediaDownloadView alloc] initWithAttachmentId:uniqueId radius:self.conversationStyle.maxMessageWidth * 0.1f];
  801. bodyMediaView.layer.opacity = 0.5f;
  802. [self.bubbleView addSubview:downloadView];
  803. [downloadView autoPinEdgesToSuperviewEdges];
  804. [downloadView setContentHuggingLow];
  805. [downloadView setCompressionResistanceLow];
  806. }
  807. - (void)addTapToRetryView:(UIView *)bodyMediaView
  808. {
  809. OWSAssertDebug(self.viewItem.attachmentPointer);
  810. // Hide the body media view, replace with "tap to retry" indicator.
  811. UILabel *label = [UILabel new];
  812. label.text = NSLocalizedString(
  813. @"ATTACHMENT_DOWNLOADING_STATUS_FAILED", @"Status label when an attachment download has failed.");
  814. label.font = UIFont.ows_dynamicTypeBodyFont;
  815. label.textColor = Theme.secondaryColor;
  816. label.numberOfLines = 0;
  817. label.lineBreakMode = NSLineBreakByWordWrapping;
  818. label.textAlignment = NSTextAlignmentCenter;
  819. label.backgroundColor = self.bubbleColor;
  820. [bodyMediaView addSubview:label];
  821. [label autoPinEdgesToSuperviewMargins];
  822. [label setContentHuggingLow];
  823. [label setCompressionResistanceLow];
  824. }
  825. - (void)showAttachmentErrorViewWithMediaView:(UIView *)mediaView
  826. {
  827. OWSAssertDebug(mediaView);
  828. // TODO: We could do a better job of indicating that the media could not be loaded.
  829. UIView *errorView = [UIView new];
  830. errorView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
  831. errorView.userInteractionEnabled = NO;
  832. [mediaView addSubview:errorView];
  833. [errorView autoPinEdgesToSuperviewEdges];
  834. }
  835. #pragma mark - Measurement
  836. // Size of "message body" text, not quoted reply text.
  837. - (nullable NSValue *)bodyTextSize
  838. {
  839. OWSAssertDebug(self.conversationStyle);
  840. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  841. if (!self.hasBodyText) {
  842. return nil;
  843. }
  844. CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
  845. const int maxTextWidth = (int)floor(self.conversationStyle.maxMessageWidth - hMargins);
  846. [self configureBodyTextView];
  847. CGSize result = CGSizeCeil([self.bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
  848. return [NSValue valueWithCGSize:CGSizeCeil(result)];
  849. }
  850. - (nullable NSValue *)bodyMediaSize
  851. {
  852. OWSAssertDebug(self.conversationStyle);
  853. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  854. // This upper bound should have no effect in portrait orientation.
  855. // It limits body media size in landscape.
  856. const CGFloat kMaxBodyMediaSize = 350;
  857. CGFloat maxMessageWidth = MIN(kMaxBodyMediaSize, self.conversationStyle.maxMessageWidth);
  858. if (!self.hasFullWidthMediaView) {
  859. CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
  860. maxMessageWidth -= hMargins;
  861. }
  862. CGSize result = CGSizeZero;
  863. switch (self.cellType) {
  864. case OWSMessageCellType_Unknown:
  865. case OWSMessageCellType_TextOnlyMessage: {
  866. return nil;
  867. }
  868. case OWSMessageCellType_Audio:
  869. result = CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight);
  870. break;
  871. case OWSMessageCellType_GenericAttachment: {
  872. TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);
  873. OWSAssertDebug(attachment);
  874. OWSGenericAttachmentView *attachmentView =
  875. [[OWSGenericAttachmentView alloc] initWithAttachment:attachment isIncoming:self.isIncoming];
  876. [attachmentView createContentsWithConversationStyle:self.conversationStyle];
  877. result = [attachmentView measureSizeWithMaxMessageWidth:maxMessageWidth];
  878. break;
  879. }
  880. case OWSMessageCellType_ContactShare:
  881. OWSAssertDebug(self.viewItem.contactShare);
  882. result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]);
  883. break;
  884. case OWSMessageCellType_MediaMessage:
  885. result = [OWSMediaAlbumCellView layoutSizeForMaxMessageWidth:maxMessageWidth
  886. items:self.viewItem.mediaAlbumItems];
  887. if (self.viewItem.mediaAlbumItems.count == 1) {
  888. // Honor the content aspect ratio for single media.
  889. ConversationMediaAlbumItem *mediaAlbumItem = self.viewItem.mediaAlbumItems.firstObject;
  890. if (mediaAlbumItem.mediaSize.width > 0 && mediaAlbumItem.mediaSize.height > 0) {
  891. CGSize mediaSize = mediaAlbumItem.mediaSize;
  892. CGFloat contentAspectRatio = mediaSize.width / mediaSize.height;
  893. // Clamp the aspect ratio so that very thin/wide content is presented
  894. // in a reasonable way.
  895. const CGFloat minAspectRatio = 0.35f;
  896. const CGFloat maxAspectRatio = 1 / minAspectRatio;
  897. contentAspectRatio = MAX(minAspectRatio, MIN(maxAspectRatio, contentAspectRatio));
  898. const CGFloat maxMediaWidth = maxMessageWidth;
  899. const CGFloat maxMediaHeight = maxMessageWidth;
  900. CGFloat mediaWidth = maxMediaHeight * contentAspectRatio;
  901. CGFloat mediaHeight = maxMediaHeight;
  902. if (mediaWidth > maxMediaWidth) {
  903. mediaWidth = maxMediaWidth;
  904. mediaHeight = maxMediaWidth / contentAspectRatio;
  905. }
  906. // We don't want to blow up small images unnecessarily.
  907. const CGFloat kMinimumSize = 150.f;
  908. CGFloat shortSrcDimension = MIN(mediaSize.width, mediaSize.height);
  909. CGFloat shortDstDimension = MIN(mediaWidth, mediaHeight);
  910. if (shortDstDimension > kMinimumSize && shortDstDimension > shortSrcDimension) {
  911. CGFloat factor = kMinimumSize / shortDstDimension;
  912. mediaWidth *= factor;
  913. mediaHeight *= factor;
  914. }
  915. result = CGSizeRound(CGSizeMake(mediaWidth, mediaHeight));
  916. }
  917. }
  918. break;
  919. case OWSMessageCellType_OversizeTextDownloading:
  920. // There's no way to predict the size of the oversize text,
  921. // so we just use a square bubble.
  922. result = CGSizeMake(maxMessageWidth, maxMessageWidth);
  923. break;
  924. }
  925. OWSAssertDebug(result.width <= maxMessageWidth);
  926. result.width = MIN(result.width, maxMessageWidth);
  927. return [NSValue valueWithCGSize:CGSizeCeil(result)];
  928. }
  929. - (nullable NSValue *)quotedMessageSize
  930. {
  931. OWSAssertDebug(self.conversationStyle);
  932. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  933. OWSAssertDebug(self.viewItem);
  934. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  935. if (!self.isQuotedReply) {
  936. return nil;
  937. }
  938. DisplayableText *_Nullable displayableQuotedText
  939. = (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
  940. OWSQuotedMessageView *quotedMessageView =
  941. [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply
  942. displayableQuotedText:displayableQuotedText
  943. conversationStyle:self.conversationStyle
  944. isOutgoing:self.isOutgoing
  945. sharpCorners:self.sharpCornersForQuotedMessage];
  946. CGSize result = [quotedMessageView sizeForMaxWidth:self.conversationStyle.maxMessageWidth];
  947. return [NSValue valueWithCGSize:CGSizeCeil(result)];
  948. }
  949. - (nullable NSValue *)senderNameSize
  950. {
  951. OWSAssertDebug(self.conversationStyle);
  952. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  953. if (!self.shouldShowSenderName) {
  954. return nil;
  955. }
  956. CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
  957. const int maxTextWidth = (int)floor(self.conversationStyle.maxMessageWidth - hMargins);
  958. [self configureSenderNameLabel];
  959. CGSize result = CGSizeCeil([self.senderNameLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
  960. result.width = MIN(result.width, maxTextWidth);
  961. result.height += self.senderNameBottomSpacing;
  962. return [NSValue valueWithCGSize:result];
  963. }
  964. - (nullable NSValue *)actionButtonsSize
  965. {
  966. OWSAssertDebug(self.conversationStyle);
  967. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  968. if (self.cellType == OWSMessageCellType_ContactShare) {
  969. OWSAssertDebug(self.viewItem.contactShare);
  970. if ([OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
  971. CGSize buttonsSize = CGSizeCeil(
  972. CGSizeMake(self.conversationStyle.maxMessageWidth, [OWSContactShareButtonsView bubbleHeight]));
  973. return [NSValue valueWithCGSize:buttonsSize];
  974. }
  975. }
  976. return nil;
  977. }
  978. - (CGSize)measureSize
  979. {
  980. OWSAssertDebug(self.conversationStyle);
  981. OWSAssertDebug(self.conversationStyle.viewWidth > 0);
  982. OWSAssertDebug(self.viewItem);
  983. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  984. CGSize cellSize = CGSizeZero;
  985. [self configureBubbleRounding];
  986. NSMutableArray<NSValue *> *textViewSizes = [NSMutableArray new];
  987. NSValue *_Nullable senderNameSize = [self senderNameSize];
  988. if (senderNameSize) {
  989. [textViewSizes addObject:senderNameSize];
  990. }
  991. NSValue *_Nullable quotedMessageSize = [self quotedMessageSize];
  992. if (quotedMessageSize) {
  993. if (!senderNameSize) {
  994. cellSize.height += self.quotedReplyTopMargin;
  995. }
  996. cellSize.width = MAX(cellSize.width, quotedMessageSize.CGSizeValue.width);
  997. cellSize.height += quotedMessageSize.CGSizeValue.height;
  998. }
  999. NSValue *_Nullable bodyMediaSize = [self bodyMediaSize];
  1000. if (bodyMediaSize) {
  1001. if (self.hasFullWidthMediaView) {
  1002. cellSize.width = MAX(cellSize.width, bodyMediaSize.CGSizeValue.width);
  1003. cellSize.height += bodyMediaSize.CGSizeValue.height;
  1004. } else {
  1005. [textViewSizes addObject:bodyMediaSize];
  1006. bodyMediaSize = nil;
  1007. }
  1008. if (self.contactShareHasSpacerTop) {
  1009. cellSize.height += self.contactShareVSpacing;
  1010. }
  1011. if (self.contactShareHasSpacerBottom) {
  1012. cellSize.height += self.contactShareVSpacing;
  1013. }
  1014. }
  1015. if (bodyMediaSize || quotedMessageSize) {
  1016. if (textViewSizes.count > 0) {
  1017. CGSize groupSize = [self sizeForTextViewGroup:textViewSizes];
  1018. cellSize.width = MAX(cellSize.width, groupSize.width);
  1019. cellSize.height += groupSize.height;
  1020. [textViewSizes removeAllObjects];
  1021. }
  1022. if (bodyMediaSize && quotedMessageSize && self.hasFullWidthMediaView) {
  1023. cellSize.height += self.bodyMediaQuotedReplyVSpacing;
  1024. } else if (quotedMessageSize && self.viewItem.linkPreview) {
  1025. cellSize.height += self.bodyMediaQuotedReplyVSpacing;
  1026. }
  1027. }
  1028. if (self.viewItem.linkPreview) {
  1029. CGSize linkPreviewSize = [self.linkPreviewView measureWithSentState:self.linkPreviewState];
  1030. linkPreviewSize.width = MIN(linkPreviewSize.width, self.conversationStyle.maxMessageWidth);
  1031. cellSize.width = MAX(cellSize.width, linkPreviewSize.width);
  1032. cellSize.height += linkPreviewSize.height;
  1033. }
  1034. NSValue *_Nullable bodyTextSize = [self bodyTextSize];
  1035. if (bodyTextSize) {
  1036. [textViewSizes addObject:bodyTextSize];
  1037. }
  1038. if (self.hasBottomFooter) {
  1039. CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
  1040. footerSize.width = MIN(footerSize.width, self.conversationStyle.maxMessageWidth);
  1041. [textViewSizes addObject:[NSValue valueWithCGSize:footerSize]];
  1042. }
  1043. if (textViewSizes.count > 0) {
  1044. CGSize groupSize = [self sizeForTextViewGroup:textViewSizes];
  1045. cellSize.width = MAX(cellSize.width, groupSize.width);
  1046. cellSize.height += groupSize.height;
  1047. }
  1048. // Make sure the bubble is always wide enough to complete it's bubble shape.
  1049. cellSize.width = MAX(cellSize.width, self.bubbleView.minWidth);
  1050. OWSAssertDebug(cellSize.width > 0 && cellSize.height > 0);
  1051. if (self.hasTapForMore) {
  1052. cellSize.height += self.tapForMoreHeight + self.textViewVSpacing;
  1053. }
  1054. NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
  1055. if (actionButtonsSize) {
  1056. cellSize.width = MAX(cellSize.width, actionButtonsSize.CGSizeValue.width);
  1057. cellSize.height += actionButtonsSize.CGSizeValue.height;
  1058. }
  1059. cellSize = CGSizeCeil(cellSize);
  1060. OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
  1061. cellSize.width = MIN(cellSize.width, self.conversationStyle.maxMessageWidth);
  1062. return cellSize;
  1063. }
  1064. - (CGSize)sizeForTextViewGroup:(NSArray<NSValue *> *)textViewSizes
  1065. {
  1066. OWSAssertDebug(textViewSizes);
  1067. OWSAssertDebug(textViewSizes.count > 0);
  1068. OWSAssertDebug(self.conversationStyle);
  1069. OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
  1070. CGSize result = CGSizeZero;
  1071. for (NSValue *size in textViewSizes) {
  1072. result.width = MAX(result.width, size.CGSizeValue.width);
  1073. result.height += size.CGSizeValue.height;
  1074. }
  1075. result.height += self.textViewVSpacing * (textViewSizes.count - 1);
  1076. result.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
  1077. result.width += self.conversationStyle.textInsetHorizontal * 2;
  1078. return result;
  1079. }
  1080. - (UIFont *)tapForMoreFont
  1081. {
  1082. return UIFont.ows_dynamicTypeCaption1Font;
  1083. }
  1084. - (CGFloat)tapForMoreHeight
  1085. {
  1086. return (CGFloat)ceil([self tapForMoreFont].lineHeight * 1.25);
  1087. }
  1088. #pragma mark -
  1089. - (UIColor *)bodyTextColor
  1090. {
  1091. OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
  1092. TSMessage *message = (TSMessage *)self.viewItem.interaction;
  1093. return [self.conversationStyle bubbleTextColorWithMessage:message];
  1094. }
  1095. - (void)prepareForReuse
  1096. {
  1097. [NSLayoutConstraint deactivateConstraints:self.viewConstraints];
  1098. self.viewConstraints = [NSMutableArray new];
  1099. self.delegate = nil;
  1100. [self.bodyTextView removeFromSuperview];
  1101. self.bodyTextView.text = nil;
  1102. self.bodyTextView.attributedText = nil;
  1103. self.bodyTextView.hidden = YES;
  1104. self.bubbleView.bubbleColor = nil;
  1105. [self.bubbleView clearPartnerViews];
  1106. for (UIView *subview in self.bubbleView.subviews) {
  1107. [subview removeFromSuperview];
  1108. }
  1109. if (self.unloadCellContentBlock) {
  1110. self.unloadCellContentBlock();
  1111. }
  1112. self.loadCellContentBlock = nil;
  1113. self.unloadCellContentBlock = nil;
  1114. for (UIView *subview in self.bodyMediaView.subviews) {
  1115. [subview removeFromSuperview];
  1116. }
  1117. [self.bodyMediaView removeFromSuperview];
  1118. self.bodyMediaView = nil;
  1119. [self.quotedMessageView removeFromSuperview];
  1120. self.quotedMessageView = nil;
  1121. [self.footerView removeFromSuperview];
  1122. [self.footerView prepareForReuse];
  1123. for (UIView *subview in self.stackView.subviews) {
  1124. [subview removeFromSuperview];
  1125. }
  1126. for (UIView *subview in self.subviews) {
  1127. if (subview != self.bubbleView) {
  1128. [subview removeFromSuperview];
  1129. }
  1130. }
  1131. [self.contactShareButtonsView removeFromSuperview];
  1132. self.contactShareButtonsView = nil;
  1133. [self.linkPreviewView removeFromSuperview];
  1134. self.linkPreviewView.state = nil;
  1135. }
  1136. #pragma mark - Gestures
  1137. - (void)addTapGestureHandler
  1138. {
  1139. UITapGestureRecognizer *tap =
  1140. [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
  1141. [self addGestureRecognizer:tap];
  1142. }
  1143. - (void)handleTapGesture:(UITapGestureRecognizer *)sender
  1144. {
  1145. OWSAssertDebug(self.delegate);
  1146. if (sender.state != UIGestureRecognizerStateRecognized) {
  1147. OWSLogVerbose(@"Ignoring tap on message: %@", self.viewItem.interaction.debugDescription);
  1148. return;
  1149. }
  1150. if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
  1151. TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
  1152. if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
  1153. return;
  1154. } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
  1155. // Ignore taps on outgoing messages being sent.
  1156. return;
  1157. }
  1158. }
  1159. if (self.contactShareButtonsView) {
  1160. if ([self.contactShareButtonsView handleTapGesture:sender]) {
  1161. return;
  1162. }
  1163. }
  1164. CGPoint locationInMessageBubble = [sender locationInView:self];
  1165. switch ([self gestureLocationForLocation:locationInMessageBubble]) {
  1166. case OWSMessageGestureLocation_Default:
  1167. // Do nothing.
  1168. return;
  1169. case OWSMessageGestureLocation_OversizeText:
  1170. [self.delegate didTapTruncatedTextMessage:self.viewItem];
  1171. return;
  1172. case OWSMessageGestureLocation_Media:
  1173. [self handleMediaTapGesture:locationInMessageBubble];
  1174. break;
  1175. case OWSMessageGestureLocation_QuotedReply:
  1176. if (self.viewItem.quotedReply) {
  1177. [self.delegate didTapConversationItem:self.viewItem quotedReply:self.viewItem.quotedReply];
  1178. } else {
  1179. OWSFailDebug(@"Missing quoted message.");
  1180. }
  1181. break;
  1182. case OWSMessageGestureLocation_LinkPreview:
  1183. if (self.viewItem.linkPreview) {
  1184. [self.delegate didTapConversationItem:self.viewItem linkPreview:self.viewItem.linkPreview];
  1185. } else {
  1186. OWSFailDebug(@"Missing link preview.");
  1187. }
  1188. break;
  1189. }
  1190. }
  1191. - (void)handleMediaTapGesture:(CGPoint)locationInMessageBubble
  1192. {
  1193. OWSAssertDebug(self.delegate);
  1194. if (self.viewItem.attachmentPointer && self.viewItem.attachmentPointer.state == TSAttachmentPointerStateFailed) {
  1195. [self.delegate didTapFailedIncomingAttachment:self.viewItem];
  1196. return;
  1197. }
  1198. switch (self.cellType) {
  1199. case OWSMessageCellType_Unknown:
  1200. case OWSMessageCellType_TextOnlyMessage:
  1201. case OWSMessageCellType_OversizeTextDownloading:
  1202. break;
  1203. case OWSMessageCellType_Audio:
  1204. if (self.viewItem.attachmentStream) {
  1205. [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.viewItem.attachmentStream];
  1206. }
  1207. return;
  1208. case OWSMessageCellType_GenericAttachment:
  1209. if (self.viewItem.attachmentStream) {
  1210. [AttachmentSharing showShareUIForAttachment:self.viewItem.attachmentStream];
  1211. }
  1212. break;
  1213. case OWSMessageCellType_ContactShare:
  1214. [self.delegate didTapContactShareViewItem:self.viewItem];
  1215. break;
  1216. case OWSMessageCellType_MediaMessage: {
  1217. OWSAssertDebug(self.bodyMediaView);
  1218. OWSAssertDebug(self.viewItem.mediaAlbumItems.count > 0);
  1219. if (![self.bodyMediaView isKindOfClass:[OWSMediaAlbumCellView class]]) {
  1220. OWSFailDebug(@"Unexpected body media view: %@", self.bodyMediaView.class);
  1221. return;
  1222. }
  1223. OWSMediaAlbumCellView *_Nullable mediaAlbumCellView = (OWSMediaAlbumCellView *)self.bodyMediaView;
  1224. CGPoint location = [self convertPoint:locationInMessageBubble toView:self.bodyMediaView];
  1225. OWSConversationMediaView *_Nullable mediaView = [mediaAlbumCellView mediaViewForLocation:location];
  1226. if (!mediaView) {
  1227. OWSFailDebug(@"Missing media view.");
  1228. return;
  1229. }
  1230. if ([mediaAlbumCellView isMoreItemsViewWithMediaView:mediaView]
  1231. && self.viewItem.mediaAlbumHasFailedAttachment) {
  1232. [self.delegate didTapFailedIncomingAttachment:self.viewItem];
  1233. return;
  1234. }
  1235. TSAttachment *attachment = mediaView.attachment;
  1236. if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
  1237. TSAttachmentPointer *attachmentPointer = (TSAttachmentPointer *)attachment;
  1238. if (attachmentPointer.state == TSAttachmentPointerStateFailed) {
  1239. // Treat the tap as a "retry" tap if the user taps on a failed download.
  1240. [self.delegate didTapFailedIncomingAttachment:self.viewItem];
  1241. return;
  1242. }
  1243. } else if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
  1244. OWSLogWarn(@"Media attachment not yet downloaded.");
  1245. return;
  1246. }
  1247. TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
  1248. [self.delegate didTapImageViewItem:self.viewItem attachmentStream:attachmentStream imageView:mediaView];
  1249. break;
  1250. }
  1251. }
  1252. }
  1253. - (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble
  1254. {
  1255. if (self.quotedMessageView) {
  1256. // Treat this as a "quoted reply" gesture if:
  1257. //
  1258. // * There is a "quoted reply" view.
  1259. // * The gesture occured within or above the "quoted reply" view.
  1260. CGPoint location = [self convertPoint:locationInMessageBubble toView:self.quotedMessageView];
  1261. if (location.y <= self.quotedMessageView.height) {
  1262. return OWSMessageGestureLocation_QuotedReply;
  1263. }
  1264. }
  1265. if (self.viewItem.linkPreview) {
  1266. CGPoint location = [self convertPoint:locationInMessageBubble toView:self.linkPreviewView];
  1267. if (CGRectContainsPoint(self.linkPreviewView.bounds, location)) {
  1268. return OWSMessageGestureLocation_LinkPreview;
  1269. }
  1270. }
  1271. if (self.bodyMediaView) {
  1272. // Treat this as a "body media" gesture if:
  1273. //
  1274. // * There is a "body media" view.
  1275. // * The gesture occured within or above the "body media" view...
  1276. // * ...OR if the message doesn't have body text.
  1277. CGPoint location = [self convertPoint:locationInMessageBubble toView:self.bodyMediaView];
  1278. if (location.y <= self.bodyMediaView.height) {
  1279. return OWSMessageGestureLocation_Media;
  1280. }
  1281. if (!self.viewItem.hasBodyText) {
  1282. return OWSMessageGestureLocation_Media;
  1283. }
  1284. }
  1285. if (self.hasTapForMore) {
  1286. return OWSMessageGestureLocation_OversizeText;
  1287. }
  1288. return OWSMessageGestureLocation_Default;
  1289. }
  1290. - (void)didTapQuotedReply:(OWSQuotedReplyModel *)quotedReply
  1291. failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer
  1292. {
  1293. [self.delegate didTapConversationItem:self.viewItem
  1294. quotedReply:quotedReply
  1295. failedThumbnailDownloadAttachmentPointer:attachmentPointer];
  1296. }
  1297. - (void)didCancelQuotedReply
  1298. {
  1299. OWSFailDebug(@"Sent quoted replies should not be cancellable.");
  1300. }
  1301. #pragma mark - OWSContactShareButtonsViewDelegate
  1302. - (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
  1303. {
  1304. OWSAssertIsOnMainThread();
  1305. OWSAssertDebug(contactShare);
  1306. [self.delegate didTapSendMessageToContactShare:contactShare];
  1307. }
  1308. - (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
  1309. {
  1310. OWSAssertIsOnMainThread();
  1311. OWSAssertDebug(contactShare);
  1312. [self.delegate didTapSendInviteToContactShare:contactShare];
  1313. }
  1314. - (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
  1315. {
  1316. OWSAssertIsOnMainThread();
  1317. OWSAssertDebug(contactShare);
  1318. [self.delegate didTapShowAddToContactUIForContactShare:contactShare];
  1319. }
  1320. @end
  1321. NS_ASSUME_NONNULL_END