mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Replies now display mentions with @displayname (#1957)
* reply with mentions * fixed a bug and wrote a test for it * error message
This commit is contained in:
@@ -56,8 +56,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
|
||||
let mutableAttributedString = NSMutableAttributedString(string: string)
|
||||
addLinks(mutableAttributedString)
|
||||
addAllUsersMention(mutableAttributedString)
|
||||
addLinksAndMentions(mutableAttributedString)
|
||||
detectPermalinks(mutableAttributedString)
|
||||
removeLinkColors(mutableAttributedString)
|
||||
|
||||
@@ -110,8 +109,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||
removeDefaultForegroundColor(mutableAttributedString)
|
||||
addLinks(mutableAttributedString)
|
||||
addAllUsersMention(mutableAttributedString)
|
||||
addLinksAndMentions(mutableAttributedString)
|
||||
replaceMarkedBlockquotes(mutableAttributedString)
|
||||
replaceMarkedCodeBlocks(mutableAttributedString)
|
||||
detectPermalinks(mutableAttributedString)
|
||||
@@ -150,7 +148,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) {
|
||||
private func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) {
|
||||
attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||
if let value = value as? UIColor,
|
||||
value == temporaryCodeBlockMarkingColor {
|
||||
@@ -175,7 +173,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func addLinks(_ attributedString: NSMutableAttributedString) {
|
||||
private func addLinksAndMentions(_ attributedString: NSMutableAttributedString) {
|
||||
let string = attributedString.string
|
||||
|
||||
var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: [])
|
||||
@@ -187,11 +185,16 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
|
||||
matches.append(contentsOf: linkMatches)
|
||||
|
||||
let allUserMentionsMatches = MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: [])
|
||||
matches.append(contentsOf: allUserMentionsMatches)
|
||||
|
||||
let allUsersMentionsCount = allUserMentionsMatches.count
|
||||
|
||||
guard matches.count > 0 else {
|
||||
return
|
||||
}
|
||||
// Sort the links by length so the longest one always takes priority
|
||||
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
|
||||
matches.sorted { $0.range.length > $1.range.length }.enumerated().forEach { offset, match in
|
||||
guard let matchRange = Range(match.range, in: string) else {
|
||||
return
|
||||
}
|
||||
@@ -208,22 +211,18 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var link = String(string[matchRange])
|
||||
|
||||
if linkMatches.contains(match), !link.contains("://") {
|
||||
link.insert(contentsOf: "https://", at: link.startIndex)
|
||||
}
|
||||
|
||||
if let url = URL(string: link) {
|
||||
attributedString.addAttribute(.link, value: url, range: match.range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAllUsersMention(_ attributedString: NSMutableAttributedString) {
|
||||
MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).forEach { match in
|
||||
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) as? URL == nil {
|
||||
if offset > matches.count - allUsersMentionsCount - 1 {
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||
} else {
|
||||
var link = String(string[matchRange])
|
||||
|
||||
if linkMatches.contains(match), !link.contains("://") {
|
||||
link.insert(contentsOf: "https://", at: link.startIndex)
|
||||
}
|
||||
|
||||
if let url = URL(string: link) {
|
||||
attributedString.addAttribute(.link, value: url, range: match.range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font]
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font, .MatrixAllUsersMention: true]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
|
||||
@@ -67,7 +67,12 @@ struct MessageText: UIViewRepresentable {
|
||||
let textView = MessageTextView(usingTextLayoutManager: false)
|
||||
textView.roomContext = viewModel
|
||||
textView.updateClosure = {
|
||||
attributedString = AttributedString(textView.attributedText)
|
||||
do {
|
||||
attributedString = try AttributedString(textView.attributedText, including: \.elementX)
|
||||
} catch {
|
||||
MXLog.error("[MessageText] Failed to update attributedString: \(error)]")
|
||||
return
|
||||
}
|
||||
}
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
@@ -87,14 +92,16 @@ struct MessageText: UIViewRepresentable {
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.layoutManager.usesFontLeading = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.attributedText = NSAttributedString(attributedString)
|
||||
if let attributedText = try? NSAttributedString(attributedString, including: \.elementX) {
|
||||
textView.attributedText = attributedText
|
||||
}
|
||||
textView.delegate = context.coordinator
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: MessageTextView, context: Context) {
|
||||
let newAttributedText = NSAttributedString(attributedString)
|
||||
if uiView.attributedText != newAttributedText {
|
||||
if let newAttributedText = try? NSAttributedString(attributedString, including: \.elementX),
|
||||
uiView.attributedText != newAttributedText {
|
||||
uiView.flushPills()
|
||||
uiView.attributedText = newAttributedText
|
||||
}
|
||||
|
||||
@@ -126,8 +126,26 @@ struct TimelineReplyView: View {
|
||||
/// and render with a consistent font size. This conversion is done to avoid
|
||||
/// showing markdown characters in the preview for messages with formatting.
|
||||
var messagePreview: String {
|
||||
guard let formattedBody else { return plainBody }
|
||||
return String(formattedBody.characters)
|
||||
guard let formattedBody,
|
||||
let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else {
|
||||
return plainBody
|
||||
}
|
||||
|
||||
let range = NSRange(location: 0, length: attributedString.length)
|
||||
attributedString.enumerateAttributes(in: range) { attributes, range, _ in
|
||||
if let userID = attributes[.MatrixUserID] as? String {
|
||||
if let displayName = context.viewState.members[userID]?.displayName {
|
||||
attributedString.replaceCharacters(in: range, with: "@\(displayName)")
|
||||
} else {
|
||||
attributedString.replaceCharacters(in: range, with: userID)
|
||||
}
|
||||
}
|
||||
|
||||
if attributes[.MatrixAllUsersMention] as? Bool == true {
|
||||
attributedString.replaceCharacters(in: range, with: PillConstants.atRoom)
|
||||
}
|
||||
}
|
||||
return attributedString.string
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -187,6 +205,18 @@ struct TimelineReplyView: View {
|
||||
struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static let attributedStringWithMention = {
|
||||
var attributedString = AttributedString("To be replaced")
|
||||
attributedString.userID = "@alice:matrix.org"
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
static let attributedStringWithAtRoomMention = {
|
||||
var attributedString = AttributedString("to be replaced")
|
||||
attributedString.allUsersMention = true
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
static var previewItems: [TimelineReplyView] {
|
||||
let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png")
|
||||
|
||||
@@ -204,7 +234,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
contentType: .emote(.init(body: "says hello")))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bot"),
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
contentType: .notice(.init(body: "Hello world")))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
@@ -244,7 +274,13 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
duration: 0,
|
||||
waveform: nil,
|
||||
source: nil,
|
||||
contentType: nil))))
|
||||
contentType: nil)))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
contentType: .notice(.init(body: "", formattedBody: attributedStringWithMention)))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
contentType: .notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention))))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user