Skip to content

Commit 478cd20

Browse files
authored
Merge branch 'master' into claude/feed-category-loading-state-011CUhjzTbEhavoXDwAGfgv1
2 parents caf524b + edb5b75 commit 478cd20

File tree

6 files changed

+104
-14
lines changed

6 files changed

+104
-14
lines changed

DesignSystem/Sources/DesignSystem/Components/PostDisplayView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public struct PostDisplayView: View {
5151

5252
public var body: some View {
5353
VStack(alignment: .leading, spacing: 0) {
54-
HStack(alignment: .top, spacing: 12) {
54+
HStack(alignment: .center, spacing: 12) {
5555
// Thumbnail with proper loading
5656
ThumbnailView(url: post.url, isEnabled: showThumbnails)
5757
.frame(width: 55, height: 55)
@@ -67,7 +67,7 @@ public struct PostDisplayView: View {
6767
if let host = post.url.host,
6868
!isHackerNewsItemURL(post.url)
6969
{
70-
Text(truncatedHost(host))
70+
Text(truncatedHost(host).uppercased())
7171
.scaledFont(.caption)
7272
.foregroundColor(.secondary)
7373
.lineLimit(1)

Features/Comments/Sources/Comments/CommentsComponents.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ struct CommentsForEach: View {
163163
value: [comment.id: geometry.frame(in: .global)],
164164
)
165165
})
166-
.listRowSeparator(.hidden)
166+
.listRowSeparator(.visible)
167167
.if(comment.voteLinks?.upvote != nil && !comment.upvoted) { view in
168168
view.swipeActions(edge: .leading, allowsFullSwipe: true) {
169169
Button {
@@ -274,7 +274,6 @@ struct CommentRow: View {
274274

275275
var body: some View {
276276
VStack(alignment: .leading, spacing: 8) {
277-
Divider().padding(.bottom, 6)
278277
HStack {
279278
Text(comment.by)
280279
.scaledFont(.subheadline)
@@ -308,11 +307,12 @@ struct CommentRow: View {
308307
.foregroundColor(.primary)
309308
}
310309
}
311-
.listRowInsets(.init(top: 12, leading: CGFloat((comment.level + 1) * 16), bottom: 8, trailing: 16))
310+
.listRowInsets([.top, .bottom, .trailing], 16)
311+
.listRowInsets([.leading], CGFloat((comment.level + 1) * 16))
312312
.contentShape(Rectangle())
313313
.onTapGesture { onToggle() }
314314
.accessibilityAddTraits(.isButton)
315-
.accessibilityHint(comment.visibility == .visible ? "Double-tap to collapse" : "Double-tap to expand")
315+
.accessibilityHint(comment.visibility == .visible ? "Tap to collapse" : "Tap to expand")
316316
.contextMenu {
317317
VotingContextMenuItems.commentVotingMenuItems(
318318
for: comment,
@@ -484,6 +484,32 @@ struct ToolbarTitle: View {
484484
}
485485
}
486486

487+
struct BookmarkToolbarButton: View {
488+
let isBookmarked: Bool
489+
let toggleBookmark: @Sendable () async -> Bool
490+
@State private var isSubmitting = false
491+
492+
var body: some View {
493+
Button {
494+
guard !isSubmitting else { return }
495+
isSubmitting = true
496+
Task { @MainActor in
497+
_ = await toggleBookmark()
498+
isSubmitting = false
499+
}
500+
} label: {
501+
Image(systemName: isBookmarked ? "bookmark.fill" : "bookmark")
502+
}
503+
.accessibilityLabel(isBookmarked ? "Remove Bookmark" : "Save Bookmark")
504+
.accessibilityHint(
505+
isBookmarked
506+
? "Double-tap to remove from bookmarks"
507+
: "Double-tap to add to bookmarks"
508+
)
509+
.disabled(isSubmitting)
510+
}
511+
}
512+
487513
struct ShareMenu: View {
488514
let post: Post
489515

Features/Comments/Sources/Comments/CommentsView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ public struct CommentsView<NavigationStore: NavigationStoreProtocol>: View {
9898
)
9999
}
100100
}
101+
ToolbarItem(placement: .navigationBarTrailing) {
102+
if let post = viewModel.post {
103+
BookmarkToolbarButton(
104+
isBookmarked: post.isBookmarked,
105+
toggleBookmark: { await viewModel.toggleBookmark() }
106+
)
107+
}
108+
}
101109
ToolbarItem(placement: .navigationBarTrailing) {
102110
if let post = viewModel.post {
103111
ShareMenu(post: post)

Features/Comments/Sources/Comments/CommentsViewModel.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public final class CommentsViewModel: @unchecked Sendable {
2828
private let settingsUseCase: any SettingsUseCase
2929
private let bookmarksController: BookmarksController
3030
private var settingsCancellable: AnyCancellable?
31+
private var bookmarksObservation: AnyCancellable?
3132

3233
public var comments: [Comment] { commentsLoader.data }
3334
public var isLoading: Bool { commentsLoader.isLoading }
@@ -77,6 +78,20 @@ public final class CommentsViewModel: @unchecked Sendable {
7778
self.showThumbnails = currentValue
7879
}
7980
}
81+
82+
bookmarksObservation = NotificationCenter.default.publisher(for: .bookmarksDidChange)
83+
.receive(on: DispatchQueue.main)
84+
.sink { [weak self] notification in
85+
guard let self else { return }
86+
guard let postId = notification.userInfo?["postId"] as? Int,
87+
postId == self.postID,
88+
let isBookmarked = notification.userInfo?["isBookmarked"] as? Bool
89+
else { return }
90+
if var currentPost = self.post {
91+
currentPost.isBookmarked = isBookmarked
92+
self.post = currentPost
93+
}
94+
}
8095
}
8196

8297
@MainActor

Features/Comments/Tests/CommentsTests/CommentsViewModelTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,47 @@ struct CommentsViewModelTests {
209209
#expect(viewModel.showThumbnails == true)
210210
}
211211

212+
@Test("Bookmark notifications update local post state")
213+
@MainActor
214+
func bookmarkNotificationsUpdatePostState() async {
215+
var unbookmarkedPost = testPost
216+
unbookmarkedPost.isBookmarked = false
217+
let viewModel = CommentsViewModel(
218+
post: unbookmarkedPost,
219+
postUseCase: mockPostUseCase,
220+
commentUseCase: mockCommentUseCase,
221+
voteUseCase: mockVoteUseCase,
222+
settingsUseCase: StubSettingsUseCase(showThumbnails: true),
223+
bookmarksController: bookmarksController
224+
)
225+
226+
#expect(viewModel.post?.isBookmarked == false)
227+
228+
NotificationCenter.default.post(
229+
name: .bookmarksDidChange,
230+
object: nil,
231+
userInfo: ["postId": unbookmarkedPost.id, "isBookmarked": true]
232+
)
233+
await Task.yield()
234+
#expect(viewModel.post?.isBookmarked == true)
235+
236+
NotificationCenter.default.post(
237+
name: .bookmarksDidChange,
238+
object: nil,
239+
userInfo: ["postId": unbookmarkedPost.id + 1, "isBookmarked": false]
240+
)
241+
await Task.yield()
242+
#expect(viewModel.post?.isBookmarked == true)
243+
244+
NotificationCenter.default.post(
245+
name: .bookmarksDidChange,
246+
object: nil,
247+
userInfo: ["postId": unbookmarkedPost.id, "isBookmarked": false]
248+
)
249+
await Task.yield()
250+
#expect(viewModel.post?.isBookmarked == false)
251+
}
252+
212253
// MARK: - Voting Tests
213254

214255
@Test("Upvoting post updates state correctly", arguments: [

Hackers.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@
320320
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
321321
CODE_SIGN_IDENTITY = "Apple Development";
322322
CODE_SIGN_STYLE = Automatic;
323-
CURRENT_PROJECT_VERSION = 143;
323+
CURRENT_PROJECT_VERSION = 144;
324324
DEBUG_INFORMATION_FORMAT = dwarf;
325325
DEVELOPMENT_TEAM = 2KB59GPA9B;
326326
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -332,7 +332,7 @@
332332
"@executable_path/Frameworks",
333333
"@executable_path/../../Frameworks",
334334
);
335-
MARKETING_VERSION = 5.2.0;
335+
MARKETING_VERSION = 5.2.1;
336336
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
337337
MTL_FAST_MATH = YES;
338338
PRODUCT_BUNDLE_IDENTIFIER = com.weiranzhang.Hackers.ActionExtension;
@@ -359,7 +359,7 @@
359359
CODE_SIGN_IDENTITY = "Apple Development";
360360
CODE_SIGN_STYLE = Automatic;
361361
COPY_PHASE_STRIP = NO;
362-
CURRENT_PROJECT_VERSION = 143;
362+
CURRENT_PROJECT_VERSION = 144;
363363
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
364364
DEVELOPMENT_TEAM = 2KB59GPA9B;
365365
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -371,7 +371,7 @@
371371
"@executable_path/Frameworks",
372372
"@executable_path/../../Frameworks",
373373
);
374-
MARKETING_VERSION = 5.2.0;
374+
MARKETING_VERSION = 5.2.1;
375375
MTL_ENABLE_DEBUG_INFO = NO;
376376
MTL_FAST_MATH = YES;
377377
PRODUCT_BUNDLE_IDENTIFIER = com.weiranzhang.Hackers.ActionExtension;
@@ -496,7 +496,7 @@
496496
CODE_SIGN_ENTITLEMENTS = "App/Supporting Files/Hackers.entitlements";
497497
CODE_SIGN_IDENTITY = "Apple Development";
498498
CODE_SIGN_STYLE = Automatic;
499-
CURRENT_PROJECT_VERSION = 143;
499+
CURRENT_PROJECT_VERSION = 144;
500500
DEVELOPMENT_TEAM = 2KB59GPA9B;
501501
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
502502
GCC_PRECOMPILE_PREFIX_HEADER = YES;
@@ -508,7 +508,7 @@
508508
"$(inherited)",
509509
"@executable_path/Frameworks",
510510
);
511-
MARKETING_VERSION = 5.2.0;
511+
MARKETING_VERSION = 5.2.1;
512512
PRODUCT_BUNDLE_IDENTIFIER = com.weiranzhang.Hackers;
513513
PRODUCT_NAME = Hackers;
514514
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -528,7 +528,7 @@
528528
CODE_SIGN_ENTITLEMENTS = "App/Supporting Files/Hackers.entitlements";
529529
CODE_SIGN_IDENTITY = "Apple Development";
530530
CODE_SIGN_STYLE = Automatic;
531-
CURRENT_PROJECT_VERSION = 143;
531+
CURRENT_PROJECT_VERSION = 144;
532532
DEVELOPMENT_TEAM = 2KB59GPA9B;
533533
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
534534
GCC_PRECOMPILE_PREFIX_HEADER = YES;
@@ -540,7 +540,7 @@
540540
"$(inherited)",
541541
"@executable_path/Frameworks",
542542
);
543-
MARKETING_VERSION = 5.2.0;
543+
MARKETING_VERSION = 5.2.1;
544544
PRODUCT_BUNDLE_IDENTIFIER = com.weiranzhang.Hackers;
545545
PRODUCT_NAME = Hackers;
546546
PROVISIONING_PROFILE_SPECIFIER = "";

0 commit comments

Comments
 (0)