Skip to content

Commit 81f7fd1

Browse files
authored
Beta feedback (#325)
- adds a content preview if you long press - adds a nice animation when collapsing/expanding comments - adds swipe navigation to CommentScreen - strip HTML from the post text - fix comments clipping at the bottom - fix crash
1 parent de348f1 commit 81f7fd1

File tree

6 files changed

+141
-43
lines changed

6 files changed

+141
-43
lines changed

ios/HackerNews/AppViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ final class AppViewModel {
195195
}
196196
let nextPage = pager.nextPage()
197197
let items = await api.fetchPage(page: nextPage)
198-
feedState.stories.removeLast() // remove the loading view
198+
if !feedState.stories.isEmpty {
199+
feedState.stories.removeLast() // remove the loading view
200+
}
199201
feedState.stories += items.map { story in
200202
let bookmarked = bookmarkStore.containsBookmark(with: story.id)
201203
return .loaded(content: story.toStoryContent(bookmarked: bookmarked))

ios/HackerNews/Comments/CommentRow.swift

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,68 @@ struct CommentRow: View {
1414
let state: CommentState
1515
let likeComment: (CommentState) -> Void
1616
let toggleComment: () -> Void
17+
18+
@State private var isPressed = false
1719

1820
var body: some View {
19-
VStack(alignment: .leading) {
21+
VStack(alignment: .leading, spacing: 0) {
2022
// first row
2123
HStack {
22-
// author
23-
Text("@\(state.user)")
24-
.font(.ibmPlexMono(.bold, size: 12))
25-
// time
26-
HStack(alignment: .center, spacing: 4.0) {
27-
Image(systemName: "clock")
24+
Group {
25+
// author
26+
Text("@\(state.user)")
27+
.font(.ibmPlexMono(.bold, size: 12))
28+
// time
29+
HStack(alignment: .center, spacing: 4.0) {
30+
Image(systemName: "clock")
31+
.font(.system(size: 12))
32+
Text(state.age)
33+
.font(.ibmPlexSans(.medium, size: 12))
34+
}
35+
.font(.caption)
36+
// collapse/expand
37+
Image(systemName: "chevron.up.chevron.down")
2838
.font(.system(size: 12))
29-
Text(state.age)
30-
.font(.ibmPlexSans(.medium, size: 12))
39+
.rotationEffect(.degrees(state.hidden ? 180 : 0))
40+
// space between
41+
Spacer()
42+
// upvote
43+
Button(action: {
44+
likeComment(state)
45+
}) {
46+
Image(systemName: "arrow.up")
47+
.font(.system(size: 12))
48+
.padding(.horizontal, 8)
49+
.padding(.vertical, 4)
50+
}
51+
.background(state.upvoted ? .green.opacity(0.2) : .white.opacity(0.2))
52+
.foregroundStyle(state.upvoted ? .green : .onBackground)
53+
.clipShape(Capsule())
3154
}
32-
.font(.caption)
33-
// collapse/expand
34-
Image(systemName: "chevron.up.chevron.down")
35-
.font(.system(size: 12))
36-
// space between
37-
Spacer()
38-
// upvote
39-
Button(action: {
40-
likeComment(state)
41-
}) {
42-
Image(systemName: "arrow.up")
43-
.font(.system(size: 12))
44-
.padding(.horizontal, 8)
45-
.padding(.vertical, 4)
46-
}
47-
.background(state.upvoted ? .green.opacity(0.2) : .white.opacity(0.2))
48-
.foregroundStyle(state.upvoted ? .green : .onBackground)
49-
.clipShape(Capsule())
5055
}
56+
.padding(8)
57+
.background(isPressed ? .surface.opacity(0.85) : .surface)
58+
.zIndex(1) // Ensure header stays on top
5159

5260
// Comment Body
5361
if !state.hidden {
54-
Text(state.text.strippingHTML())
55-
.font(.ibmPlexMono(.regular, size: 12))
62+
VStack(alignment: .leading) {
63+
Text(state.text.strippingHTML())
64+
.font(.ibmPlexMono(.regular, size: 12))
65+
}
66+
.padding(EdgeInsets(top: -3, leading: 8, bottom: 8, trailing: 8))
67+
.transition(
68+
.asymmetric(
69+
insertion: .move(edge: .top).combined(with: .opacity),
70+
removal: .move(edge: .top).combined(with: .opacity)
71+
)
72+
)
5673
}
5774
}
58-
.padding(8.0)
59-
.background(.surface)
75+
.background(isPressed ? .surface.opacity(0.85) : .surface)
6076
.clipShape(RoundedRectangle(cornerRadius: 16.0))
61-
.onTapGesture {
62-
toggleComment()
63-
}
77+
.animation(.spring(duration: 0.3), value: state.hidden)
78+
.simultaneousGesture(makeCommentGesture())
6479
.padding(
6580
EdgeInsets(
6681
top: 0,
@@ -71,6 +86,27 @@ struct CommentRow: View {
7186
}
7287
}
7388

89+
extension CommentRow {
90+
fileprivate func makeCommentGesture() -> some Gesture {
91+
DragGesture(minimumDistance: 0)
92+
.onChanged { value in
93+
// Only show press effect if we haven't moved far
94+
if abs(value.translation.height) < 2 && abs(value.translation.width) < 2 {
95+
isPressed = true
96+
} else {
97+
isPressed = false
98+
}
99+
}
100+
.onEnded { value in
101+
isPressed = false
102+
// Trigger tap if it was a small movement (effectively a tap)
103+
if abs(value.translation.height) < 2 && abs(value.translation.width) < 2 {
104+
toggleComment()
105+
}
106+
}
107+
}
108+
}
109+
74110
struct CommentView_Preview: PreviewProvider {
75111
static var previews: some View {
76112
PreviewVariants {

ios/HackerNews/Comments/CommentsHeader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ struct CommentsHeader: View {
6161
.clipShape(Capsule())
6262
}
6363
// body
64-
if state.story.text != nil {
64+
if let text = state.story.text {
6565
VStack(alignment: .leading, spacing: 8.0) {
6666
Image(systemName: "chevron.up.chevron.down")
6767
.font(.caption2)
68-
Text(state.story.text!)
68+
Text(text.strippingHTML())
6969
.font(.ibmPlexMono(.regular, size: 12))
7070
.frame(maxWidth: .infinity, alignment: .leading)
7171
.lineLimit(state.expanded ? nil : 4)

ios/HackerNews/Comments/CommentsScreen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ struct CommentsScreen: View {
8484
.clipShape(Circle())
8585
.padding(8)
8686
}
87-
.overlay(alignment: .bottom) {
87+
.safeAreaInset(edge: .bottom) {
8888
if model.state.postCommentState != nil {
8989
CommentComposer(
9090
state: Binding(

ios/HackerNews/Feed/FeedScreen.swift

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,39 @@ struct FeedScreen: View {
2323
}
2424
.listRowInsets(EdgeInsets())
2525
.listRowSeparatorTint(Color.gray.opacity(0.3))
26+
.contextMenu(
27+
menuItems: {
28+
if case .loaded(var content) = storyState {
29+
Button {
30+
content.bookmarked.toggle()
31+
model.toggleBookmark(content)
32+
} label: {
33+
Label(
34+
content.bookmarked ? "Remove Bookmark" : "Bookmark",
35+
systemImage: content.bookmarked ? "book.fill" : "book"
36+
)
37+
}
38+
39+
if let url = content.makeUrl() {
40+
ShareLink(
41+
item: url,
42+
preview: SharePreview(
43+
content.title,
44+
image: Image(systemName: "link")
45+
)
46+
)
47+
}
48+
}
49+
},
50+
preview: {
51+
if case .loaded(let content) = storyState,
52+
let url = content.makeUrl()
53+
{
54+
WebView(url: url)
55+
.frame(width: 300, height: 400)
56+
}
57+
}
58+
)
2659
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
2760
if case .loaded(var content) = storyState {
2861
Button {
@@ -57,8 +90,11 @@ struct FeedScreen: View {
5790
}) {
5891
Text(feedType.title)
5992
.font(.ibmPlexMono(.bold, size: 24))
60-
.scaleEffect(model.feedState.selectedFeed == feedType ? 1.0 : 0.8)
61-
.foregroundColor(model.feedState.selectedFeed == feedType ? .hnOrange : .gray)
93+
.scaleEffect(
94+
model.feedState.selectedFeed == feedType ? 1.0 : 0.8
95+
)
96+
.foregroundColor(
97+
model.feedState.selectedFeed == feedType ? .hnOrange : .gray)
6298
}
6399
}
64100
}
@@ -69,19 +105,22 @@ struct FeedScreen: View {
69105
}
70106

71107
#Preview {
72-
@Previewable @State var model = AppViewModel(bookmarkStore: FakeBookmarkDataStore())
108+
@Previewable @State var model = AppViewModel(
109+
bookmarkStore: FakeBookmarkDataStore())
73110
FeedScreen(model: $model)
74111
}
75112

76113
#Preview("Loading") {
77-
@Previewable @State var appModel = AppViewModel(bookmarkStore: FakeBookmarkDataStore())
114+
@Previewable @State var appModel = AppViewModel(
115+
bookmarkStore: FakeBookmarkDataStore())
78116
appModel.feedState = FeedState()
79117

80118
return FeedScreen(model: $appModel)
81119
}
82120

83121
#Preview("Has posts") {
84-
@Previewable @State var appModel = AppViewModel(bookmarkStore: FakeBookmarkDataStore())
122+
@Previewable @State var appModel = AppViewModel(
123+
bookmarkStore: FakeBookmarkDataStore())
85124
let fakeStories =
86125
PreviewHelpers
87126
.makeFakeStories()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
extension UINavigationController: @retroactive UIGestureRecognizerDelegate {
5+
override open func viewDidLoad() {
6+
super.viewDidLoad()
7+
interactivePopGestureRecognizer?.delegate = self
8+
}
9+
10+
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
11+
return viewControllers.count > 1
12+
}
13+
14+
// This makes it work properly with ScrollView
15+
public func gestureRecognizer(
16+
_ gestureRecognizer: UIGestureRecognizer,
17+
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
18+
) -> Bool {
19+
return true
20+
}
21+
}

0 commit comments

Comments
 (0)