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
15 changes: 9 additions & 6 deletions V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public class HTMLToMarkdownConverter {
result += "*\(content)*"

case "a":
let text = try convertElement(childElement)
// Get raw text without escaping for links
let text = try childElement.text()
Comment on lines +91 to +92
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing from convertElement(childElement) to childElement.text() prevents nested formatting in link text from being preserved. For example, <a href="..."><strong>Bold Link</strong></a> would previously render as [**Bold Link**](...) but now renders as [Bold Link](...). If this behavior change is intentional to fix URL escaping issues, consider documenting it or adding a test case to verify the expected behavior for links with nested formatting.

Copilot uses AI. Check for mistakes.
if let href = try? childElement.attr("href") {
result += "[\(text)](\(href))"
} else {
Expand Down Expand Up @@ -213,14 +214,16 @@ public class HTMLToMarkdownConverter {

/// Escape special Markdown characters
private func escapeMarkdown(_ text: String) -> String {
// Only escape if not already in a code context
// This is a simplified version - a full implementation would track context
// Only escape characters that would cause markdown parsing issues
// Don't escape common characters like . and - as they rarely cause problems
// and escaping them breaks URLs and normal text readability
var escaped = text

// Don't escape inside code blocks (this is simplified)
// Don't escape inside code blocks
if !text.contains("```") && !text.contains("`") {
// Escape special Markdown characters
let charactersToEscape = ["\\", "*", "_", "[", "]", "(", ")", "#", "+", "-", ".", "!"]
// Only escape the most problematic markdown characters
// Avoid escaping . and - as they appear frequently in URLs and text
let charactersToEscape = ["\\", "*", "_", "[", "]"]
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reduced character escaping list no longer includes . and - characters, which is the intended behavior fix. However, there are no tests covering URLs with these characters (e.g., https://n.wtf/ or domains with hyphens). Consider adding test cases to HTMLToMarkdownConverterTests.swift that verify URLs containing dots and hyphens are not escaped and remain functional.

Copilot uses AI. Check for mistakes.
for char in charactersToEscape {
escaped = escaped.replacingOccurrences(of: char, with: "\\\(char)")
}
Expand Down
18 changes: 9 additions & 9 deletions V2er/Sources/RichView/Models/RenderStylesheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ public struct TextStyle: Equatable {
public var color: Color

public init(
fontSize: CGFloat = 16,
fontSize: CGFloat = 17,
fontWeight: Font.Weight = .regular,
lineSpacing: CGFloat = 4,
paragraphSpacing: CGFloat = 8,
lineSpacing: CGFloat = 5,
paragraphSpacing: CGFloat = 10,
color: Color = .primary
) {
self.fontSize = fontSize
Expand Down Expand Up @@ -264,10 +264,10 @@ extension RenderStylesheet {
public static let `default`: RenderStylesheet = {
RenderStylesheet(
body: TextStyle(
fontSize: 16,
fontSize: 17,
fontWeight: .regular,
lineSpacing: 4,
paragraphSpacing: 8,
lineSpacing: 5,
paragraphSpacing: 10,
color: .primary
),
heading: HeadingStyle(
Expand Down Expand Up @@ -343,10 +343,10 @@ extension RenderStylesheet {
public static let compact: RenderStylesheet = {
RenderStylesheet(
body: TextStyle(
fontSize: 14,
fontSize: 15,
fontWeight: .regular,
lineSpacing: 2,
paragraphSpacing: 6,
lineSpacing: 4,
paragraphSpacing: 8,
color: .primary
),
heading: HeadingStyle(
Expand Down
10 changes: 4 additions & 6 deletions V2er/Sources/RichView/Views/RichContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,12 @@ public struct RichContentView: View {
} else if let error = error {
ErrorView(error: error)
} else if !contentElements.isEmpty {
ScrollView {
VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) {
ForEach(contentElements) { element in
renderElement(element)
}
VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) {
ForEach(contentElements) { element in
renderElement(element)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text("No content")
.foregroundColor(.secondary)
Expand Down
56 changes: 28 additions & 28 deletions V2er/View/Feed/FilterMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,40 @@ struct FilterMenuView: View {
onDismiss()
}

// Menu content - positioned below navbar
// Menu content - positioned right below navbar
VStack(spacing: 0) {
HStack {
Spacer()
ScrollView {
VStack(spacing: 4) {
ForEach(Tab.allTabs, id: \.self) { tab in
let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn()
TabFilterMenuItem(
tab: tab,
isSelected: tab == selectedTab,
needsLogin: tabNeedsLogin
) {
// Soft haptic feedback
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()

if tabNeedsLogin {
Toast.show("登录后才能查看「\(tab.displayName())」下的内容")
} else {
onTabSelected(tab)
}
// Minimal spacing - just clear the safe area
Spacer()
.frame(height: topSafeAreaInset().top)

ScrollView {
VStack(spacing: 4) {
ForEach(Tab.allTabs, id: \.self) { tab in
let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn()
TabFilterMenuItem(
tab: tab,
isSelected: tab == selectedTab,
needsLogin: tabNeedsLogin
) {
// Soft haptic feedback
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()

if tabNeedsLogin {
Toast.show("登录后才能查看「\(tab.displayName())」下的内容")
} else {
onTabSelected(tab)
}
}
}
.padding(.vertical, 8)
}
.frame(width: 200)
.background(Color.itemBg)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4)
.frame(maxHeight: 450)
Spacer()
.padding(.vertical, 8)
}
.frame(width: 200)
.background(Color.itemBg)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4)
.frame(maxHeight: 450)
.transition(.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .move(edge: .top).combined(with: .opacity)
Expand Down
8 changes: 7 additions & 1 deletion V2er/View/FeedDetail/NewsContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ struct NewsContentView: View {
VStack(spacing: 0) {
Divider()

RichView(htmlContent: contentInfo?.html ?? "")
RichContentView(htmlContent: contentInfo?.html ?? "")
.configuration(configurationForAppearance())
.onLinkTapped { url in
handleLinkTap(url)
}
.onImageTapped { url in
// Open image in SafariView for now
openInSafari(url)
}
.onRenderCompleted { metadata in
// Mark as rendered after content is ready
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
Expand All @@ -40,6 +44,8 @@ struct NewsContentView: View {
print("Render error: \(error)")
self.rendered = true
}
.padding(.horizontal, 12)
.padding(.vertical, 8)

Divider()
}
Expand Down
25 changes: 22 additions & 3 deletions V2er/View/FeedDetail/ReplyItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct ReplyItemView: View {
@Environment(\.colorScheme) var colorScheme
@State private var showingSafari = false
@State private var safariURL: URL?
@State private var navigateToUser: String? = nil

var body: some View {
HStack(alignment: .top) {
Expand Down Expand Up @@ -50,14 +51,16 @@ struct ReplyItemView: View {
.foregroundColor(info.hadThanked ? .red : .secondaryText)
}

RichView(htmlContent: info.content)
RichContentView(htmlContent: info.content)
.configuration(compactConfigurationForAppearance())
.onLinkTapped { url in
handleLinkTap(url)
}
.onImageTapped { url in
openInSafari(url)
}
.onMentionTapped { username in
print("Navigate to mentioned user: @\(username)")
// TODO: Implement proper navigation to UserDetailPage
navigateToUser = username
}

Text("\(info.floor)楼")
Expand All @@ -73,6 +76,22 @@ struct ReplyItemView: View {
SafariView(url: url)
}
}
.background(
NavigationLink(
destination: Group {
if let username = navigateToUser {
UserDetailPage(userId: username)
}
},
isActive: Binding(
get: { navigateToUser != nil },
set: { if !$0 { navigateToUser = nil } }
)
) {
EmptyView()
}
.hidden()
)
}

private func handleLinkTap(_ url: URL) {
Expand Down
Loading