Skip to content
Open
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
26 changes: 26 additions & 0 deletions Sources/Utilities/KMMarkdownDetector.swift
Original file line number Diff line number Diff line change
@@ -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
}
}


48 changes: 48 additions & 0 deletions Sources/Utilities/KMMarkdownParser.swift
Original file line number Diff line number Diff line change
@@ -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
]
)
}
}
}
39 changes: 39 additions & 0 deletions Sources/Utilities/KMMarkdownRenderer.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
23 changes: 20 additions & 3 deletions Sources/Views/KMChatFriendMessageCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,19 +284,36 @@ 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)
}

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
Expand Down
96 changes: 88 additions & 8 deletions Sources/Views/KMChatMessageBaseCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,46 @@ open class KMChatMessageCell: KMChatChatBaseCell<KMChatMessageViewModel> {

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) ||
Comment on lines +327 to +332
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Height calculation in messageHeight() assumes markdown for all messages, but setMessageText() only renders it for received messages, causing layout bugs for self-sent markdown messages.
Severity: MEDIUM

Suggested Fix

The height calculation logic in messageHeight() should be aligned with the display logic in setMessageText(). It should check if the message is self-sent (viewModel.isMyMessage) and only apply markdown sizing for received messages, matching the rendering behavior.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: Sources/Views/KMChatMessageBaseCell.swift#L327-L332

Potential issue: There is an inconsistency between message height calculation and
rendering for self-sent messages. The `messageHeight()` method calculates the cell
height assuming markdown is applied to all messages, including those sent by the user.
However, the `setMessageText()` method only renders markdown for received messages. This
causes a mismatch for self-sent messages containing markdown syntax, leading to
incorrect UI rendering, such as text clipping or improper bubble sizing.

Did we get this right? 👍 / 👎 to inform future reviews.

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
Expand Down Expand Up @@ -564,15 +595,64 @@ open class KMChatMessageCell: KMChatChatBaseCell<KMChatMessageViewModel> {
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
}

}
Loading