From e0b7a0e87e97735e79f87cace73ef9e7b230c4e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 15:42:17 +0000 Subject: [PATCH] Add tap-to-filter posts by user ID in thread view Tapping a poster's colored ID badge now filters the thread to show only posts from that user. A banner at the bottom shows the active filter with post count and an X button to clear it. Tapping the same ID again also clears the filter. The filter also appears as a chip in the search toolbar when searching. https://claude.ai/code/session_012Wd384Z4N4PGehDv6Lbn4N --- .../Boards/Catalog/Thread/PostView.swift | 14 ++++++ .../Boards/Catalog/Thread/ThreadView.swift | 45 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/swiftchan/Views/Boards/Catalog/Thread/PostView.swift b/swiftchan/Views/Boards/Catalog/Thread/PostView.swift index df7d201..427686f 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/PostView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/PostView.swift @@ -94,6 +94,7 @@ struct PostView: View { } if let id = post.pid { let color = Color.randomColor(seed: id) + let isFiltered = viewModel.searchFilters.posterID == id Text(id.description) .foregroundColor(Color.isRandomColorLight(seed: id) ? .black : .white) .background( @@ -102,7 +103,20 @@ struct PostView: View { .cornerRadius(5) .padding(.horizontal, -5) ) + .overlay( + RoundedRectangle(cornerRadius: 5) + .stroke(Color.accentColor, lineWidth: isFiltered ? 2 : 0) + .padding(.horizontal, -5) + ) .offset(x: 5) + .onTapGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + if viewModel.searchFilters.posterID == id { + viewModel.searchFilters.posterID = nil + } else { + viewModel.searchFilters.posterID = id + } + } } // Anonymous diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index e6c8227..6777f79 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -106,6 +106,8 @@ struct ThreadView: View { .overlay(alignment: .bottom) { if isSearching && !viewModel.searchResultIndices.isEmpty { searchToolbar + } else if viewModel.searchFilters.posterID != nil { + posterIDFilterBanner } } .overlay { @@ -305,6 +307,16 @@ struct ThreadView: View { viewModel.searchFilters.hasReplies.toggle() } ) + + if let posterID = viewModel.searchFilters.posterID { + FilterChip( + label: "ID: \(posterID)", + isSelected: true, + action: { + viewModel.searchFilters.posterID = nil + } + ) + } } .padding(.horizontal) } @@ -338,6 +350,39 @@ struct ThreadView: View { .background(.regularMaterial) } + @ViewBuilder + var posterIDFilterBanner: some View { + if let posterID = viewModel.searchFilters.posterID { + HStack { + let color = Color.randomColor(seed: posterID) + Text("Showing posts by") + .font(.subheadline) + .foregroundColor(.secondary) + Text(posterID) + .font(.subheadline) + .bold() + .foregroundColor(Color.isRandomColorLight(seed: posterID) ? .black : .white) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background(color) + .cornerRadius(5) + Text("(\(viewModel.searchResultIndices.count))") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Button { + viewModel.searchFilters.posterID = nil + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + } + } + .padding(.horizontal) + .padding(.vertical, 8) + .background(.regularMaterial) + } + } + private func fetchAndPrefetchMedia(auto: Bool = false) async { let hadPosts = !viewModel.posts.isEmpty await viewModel.getPosts()