Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add markdown support for chat messages #1296

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
1F0A1D442A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
1F0ECBF52A68274400921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF42A68274400921E90 /* CDMarkdownKit */; };
1F0ECBF72A68277000921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF62A68277000921E90 /* CDMarkdownKit */; };
1F0ECBF92A68277C00921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF82A68277C00921E90 /* CDMarkdownKit */; };
1F11FB7229C07B04001E21E7 /* NCZoomableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */; };
1F1C0D7F29A7F33600D17C6D /* NCNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */; };
1F1C0D8729AFB88800D17C6D /* VLCKitVideoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */; };
Expand Down Expand Up @@ -73,6 +77,8 @@
1FA732FC2966CBB7003D2103 /* CallFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA732FB2966CBB7003D2103 /* CallFlowLayout.swift */; };
1FB52E762842C75E00AC741B /* QRCodeLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB52E752842C75E00AC741B /* QRCodeLoginController.swift */; };
1FB6678F28CE381300D29F8D /* SubtitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB6678E28CE381300D29F8D /* SubtitleTableViewCell.swift */; };
1FC940B92A5F21FC00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
1FD8AE6B2A3A216300787C16 /* NextcloudTalkUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8AD8C2A3A162100787C16 /* NextcloudTalkUITests.swift */; };
1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9182828C55A73009092AB /* BGTaskHelper.swift */; };
1FDCC3D429EBF6E700DEB39B /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */; };
Expand Down Expand Up @@ -419,6 +425,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMarkdownObjCBridge.swift; sourceTree = "<group>"; };
1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCZoomableView.swift; sourceTree = "<group>"; };
1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VLCKitVideoViewController.xib; sourceTree = "<group>"; };
1F1C0D8829AFB89900D17C6D /* VLCKitVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCKitVideoViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -891,6 +898,7 @@
1F45A1162A01D6EC005FE87D /* SDWebImage in Frameworks */,
2C90E5641EDDE0FB0093D85A /* Foundation.framework in Frameworks */,
1F468E7628DCC6C60099597B /* Dynamic in Frameworks */,
1F0ECBF52A68274400921E90 /* CDMarkdownKit in Frameworks */,
2C38D4AC27BBAFCC00BAE015 /* WebRTC.xcframework in Frameworks */,
1F66B72F29FABD01003FB168 /* SwiftyAttributes in Frameworks */,
1F7AE07829142CA1009F72AD /* NextcloudKit in Frameworks */,
Expand All @@ -903,6 +911,7 @@
buildActionMask = 2147483647;
files = (
1F7AE07C29142E6A009F72AD /* NextcloudKit in Frameworks */,
1F0ECBF92A68277C00921E90 /* CDMarkdownKit in Frameworks */,
8789AE73BFCAA413B43319C0 /* libPods-ShareExtension.a in Frameworks */,
1F45A11A2A01D70E005FE87D /* SDWebImage in Frameworks */,
1F45A1252A01D8F7005FE87D /* SDWebImageSVGKitPlugin in Frameworks */,
Expand All @@ -914,6 +923,7 @@
buildActionMask = 2147483647;
files = (
1F45A1232A01D8F1005FE87D /* SDWebImageSVGKitPlugin in Frameworks */,
1F0ECBF72A68277000921E90 /* CDMarkdownKit in Frameworks */,
1F7AE07A29142E62009F72AD /* NextcloudKit in Frameworks */,
1F7AE07D29158878009F72AD /* IntentsUI.framework in Frameworks */,
3FCA62550CD1442D28E8A7C6 /* libPods-NotificationServiceExtension.a in Frameworks */,
Expand Down Expand Up @@ -1371,6 +1381,7 @@
2CA52ACF267613CA00619610 /* VoiceMessageTableViewCell.m */,
2C5BFBEB28895E6A00E75118 /* ObjectShareMessageTableViewCell.h */,
2C5BFBEC28895E6B00E75118 /* ObjectShareMessageTableViewCell.m */,
1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */,
);
name = "Chat cells";
sourceTree = "<group>";
Expand Down Expand Up @@ -1658,6 +1669,7 @@
1F66B72E29FABD01003FB168 /* SwiftyAttributes */,
1F45A1152A01D6EC005FE87D /* SDWebImage */,
1F45A1202A01D8BA005FE87D /* SDWebImageSVGKitPlugin */,
1F0ECBF42A68274400921E90 /* CDMarkdownKit */,
);
productName = NextcloudTalk;
productReference = 2C05747D1EDD9E8E00D9E7F2 /* NextcloudTalk.app */;
Expand All @@ -1682,6 +1694,7 @@
1F7AE07B29142E6A009F72AD /* NextcloudKit */,
1F45A1192A01D70E005FE87D /* SDWebImage */,
1F45A1242A01D8F7005FE87D /* SDWebImageSVGKitPlugin */,
1F0ECBF82A68277C00921E90 /* CDMarkdownKit */,
);
productName = ShareExtension;
productReference = 2C62AFA324C08845007E460A /* ShareExtension.appex */;
Expand All @@ -1705,6 +1718,7 @@
1F7AE07929142E62009F72AD /* NextcloudKit */,
1F45A11D2A01D719005FE87D /* SDWebImage */,
1F45A1222A01D8F1005FE87D /* SDWebImageSVGKitPlugin */,
1F0ECBF62A68277000921E90 /* CDMarkdownKit */,
);
productName = NotificationServiceExtension;
productReference = 2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */;
Expand Down Expand Up @@ -1792,6 +1806,7 @@
1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */,
1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */,
1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */,
1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */,
);
productRefGroup = 2C05747E1EDD9E8E00D9E7F2 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -2203,6 +2218,7 @@
2C5BFBED28895E6B00E75118 /* ObjectShareMessageTableViewCell.m in Sources */,
2C6E74462386D33200AE396C /* ReplyMessageView.m in Sources */,
DA8801A227A2DA00009EF248 /* UserProfileTableViewController.swift in Sources */,
1F0A1D442A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift in Sources */,
2C16A82C28E7284D00EDE523 /* NCButton.swift in Sources */,
DA75580F278EEA1000A48A1B /* SettingsTableViewController.swift in Sources */,
2CB6ACD22640814100D3D641 /* LocationMessageTableViewCell.m in Sources */,
Expand Down Expand Up @@ -2293,6 +2309,7 @@
2C444705265D641300DF1DBC /* NCUserDefaults.m in Sources */,
2C4446F5265D583200DF1DBC /* NCKeyChainController.m in Sources */,
2C62AFFA24C1BDA5007E460A /* NCChatMessage.m in Sources */,
1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */,
2C4446D52658147900DF1DBC /* TalkAccount.m in Sources */,
2C1EF36D25505DCE007C9768 /* NCNavigationController.m in Sources */,
1FEDE3D0257D43AB00853F79 /* NCMessageFileParameter.m in Sources */,
Expand Down Expand Up @@ -2330,6 +2347,7 @@
2CC001C124A37AC500A20167 /* NCNotification.m in Sources */,
2C4446FD265D5DFA00DF1DBC /* ABContact.m in Sources */,
2C4446F8265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */,
1FC940B92A5F21FC00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */,
2CB6ACDB2641483800D3D641 /* NCMessageLocationParameter.m in Sources */,
2C4446FB265D5C5700DF1DBC /* NCRoomParticipants.m in Sources */,
2CC1FF492818395E009F7288 /* NCDeckCardParameter.m in Sources */,
Expand Down Expand Up @@ -3098,6 +3116,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud-deps/CDMarkdownKit.git";
requirement = {
kind = revision;
revision = 5581a065b865a88accea1a305923d83e14d1beba;
};
};
1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImage.git";
Expand Down Expand Up @@ -3157,6 +3183,21 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
1F0ECBF42A68274400921E90 /* CDMarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
productName = CDMarkdownKit;
};
1F0ECBF62A68277000921E90 /* CDMarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
productName = CDMarkdownKit;
};
1F0ECBF82A68277C00921E90 /* CDMarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
productName = CDMarkdownKit;
};
1F45A1152A01D6EC005FE87D /* SDWebImage */ = {
isa = XCSwiftPackageProductDependency;
package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
Expand Down
4 changes: 2 additions & 2 deletions NextcloudTalk/ChatMessageTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ - (MessageBodyTextView *)bodyTextView
- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead
{
self.titleLabel.text = message.actorDisplayName;
self.bodyTextView.attributedText = message.parsedMessageForChat;
self.bodyTextView.attributedText = message.parsedMarkdownForChat;
self.messageId = message.messageId;
self.message = message;
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp];
Expand Down Expand Up @@ -318,7 +318,7 @@ - (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSIn
NCChatMessage *parent = message.parent;
if (parent.message) {
self.quotedMessageView.actorLabel.text = ([parent.actorDisplayName isEqualToString:@""]) ? NSLocalizedString(@"Guest", nil) : parent.actorDisplayName;
self.quotedMessageView.messageLabel.text = parent.parsedMessageForChat.string;
self.quotedMessageView.messageLabel.text = parent.parsedMarkdownForChat.string;
self.quotedMessageView.highlighted = [parent isMessageFromUser:activeAccount.userId];
}

Expand Down
2 changes: 1 addition & 1 deletion NextcloudTalk/FileMessageTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ - (void)prepareForReuse
- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead
{
self.titleLabel.text = message.actorDisplayName;
self.bodyTextView.attributedText = message.parsedMessageForChat;
self.bodyTextView.attributedText = message.parsedMessage;
self.messageId = message.messageId;
self.message = message;

Expand Down
2 changes: 1 addition & 1 deletion NextcloudTalk/GroupedChatMessageTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ - (void)prepareForReuse

- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead
{
self.bodyTextView.attributedText = message.parsedMessageForChat;
self.bodyTextView.attributedText = message.parsedMarkdownForChat;
self.messageId = message.messageId;
self.message = message;

Expand Down
2 changes: 1 addition & 1 deletion NextcloudTalk/LocationMessageTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ - (void)prepareForReuse
- (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead
{
self.titleLabel.text = message.actorDisplayName;
self.bodyTextView.attributedText = message.parsedMessageForChat;
self.bodyTextView.attributedText = message.parsedMessage;
self.messageId = message.messageId;
self.message = message;

Expand Down
9 changes: 7 additions & 2 deletions NextcloudTalk/MessageBodyTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ @implementation MessageBodyTextView

- (instancetype)init
{
self = [super init];
// Use TextKit1 to have the background of code/syntax blocks span the whole line
if (@available(iOS 16.0, *)) {
self = [MessageBodyTextView textViewUsingTextLayoutManager:false];
} else {
self = [super init];
}

if (!self) {
return nil;
}

self.dataDetectorTypes = UIDataDetectorTypeAll;
self.textContainer.lineFragmentPadding = 0;
self.textContainerInset = UIEdgeInsetsZero;
Expand Down
3 changes: 2 additions & 1 deletion NextcloudTalk/NCChatMessage.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ typedef void (^GetReferenceDataCompletionBlock)(NCChatMessage *message, NSDictio
- (NSString *)objectShareLink;
- (NSDictionary *)messageParameters;
- (NSMutableAttributedString *)parsedMessage;
- (NSMutableAttributedString *)parsedMessageForChat;
- (NSMutableAttributedString *)parsedMarkdown;
- (NSMutableAttributedString *)parsedMarkdownForChat;
- (NSMutableAttributedString *)systemMessageFormat;
- (NSString *)sendingMessage;
- (NCChatMessage *)parent;
Expand Down
45 changes: 31 additions & 14 deletions NextcloudTalk/NCChatMessage.m
Original file line number Diff line number Diff line change
Expand Up @@ -422,22 +422,22 @@ - (NSMutableAttributedString *)parsedMessage
if (!self.message) {
return nil;
}

NSString *originalMessage = self.file.contactName ? self.file.contactName : self.message;
NSString *parsedMessage = originalMessage;
NSError *error = nil;

NSRegularExpression *parameterRegex = [NSRegularExpression regularExpressionWithPattern:@"\\{([^}]+)\\}" options:NSRegularExpressionCaseInsensitive error:&error];
NSArray *matches = [parameterRegex matchesInString:originalMessage
options:0
range:NSMakeRange(0, [originalMessage length])];

// Find message parameters
NSMutableArray *parameters = [NSMutableArray new];
for (NSTextCheckingResult *match in matches) {
NSString* parameter = [originalMessage substringWithRange:match.range];
NSString *parameterKey = [[parameter stringByReplacingOccurrencesOfString:@"{" withString:@""]
stringByReplacingOccurrencesOfString:@"}" withString:@""];
stringByReplacingOccurrencesOfString:@"}" withString:@""];
NSDictionary *parameterDict = [[self messageParameters] objectForKey:parameterKey];
if (parameterDict) {
NCMessageParameter *messageParameter = [[NCMessageParameter alloc] initWithDictionary:parameterDict] ;
Expand All @@ -460,19 +460,19 @@ - (NSMutableAttributedString *)parsedMessage
[parameters addObject:messageParameter];
}
}

UIColor *defaultColor = [NCAppBranding chatForegroundColor];
UIColor *highlightedColor = [NCAppBranding elementColor];

NSMutableAttributedString *attributedMessage = [[NSMutableAttributedString alloc] initWithString:parsedMessage];
[attributedMessage addAttribute:NSForegroundColorAttributeName value:defaultColor range:NSMakeRange(0,parsedMessage.length)];
[attributedMessage addAttribute:NSForegroundColorAttributeName value:defaultColor range:NSMakeRange(0, parsedMessage.length)];

if (self.isEmojiMessage) {
[attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:36.0f] range:NSMakeRange(0,parsedMessage.length)];
[attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:36.0f] range:NSMakeRange(0, parsedMessage.length)];
} else {
[attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16.0f] range:NSMakeRange(0,parsedMessage.length)];
[attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16.0f] range:NSMakeRange(0, parsedMessage.length)];
}

for (NCMessageParameter *param in parameters) {
//Set color for mentions
if ([param.type isEqualToString:@"user"] || [param.type isEqualToString:@"guest"] ||
Expand All @@ -490,18 +490,35 @@ - (NSMutableAttributedString *)parsedMessage
}
}
}

return attributedMessage;
}

- (NSMutableAttributedString *)parsedMessageForChat
- (NSMutableAttributedString *)parsedMarkdown
{
NSMutableAttributedString *parsedMessage = self.parsedMessage;

if (!parsedMessage) {
return nil;
}

return [SwiftMarkdownObjCBridge parseMarkdownWithMarkdownString:parsedMessage];
}

- (NSMutableAttributedString *)parsedMarkdownForChat
{
// In some circumstances we want/need to hide the message in the chat, but still want to show it in other parts like the conversation list
if ([self getDeckCardUrlForReferenceProvider]) {
return nil;
}

return self.parsedMessage;
NSMutableAttributedString *parsedMessage = self.parsedMessage;

if (!parsedMessage) {
return nil;
}

return [SwiftMarkdownObjCBridge parseMarkdownWithMarkdownString:parsedMessage];
}

- (NSMutableAttributedString *)systemMessageFormat
Expand Down
20 changes: 16 additions & 4 deletions NextcloudTalk/NCChatViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,7 @@ - (void)didPressForward:(NCChatMessage *)message {
if (message.isObjectShare) {
shareViewController = [[ShareViewController alloc] initToForwardObjectShareMessage:message fromChatViewController:self];
} else {
shareViewController = [[ShareViewController alloc] initToForwardMessage:message.parsedMessageForChat.string fromChatViewController:self];
shareViewController = [[ShareViewController alloc] initToForwardMessage:message.parsedMessage.string fromChatViewController:self];
}
shareViewController.delegate = self;
NCNavigationController *forwardMessageNC = [[NCNavigationController alloc] initWithRootViewController:shareViewController];
Expand All @@ -1641,7 +1641,7 @@ - (void)didPressResend:(NCChatMessage *)message {

- (void)didPressCopy:(NCChatMessage *)message {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = message.parsedMessageForChat.string;
pasteboard.string = message.parsedMessage.string;
[self.view makeToast:NSLocalizedString(@"Message copied", nil) duration:1.5 position:CSToastPositionCenter];
}

Expand Down Expand Up @@ -4049,14 +4049,26 @@ - (CGFloat)getCellHeightForMessage:(NCChatMessage *)message withWidth:(CGFloat)w
}

// Chat messages
NSMutableAttributedString *messageString = message.parsedMessageForChat;
NSMutableAttributedString *messageString = message.parsedMarkdownForChat;
width -= (message.isSystemMessage)? 80.0 : 30.0; // 4*right(10) + dateLabel(40) : 3*right(10)
if (message.poll) {
messageString = [[NSMutableAttributedString alloc] initWithString:message.poll.name];
[messageString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:[ObjectShareMessageTableViewCell defaultFontSize]] range:NSMakeRange(0,messageString.length)];
width -= kObjectShareMessageCellObjectTypeImageSize + 25; // 2*right(10) + left(5)
}
CGRect bodyBounds = [messageString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:NULL];

// Calculate the height of the message. "boundingRectWithSize" does not work correctly with markdown, so we use this
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:message.parsedMarkdownForChat];
CGRect targetBounding = CGRectMake(0, 0, width, CGFLOAT_MAX);
NSTextContainer *container = [[NSTextContainer alloc] initWithSize:targetBounding.size];
container.lineFragmentPadding = 0;

NSLayoutManager *manager = [[NSLayoutManager alloc] init];
[manager addTextContainer:container];
[textStorage addLayoutManager:manager];

[manager glyphRangeForBoundingRect:targetBounding inTextContainer:container];
CGRect bodyBounds = [manager usedRectForTextContainer:container];

CGFloat height = kChatCellAvatarHeight;
height += ceil(CGRectGetHeight(bodyBounds));
Expand Down
Loading