Skip to content

Commit db24c5c

Browse files
graycreateclaude
andcommitted
feat: Improve RichView image loading, text size, and UI fixes
1. Fix image loading in RichView - Switch from RichView to RichContentView in NewsContentView and ReplyItemView - RichContentView properly renders images using AsyncImageAttachment with Kingfisher 2. Fix text size in topic detail page - Increase default body font from 16 to 17px - Increase compact (reply) font from 14 to 15px - Improve line spacing and paragraph spacing 3. Fix URL escape characters in markdown - Don't escape . and - characters which broke URLs like https://n\.wtf/ - Use raw text for link content instead of escaped text 4. Fix @mention navigation - Add NavigationLink to navigate to UserDetailPage when tapping @username 5. Fix dropdown menu alignment - Position menu directly below navbar instead of overlapping content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5f3d002 commit db24c5c

File tree

6 files changed

+79
-53
lines changed

6 files changed

+79
-53
lines changed

V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public class HTMLToMarkdownConverter {
8888
result += "*\(content)*"
8989

9090
case "a":
91-
let text = try convertElement(childElement)
91+
// Get raw text without escaping for links
92+
let text = try childElement.text()
9293
if let href = try? childElement.attr("href") {
9394
result += "[\(text)](\(href))"
9495
} else {
@@ -213,14 +214,16 @@ public class HTMLToMarkdownConverter {
213214

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

220-
// Don't escape inside code blocks (this is simplified)
222+
// Don't escape inside code blocks
221223
if !text.contains("```") && !text.contains("`") {
222-
// Escape special Markdown characters
223-
let charactersToEscape = ["\\", "*", "_", "[", "]", "(", ")", "#", "+", "-", ".", "!"]
224+
// Only escape the most problematic markdown characters
225+
// Avoid escaping . and - as they appear frequently in URLs and text
226+
let charactersToEscape = ["\\", "*", "_", "[", "]"]
224227
for char in charactersToEscape {
225228
escaped = escaped.replacingOccurrences(of: char, with: "\\\(char)")
226229
}

V2er/Sources/RichView/Models/RenderStylesheet.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ public struct TextStyle: Equatable {
5050
public var color: Color
5151

5252
public init(
53-
fontSize: CGFloat = 16,
53+
fontSize: CGFloat = 17,
5454
fontWeight: Font.Weight = .regular,
55-
lineSpacing: CGFloat = 4,
56-
paragraphSpacing: CGFloat = 8,
55+
lineSpacing: CGFloat = 5,
56+
paragraphSpacing: CGFloat = 10,
5757
color: Color = .primary
5858
) {
5959
self.fontSize = fontSize
@@ -264,10 +264,10 @@ extension RenderStylesheet {
264264
public static let `default`: RenderStylesheet = {
265265
RenderStylesheet(
266266
body: TextStyle(
267-
fontSize: 16,
267+
fontSize: 17,
268268
fontWeight: .regular,
269-
lineSpacing: 4,
270-
paragraphSpacing: 8,
269+
lineSpacing: 5,
270+
paragraphSpacing: 10,
271271
color: .primary
272272
),
273273
heading: HeadingStyle(
@@ -343,10 +343,10 @@ extension RenderStylesheet {
343343
public static let compact: RenderStylesheet = {
344344
RenderStylesheet(
345345
body: TextStyle(
346-
fontSize: 14,
346+
fontSize: 15,
347347
fontWeight: .regular,
348-
lineSpacing: 2,
349-
paragraphSpacing: 6,
348+
lineSpacing: 4,
349+
paragraphSpacing: 8,
350350
color: .primary
351351
),
352352
heading: HeadingStyle(

V2er/Sources/RichView/Views/RichContentView.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,12 @@ public struct RichContentView: View {
4949
} else if let error = error {
5050
ErrorView(error: error)
5151
} else if !contentElements.isEmpty {
52-
ScrollView {
53-
VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) {
54-
ForEach(contentElements) { element in
55-
renderElement(element)
56-
}
52+
VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) {
53+
ForEach(contentElements) { element in
54+
renderElement(element)
5755
}
58-
.frame(maxWidth: .infinity, alignment: .leading)
5956
}
57+
.frame(maxWidth: .infinity, alignment: .leading)
6058
} else {
6159
Text("No content")
6260
.foregroundColor(.secondary)

V2er/View/Feed/FilterMenuView.swift

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,40 +28,40 @@ struct FilterMenuView: View {
2828
onDismiss()
2929
}
3030

31-
// Menu content - positioned below navbar
31+
// Menu content - positioned right below navbar
3232
VStack(spacing: 0) {
33-
HStack {
34-
Spacer()
35-
ScrollView {
36-
VStack(spacing: 4) {
37-
ForEach(Tab.allTabs, id: \.self) { tab in
38-
let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn()
39-
TabFilterMenuItem(
40-
tab: tab,
41-
isSelected: tab == selectedTab,
42-
needsLogin: tabNeedsLogin
43-
) {
44-
// Soft haptic feedback
45-
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
46-
impactFeedback.impactOccurred()
47-
48-
if tabNeedsLogin {
49-
Toast.show("登录后才能查看「\(tab.displayName())」下的内容")
50-
} else {
51-
onTabSelected(tab)
52-
}
33+
// Minimal spacing - just clear the safe area
34+
Spacer()
35+
.frame(height: topSafeAreaInset().top)
36+
37+
ScrollView {
38+
VStack(spacing: 4) {
39+
ForEach(Tab.allTabs, id: \.self) { tab in
40+
let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn()
41+
TabFilterMenuItem(
42+
tab: tab,
43+
isSelected: tab == selectedTab,
44+
needsLogin: tabNeedsLogin
45+
) {
46+
// Soft haptic feedback
47+
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
48+
impactFeedback.impactOccurred()
49+
50+
if tabNeedsLogin {
51+
Toast.show("登录后才能查看「\(tab.displayName())」下的内容")
52+
} else {
53+
onTabSelected(tab)
5354
}
5455
}
5556
}
56-
.padding(.vertical, 8)
5757
}
58-
.frame(width: 200)
59-
.background(Color.itemBg)
60-
.cornerRadius(8)
61-
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4)
62-
.frame(maxHeight: 450)
63-
Spacer()
58+
.padding(.vertical, 8)
6459
}
60+
.frame(width: 200)
61+
.background(Color.itemBg)
62+
.cornerRadius(8)
63+
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4)
64+
.frame(maxHeight: 450)
6565
.transition(.asymmetric(
6666
insertion: .move(edge: .top).combined(with: .opacity),
6767
removal: .move(edge: .top).combined(with: .opacity)

V2er/View/FeedDetail/NewsContentView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ struct NewsContentView: View {
2525
VStack(spacing: 0) {
2626
Divider()
2727

28-
RichView(htmlContent: contentInfo?.html ?? "")
28+
RichContentView(htmlContent: contentInfo?.html ?? "")
2929
.configuration(configurationForAppearance())
3030
.onLinkTapped { url in
3131
handleLinkTap(url)
3232
}
33+
.onImageTapped { url in
34+
// Open image in SafariView for now
35+
openInSafari(url)
36+
}
3337
.onRenderCompleted { metadata in
3438
// Mark as rendered after content is ready
3539
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
@@ -40,6 +44,8 @@ struct NewsContentView: View {
4044
print("Render error: \(error)")
4145
self.rendered = true
4246
}
47+
.padding(.horizontal, 12)
48+
.padding(.vertical, 8)
4349

4450
Divider()
4551
}

V2er/View/FeedDetail/ReplyItemView.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct ReplyItemView: View {
1616
@Environment(\.colorScheme) var colorScheme
1717
@State private var showingSafari = false
1818
@State private var safariURL: URL?
19+
@State private var navigateToUser: String? = nil
1920

2021
var body: some View {
2122
HStack(alignment: .top) {
@@ -50,14 +51,16 @@ struct ReplyItemView: View {
5051
.foregroundColor(info.hadThanked ? .red : .secondaryText)
5152
}
5253

53-
RichView(htmlContent: info.content)
54+
RichContentView(htmlContent: info.content)
5455
.configuration(compactConfigurationForAppearance())
5556
.onLinkTapped { url in
5657
handleLinkTap(url)
5758
}
59+
.onImageTapped { url in
60+
openInSafari(url)
61+
}
5862
.onMentionTapped { username in
59-
print("Navigate to mentioned user: @\(username)")
60-
// TODO: Implement proper navigation to UserDetailPage
63+
navigateToUser = username
6164
}
6265

6366
Text("\(info.floor)")
@@ -73,6 +76,22 @@ struct ReplyItemView: View {
7376
SafariView(url: url)
7477
}
7578
}
79+
.background(
80+
NavigationLink(
81+
destination: Group {
82+
if let username = navigateToUser {
83+
UserDetailPage(userId: username)
84+
}
85+
},
86+
isActive: Binding(
87+
get: { navigateToUser != nil },
88+
set: { if !$0 { navigateToUser = nil } }
89+
)
90+
) {
91+
EmptyView()
92+
}
93+
.hidden()
94+
)
7695
}
7796

7897
private func handleLinkTap(_ url: URL) {

0 commit comments

Comments
 (0)