diff --git a/Sources/Utilities/KMMarkdownDetector.swift b/Sources/Utilities/KMMarkdownDetector.swift new file mode 100644 index 00000000..f329a964 --- /dev/null +++ b/Sources/Utilities/KMMarkdownDetector.swift @@ -0,0 +1,26 @@ +// +// KMMarkdownDetector.swift +// Pods +// +// Created by Kommunicate on 18/02/26. +// + +import UIKit + +enum KMMarkdownDetector { + static func containsMarkdown(_ text: String) -> Bool { + let pattern = #"(\*\*|__|\*|_|~~|`|\[.*?\]\(.*?\)|^>\s|^\s*[-*+]\s)"# + + guard let regex = try? NSRegularExpression( + pattern: pattern, + options: [.anchorsMatchLines] + ) else { + return false + } + + let range = NSRange(location: 0, length: text.utf16.count) + return regex.firstMatch(in: text, options: [], range: range) != nil + } +} + + diff --git a/Sources/Utilities/KMMarkdownParser.swift b/Sources/Utilities/KMMarkdownParser.swift new file mode 100644 index 00000000..52a5d007 --- /dev/null +++ b/Sources/Utilities/KMMarkdownParser.swift @@ -0,0 +1,48 @@ +// +// KMMarkdownParser.swift +// Pods +// +// Created by Kommunicate on 18/02/26. +// + +import UIKit + +struct KMMarkdownParser { + static func attributedString( + from text: String, + font: UIFont, + textColor: UIColor + ) -> NSAttributedString { + if #available(iOS 15.0, *) { + do { + var attributed = try AttributedString( + markdown: text, + options: AttributedString.MarkdownParsingOptions( + interpretedSyntax: .inlineOnlyPreservingWhitespace + ) + ) + + attributed.font = font + attributed.foregroundColor = textColor + + return NSAttributedString(attributed) + } catch { + return NSAttributedString( + string: text, + attributes: [ + .font: font, + .foregroundColor: textColor + ] + ) + } + } else { + return NSAttributedString( + string: text, + attributes: [ + .font: font, + .foregroundColor: textColor + ] + ) + } + } +} diff --git a/Sources/Utilities/KMMarkdownRenderer.swift b/Sources/Utilities/KMMarkdownRenderer.swift new file mode 100644 index 00000000..40a8e2b1 --- /dev/null +++ b/Sources/Utilities/KMMarkdownRenderer.swift @@ -0,0 +1,39 @@ +// +// KMMarkdownRenderer.swift +// Pods +// +// Created by Kommunicate on 18/02/26. +// + +import UIKit + +enum KMMarkdownRenderer { + static func attributedString( + from text: String, + baseFont: UIFont, + baseAttributes: [NSAttributedString.Key: Any] + ) -> NSAttributedString { + if #available(iOS 15.0, *) { + do { + var attributed = try AttributedString( + markdown: text, + options: .init( + interpretedSyntax: .inlineOnlyPreservingWhitespace + ) + ) + attributed.font = baseFont + return NSAttributedString(attributed) + } catch { + return NSAttributedString( + string: text, + attributes: baseAttributes + ) + } + } else { + return NSAttributedString( + string: text, + attributes: baseAttributes + ) + } + } +} diff --git a/Sources/Views/KMChatFriendMessageCell.swift b/Sources/Views/KMChatFriendMessageCell.swift index b2792baf..a029727c 100644 --- a/Sources/Views/KMChatFriendMessageCell.swift +++ b/Sources/Views/KMChatFriendMessageCell.swift @@ -284,11 +284,25 @@ open class KMChatFriendMessageCell: KMChatMessageCell { let replyId = metadata[AL_MESSAGE_REPLY_KEY] as? String, let actualMessage = getMessageFor(key: replyId) else { return } + showReplyView(true) + + if let replyText = actualMessage.message { + replyMessageLabel.attributedText = KMMarkdownParser.attributedString( + from: replyText, + font: replyMessageLabel.font, + textColor: replyMessageLabel.textColor + ) + } + if actualMessage.messageType == .text || actualMessage.messageType == .html { - previewImageView.constraint(withIdentifier: ConstraintIdentifier.PreviewImage.height)?.constant = 0 + previewImageView.constraint( + withIdentifier: ConstraintIdentifier.PreviewImage.height + )?.constant = 0 } else { - previewImageView.constraint(withIdentifier: ConstraintIdentifier.PreviewImage.width)?.constant = Padding.PreviewImageView.width + previewImageView.constraint( + withIdentifier: ConstraintIdentifier.PreviewImage.width + )?.constant = Padding.PreviewImageView.width } } else { showReplyView(false) @@ -296,7 +310,10 @@ open class KMChatFriendMessageCell: KMChatMessageCell { let placeHolder = UIImage(named: "placeholder", in: Bundle.km, compatibleWith: nil) if let url = viewModel.avatarURL { - let resource = Kingfisher.ImageResource(downloadURL: url, cacheKey: url.absoluteString) + let resource = Kingfisher.ImageResource( + downloadURL: url, + cacheKey: url.absoluteString + ) avatarImageView.kf.setImage(with: resource, placeholder: placeHolder) } else { avatarImageView.image = placeHolder diff --git a/Sources/Views/KMChatMessageBaseCell.swift b/Sources/Views/KMChatMessageBaseCell.swift index 950e9380..e1f8020f 100644 --- a/Sources/Views/KMChatMessageBaseCell.swift +++ b/Sources/Views/KMChatMessageBaseCell.swift @@ -307,15 +307,46 @@ open class KMChatMessageCell: KMChatChatBaseCell { switch viewModel.messageType { case .text, .staticTopMessage: - if let attributedText = viewModel + let mentionAttributed = viewModel .attributedTextWithMentions( defaultAttributes: dummyMessageView.typingAttributes, mentionAttributes: mentionStyle.toAttributes, displayNames: displayNames - ) { - return TextViewSizeCalculator.height(dummyMessageView, attributedText: attributedText, maxWidth: width) + ) + + let markdownAttributed = KMMarkdownRenderer.attributedString( + from: message, + baseFont: font, + baseAttributes: dummyMessageView.typingAttributes + ) + + let finalAttributed: NSAttributedString + if let mentionAttributed = mentionAttributed { + let mutable = NSMutableAttributedString(attributedString: markdownAttributed) + + mentionAttributed.enumerateAttributes( + in: NSRange(location: 0, length: mentionAttributed.length), + options: [] + ) { attributes, range, _ in + if attributes.keys.contains(.foregroundColor) || + attributes.keys.contains(.backgroundColor) || + attributes.keys.contains(.font) { + mutable.addAttributes(attributes, range: range) + } + } + finalAttributed = mutable + } else { + finalAttributed = markdownAttributed } - return TextViewSizeCalculator.height(dummyMessageView, text: message, maxWidth: width) + + dummyAttributedMessageView.font = font + + return TextViewSizeCalculator.height( + dummyAttributedMessageView, + attributedText: finalAttributed, + maxWidth: width + ) + case .html: guard let attributedText = attributedStringFrom(message, for: viewModel.identifier) else { return 0 @@ -564,15 +595,64 @@ open class KMChatMessageCell: KMChatChatBaseCell { viewModel: KMChatMessageViewModel, mentionStyle: Style ) { - if let attributedText = viewModel + guard let message = viewModel.message else { + messageView.text = nil + return + } + + let shouldUseMarkdown = KMMarkdownDetector.containsMarkdown(message) + + if !viewModel.isMyMessage && shouldUseMarkdown { + applyMarkdownText(message: message, mentionStyle: mentionStyle, viewModel: viewModel) + } else { + if let attributedText = viewModel + .attributedTextWithMentions( + defaultAttributes: messageView.typingAttributes, + mentionAttributes: mentionStyle.toAttributes, + displayNames: displayNames + ) { + messageView.attributedText = attributedText + } else { + messageView.text = message + } + } + } + + private func applyMarkdownText( + message: String, + mentionStyle: Style, + viewModel: KMChatMessageViewModel + ) { + let markdownAttributed = KMMarkdownRenderer.attributedString( + from: message, + baseFont: messageView.font ?? UIFont.systemFont(ofSize: 16), + baseAttributes: messageView.typingAttributes + ) + + let mutable = NSMutableAttributedString(attributedString: markdownAttributed) + let styleColor = messageView.textColor ?? UIColor.label + + mutable.addAttribute( + .foregroundColor, + value: styleColor, + range: NSRange(location: 0, length: mutable.length) + ) + + if let mentionAttributed = viewModel .attributedTextWithMentions( defaultAttributes: messageView.typingAttributes, mentionAttributes: mentionStyle.toAttributes, displayNames: displayNames ) { - messageView.attributedText = attributedText - } else { - messageView.text = viewModel.message + mentionAttributed.enumerateAttributes( + in: NSRange(location: 0, length: mentionAttributed.length), + options: [] + ) { attributes, range, _ in + mutable.addAttributes(attributes, range: range) + } } + + messageView.attributedText = mutable } + }