diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityFormattableContentView.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityFormattableContentView.swift new file mode 100644 index 000000000000..3f07a6831291 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityFormattableContentView.swift @@ -0,0 +1,69 @@ +import SwiftUI +import UIKit +import WordPressUI +import FormattableContentKit + +struct ActivityFormattableContentView: UIViewRepresentable { + let formattableActivity: FormattableActivity + let blog: Blog + + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.isEditable = false + textView.isScrollEnabled = false + textView.backgroundColor = .clear + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.linkTextAttributes = [ + .foregroundColor: UIAppColor.primary + ] + textView.delegate = context.coordinator + return textView + } + + func updateUIView(_ textView: UITextView, context: Context) { + let styles = ActivityContentStyles() + let formattedContent = formattableActivity.formattedContent(using: styles) + textView.attributedText = formattedContent + } + + func makeCoordinator() -> Coordinator { + Coordinator(formattableActivity: formattableActivity, blog: blog) + } + + class Coordinator: NSObject, UITextViewDelegate { + let formattableActivity: FormattableActivity + let blog: Blog + + init(formattableActivity: FormattableActivity, blog: Blog) { + self.formattableActivity = formattableActivity + self.blog = blog + super.init() + } + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + guard interaction == .invokeDefaultAction else { + return false + } + + // Get the top view controller to create content coordinator + guard let viewController = UIViewController.topViewController else { + return false + } + + let contentCoordinator = DefaultContentCoordinator( + controller: viewController, + context: ContextManager.shared.mainContext + ) + + let router = ActivityContentRouter( + activity: formattableActivity, + coordinator: contentCoordinator + ) + + router.routeTo(URL) + + return false + } + } +} diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift index 0bee63d08424..4b119b5dfeee 100644 --- a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift @@ -3,18 +3,26 @@ import WordPressKit import WordPressUI import WordPressShared import Gridicons +import UIKit struct ActivityLogDetailsView: View { let activity: Activity let blog: Blog - @Environment(\.dismiss) var dismiss @State private var isLoadingRewindStatus = false + private let formattableActivity: FormattableActivity + + init(activity: Activity, blog: Blog) { + self.activity = activity + self.blog = blog + self.formattableActivity = FormattableActivity(with: activity) + } + var body: some View { ScrollView { VStack(spacing: 24) { - ActivityHeaderView(activity: activity) + ActivityHeaderView(activity: activity, blog: blog, formattableActivity: formattableActivity) if activity.isRewindable { restoreSiteCard } @@ -68,7 +76,7 @@ struct ActivityLogDetailsView: View { Label(Strings.restore, systemImage: "arrow.counterclockwise") .fontWeight(.medium) .opacity(isLoadingRewindStatus ? 0 : 1) - + if isLoadingRewindStatus { ProgressView() } @@ -95,6 +103,8 @@ struct ActivityLogDetailsView: View { private struct ActivityHeaderView: View { let activity: Activity + let blog: Blog + let formattableActivity: FormattableActivity var body: some View { VStack(alignment: .leading, spacing: 16) { @@ -122,18 +132,10 @@ private struct ActivityHeaderView: View { // Activity details if !activity.text.isEmpty { - if let formattedContent = activity.formattedContent { - Text(formattedContent) - .font(.subheadline) - .foregroundStyle(.secondary) - .lineLimit(3) - .tint(Color.accentColor) - } else { - Text(activity.text) - .font(.subheadline) - .foregroundStyle(.secondary) - .lineLimit(3) - } + ActivityFormattableContentView( + formattableActivity: formattableActivity, + blog: blog + ) } // Date and time diff --git a/WordPress/Classes/ViewRelated/Activity/Extensions/Activity+Extensions.swift b/WordPress/Classes/ViewRelated/Activity/Extensions/Activity+Extensions.swift index 39359bc00bbe..994c522abb86 100644 --- a/WordPress/Classes/ViewRelated/Activity/Extensions/Activity+Extensions.swift +++ b/WordPress/Classes/ViewRelated/Activity/Extensions/Activity+Extensions.swift @@ -4,53 +4,6 @@ import DesignSystem import WordPressKit extension Activity { - /// Returns an AttributedString with clickable links based on content ranges - var formattedContent: AttributedString? { - guard let content, - let text = content["text"] as? String, - !text.isEmpty else { - return nil - } - - var attributedString = AttributedString(text) - - // Apply links from ranges if available - if let ranges = content["ranges"] as? [[String: Any]] { - for range in ranges { - guard let indices = range["indices"] as? [NSNumber], - indices.count == 2, - let urlString = range["url"] as? String, - let url = URL(string: urlString) else { - continue - } - - let startIndex = indices[0].intValue - let endIndex = indices[1].intValue - - // Convert string indices to AttributedString indices - guard startIndex >= 0, - endIndex <= text.count, - startIndex < endIndex else { - continue - } - - // Convert character indices to AttributedString.Index - let stringStartIndex = text.index(text.startIndex, offsetBy: startIndex) - let stringEndIndex = text.index(text.startIndex, offsetBy: endIndex) - - // Find corresponding indices in AttributedString - guard let attrStartIndex = AttributedString.Index(stringStartIndex, within: attributedString), - let attrEndIndex = AttributedString.Index(stringEndIndex, within: attributedString) else { - continue - } - - // Apply the link attribute to the exact range - attributedString[attrStartIndex..