Add mentions to DynamicLinkText in ChatBubbleView

This commit is contained in:
Benoit Martins 2026-01-05 12:26:58 +01:00
parent 8d5c0ce79b
commit 990d2f36af
6 changed files with 101 additions and 25 deletions

View file

@ -2,6 +2,6 @@ import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "61931138b"
public static let commit = "8d5c0ce79"
public static let tag = "6.1.0-alpha"
}

View file

@ -4,6 +4,16 @@
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>linphone-mention</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>

View file

@ -179,7 +179,7 @@ struct ChatBubbleView: View {
}
if !eventLogMessage.message.text.isEmpty {
DynamicLinkText(text: eventLogMessage.message.text)
DynamicLinkText(text: eventLogMessage.message.text, participantConversationModel: conversationViewModel.participantConversationModel)
} else if eventLogMessage.message.isRetracted {
Text(eventLogMessage.message.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label")
.italic()
@ -946,42 +946,81 @@ struct ChatBubbleView: View {
struct DynamicLinkText: View {
let text: String
let participantConversationModel: [ContactAvatarModel]
var body: some View {
let components = text.components(separatedBy: " ")
Text(makeAttributedString(from: components))
Text(makeAttributedString(from: text))
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
.lineLimit(nil)
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
}
// Function to create an AttributedString with clickable links
private func makeAttributedString(from components: [String]) -> AttributedString {
var result = AttributedString("")
for (index, component) in components.enumerated() {
if let url = URL(string: component.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""),
url.scheme == "http" || url.scheme == "https" {
var attributedText = AttributedString(component)
attributedText.link = url
attributedText.foregroundColor = .blue
attributedText.underlineStyle = .single
result.append(attributedText)
private func makeAttributedString(from text: String) -> AttributedString {
var result = AttributedString()
var currentWord = ""
for char in text {
if char == " " || char == "\n" {
appendWord(currentWord, to: &result)
result.append(AttributedString(String(char)))
currentWord = ""
} else {
result.append(AttributedString(component))
}
// Add space between words except for the last one
if index < components.count - 1 {
result.append(AttributedString(" "))
currentWord.append(char)
}
}
appendWord(currentWord, to: &result)
return result
}
private func appendWord(_ word: String, to result: inout AttributedString) {
guard !word.isEmpty else { return }
// URL
if
let encoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encoded),
["http", "https"].contains(url.scheme)
{
var link = AttributedString(word)
link.link = url
link.foregroundColor = .blue
link.underlineStyle = .single
result.append(link)
return
}
// Mention
if isMention(word),
let participant = participantConversationModel.first(where: {($0.address.dropFirst(4).split(separator: "@").first ?? "") == word.dropFirst()}),
let mentionURL = URL(string: "linphone-mention://\(participant.address)")
{
var mention = AttributedString("@" + participant.name)
mention.link = mentionURL
mention.foregroundColor = Color.orangeMain500
mention.font = .system(size: 14, weight: .semibold)
result.append(mention)
return
}
// Text
var normal = AttributedString(word)
normal.foregroundColor = Color.grayMain2c700
result.append(normal)
}
private func isMention(_ word: String) -> Bool {
guard word.first == "@", word.count > 1 else { return false }
let username = word.dropFirst()
return username.allSatisfy {
$0.isLetter || $0.isNumber || $0 == "." || $0 == "_"
}
}
}
enum URLType {
case name(String) // local file name of gif
case url(URL) // remote url

View file

@ -368,7 +368,6 @@ struct ConversationInfoFragment: View {
let friendIndex = contactsManager.avatarListModel.first(
where: {$0.addresses.contains(where: {$0 == addressConv})})
SharedMainViewModel.shared.displayedCall = nil
SharedMainViewModel.shared.changeIndexView(indexViewInt: 0)
if friendIndex != nil {
@ -381,6 +380,8 @@ struct ConversationInfoFragment: View {
isShowEditContactFragmentAddress = String(participantConversationModel.address.dropFirst(4))
}
}
SharedMainViewModel.shared.displayedConversation = nil
},
label: {
HStack {

View file

@ -20,6 +20,7 @@
import Foundation
import linphonesw
import Combine
import SwiftUI
class URIHandler {
@ -28,6 +29,7 @@ class URIHandler {
private static let secureCallSchemes = ["sips", "sips-linphone", "linphone-sips"]
private static let configurationSchemes = ["linphone-config"]
private static let sharedExtensionSchemes = ["linphone-message"]
private static let mentionSchemes = ["linphone-mention"]
private static var uriHandlerCoreDelegate: CoreDelegateStub?
@ -66,6 +68,8 @@ class URIHandler {
initiateConfiguration(url: url)
} else if sharedExtensionSchemes.contains(scheme) {
processReceivedFiles(url: url)
} else if mentionSchemes.contains(scheme) {
openContact(url: url)
} else if scheme == SingleSignOnManager.shared.ssoRedirectUri.scheme {
continueSSO(url: url)
} else {
@ -131,6 +135,28 @@ class URIHandler {
SharedMainViewModel.shared.changeIndexView(indexViewInt: 2)
}
private static func openContact(url: URL) {
Log.info("[URIHandler] open contact from URL: \(url.resourceSpecifier)")
var urlString = url.resourceSpecifier
if urlString.starts(with: "//") {
urlString = String(urlString.dropFirst(2))
}
print("[URIHandler] urlStringurlString : \(urlString)")
let friendIndex = ContactsManager.shared.avatarListModel.first(
where: {$0.addresses.contains(where: {$0 == urlString})})
if friendIndex != nil {
SharedMainViewModel.shared.displayedConversation = nil
SharedMainViewModel.shared.changeIndexView(indexViewInt: 0)
withAnimation {
SharedMainViewModel.shared.displayedFriend = friendIndex
}
}
}
private static func continueSSO(url: URL) {
if let authorizationFlow = SingleSignOnManager.shared.currentAuthorizationFlow,
authorizationFlow.resumeExternalUserAgentFlow(with: url) {

View file

@ -123,7 +123,7 @@
"location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git",
"state" : {
"branch" : "alpha",
"revision" : "81cb4712da29fffda8d7fb673685f6add05cf325"
"revision" : "9e5a562e218530b2a38b858b5183446ceb1d6e35"
}
},
{