Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -68,7 +76,7 @@ struct ActivityLogDetailsView: View {
Label(Strings.restore, systemImage: "arrow.counterclockwise")
.fontWeight(.medium)
.opacity(isLoadingRewindStatus ? 0 : 1)

if isLoadingRewindStatus {
ProgressView()
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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..<attrEndIndex].link = url
}
}

return attributedString
}

/// Returns the appropriate GridiconType for this activity, if available
var gridiconType: GridiconType? {
Expand Down