diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift index fc44876ff..144288391 100644 --- a/Linphone/GeneratedGit.swift +++ b/Linphone/GeneratedGit.swift @@ -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" } diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 51c64d8cb..ba1ee21e1 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -4,6 +4,16 @@ CFBundleURLTypes + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + linphone-mention + + CFBundleTypeRole Editor diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 5c489df4a..9954be02b 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -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 diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift index a0ed17ce1..03f5bbb39 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -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 { diff --git a/Linphone/Utils/URIHandler.swift b/Linphone/Utils/URIHandler.swift index 8710d3b21..3bc2153d0 100644 --- a/Linphone/Utils/URIHandler.swift +++ b/Linphone/Utils/URIHandler.swift @@ -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) { diff --git a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6e08b3a1d..b49c23f54 100644 --- a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,7 +123,7 @@ "location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git", "state" : { "branch" : "alpha", - "revision" : "81cb4712da29fffda8d7fb673685f6add05cf325" + "revision" : "9e5a562e218530b2a38b858b5183446ceb1d6e35" } }, {