diff --git a/IceCubesApp/App/AppRouteur.swift b/IceCubesApp/App/AppRouteur.swift index d827ca67..10544b07 100644 --- a/IceCubesApp/App/AppRouteur.swift +++ b/IceCubesApp/App/AppRouteur.swift @@ -15,8 +15,8 @@ extension View { AccountDetailView(account: account) case let .statusDetail(id): StatusDetailView(statusId: id) - case let .hashTag(tag): - TimelineView(timeline: .hashtag(tag: tag)) + case let .hashTag(tag, accountId): + TimelineView(timeline: .hashtag(tag: tag, accountId: accountId)) } } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index c9978790..d34bbdb5 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -35,6 +35,8 @@ public struct AccountDetailView: View { } content: { LazyVStack { headerView + featuredTagsView + .offset(y: -36) if isCurrentUser { Picker("", selection: $viewModel.selectedTab) { ForEach(AccountDetailViewModel.Tab.allCases, id: \.self) { tab in @@ -107,6 +109,29 @@ public struct AccountDetailView: View { } } + @ViewBuilder + private var featuredTagsView: some View { + if !viewModel.featuredTags.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + ForEach(viewModel.featuredTags) { tag in + Button { + routeurPath.navigate(to: .hashTag(tag: tag.name, account: viewModel.accountId)) + } label: { + VStack(alignment: .leading, spacing: 0) { + Text("#\(tag.name)") + .font(.callout) + Text("\(tag.statusesCount) posts") + .font(.caption2) + } + }.buttonStyle(.bordered) + } + } + .padding(.leading, DS.Constants.layoutPadding) + } + } + } + private func makeTagsListView(tags: [Tag]) -> some View { Group { ForEach(tags) { tag in @@ -123,7 +148,7 @@ public struct AccountDetailView: View { .padding(.horizontal, DS.Constants.layoutPadding) .padding(.vertical, 8) .onTapGesture { - routeurPath.navigate(to: .hashTag(tag: tag.name)) + routeurPath.navigate(to: .hashTag(tag: tag.name, account: nil)) } } } diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index b09e99e0..d39a6d0f 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -48,6 +48,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { @Published var relationship: Relationshionship? @Published var favourites: [Status] = [] @Published var followedTags: [Tag] = [] + @Published var featuredTags: [FeaturedTag] = [] @Published var selectedTab = Tab.statuses { didSet { reloadTabState() @@ -82,6 +83,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId)) self.relationship = relationships.first } + self.featuredTags = try await client.get(endpoint: Accounts.featuredTags(id: accountId)) + self.featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt } self.title = account.displayName accountState = .data(account: account) } catch { @@ -93,7 +96,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { guard let client else { return } do { tabState = .statuses(statusesState: .loading) - statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil)) + statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil)) if isCurrentUser { favourites = try await client.get(endpoint: Accounts.favourites) } @@ -110,7 +113,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { case .statuses: guard let lastId = statuses.last?.id else { return } tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage)) - let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId)) + let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId, tag: nil)) statuses.append(contentsOf: newStatuses) tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) case .favourites, .followedTags: diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index a3981e54..97094fe2 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -25,3 +25,13 @@ public struct Tag: Codable, Identifiable { history.compactMap{ Int($0.accounts) }.reduce(0, +) } } + +public struct FeaturedTag: Codable, Identifiable { + public let id: String + public let name: String + public let url: URL + public let statusesCount: String + public var statusesCountInt: Int { + Int(statusesCount) ?? 0 + } +} diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index 89da73e9..8f2dfd08 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -4,9 +4,9 @@ public enum Accounts: Endpoint { case accounts(id: String) case favourites case followedTags - case featuredTags + case featuredTags(id: String) case verifyCredentials - case statuses(id: String, sinceId: String?) + case statuses(id: String, sinceId: String?, tag: String?) case relationships(id: String) case follow(id: String) case unfollow(id: String) @@ -19,11 +19,11 @@ public enum Accounts: Endpoint { return "favourites" case .followedTags: return "followed_tags" - case .featuredTags: - return "featured_tags" + case .featuredTags(let id): + return "accounts/\(id)/featured_tags" case .verifyCredentials: return "accounts/verify_credentials" - case .statuses(let id, _): + case .statuses(let id, _, _): return "accounts/\(id)/statuses" case .relationships: return "accounts/relationships" @@ -36,9 +36,15 @@ public enum Accounts: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { - case .statuses(_, let sinceId): - guard let sinceId else { return nil } - return [.init(name: "max_id", value: sinceId)] + case .statuses(_, let sinceId, let tag): + var params: [URLQueryItem] = [] + if let tag { + params.append(.init(name: "tagged", value: tag)) + } + if let sinceId { + params.append(.init(name: "max_id", value: sinceId)) + } + return params case let .relationships(id): return [.init(name: "id", value: id)] default: diff --git a/Packages/Routeur/Sources/Routeur/Routeur.swift b/Packages/Routeur/Sources/Routeur/Routeur.swift index 7773fd3a..f431573f 100644 --- a/Packages/Routeur/Sources/Routeur/Routeur.swift +++ b/Packages/Routeur/Sources/Routeur/Routeur.swift @@ -6,7 +6,7 @@ public enum RouteurDestinations: Hashable { case accountDetail(id: String) case accountDetailWithAccount(account: Account) case statusDetail(id: String) - case hashTag(tag: String) + case hashTag(tag: String, account: String?) } public enum SheetDestinations: Identifiable { @@ -33,7 +33,7 @@ public class RouterPath: ObservableObject { public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result { if url.pathComponents.contains(where: { $0 == "tags" }), let tag = url.pathComponents.last { - navigate(to: .hashTag(tag: tag)) + navigate(to: .hashTag(tag: tag, account: nil)) return .handled } else if let mention = status.mentions.first(where: { $0.url == url }) { navigate(to: .accountDetail(id: mention.id)) diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index 79effcbc..aa012a8e 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -4,7 +4,7 @@ import Network public enum TimelineFilter: Hashable, Equatable { case pub, home - case hashtag(tag: String) + case hashtag(tag: String, accountId: String?) public func hash(into hasher: inout Hasher) { hasher.combine(title()) @@ -20,17 +20,21 @@ public enum TimelineFilter: Hashable, Equatable { return "Public" case .home: return "Home" - case let .hashtag(tag): + case let .hashtag(tag, _): return "#\(tag)" } } - func endpoint(sinceId: String?) -> Timelines { + func endpoint(sinceId: String?) -> Endpoint { switch self { - case .pub: return .pub(sinceId: sinceId) - case .home: return .home(sinceId: sinceId) - case let .hashtag(tag): - return .hashtag(tag: tag, sinceId: sinceId) + case .pub: return Timelines.pub(sinceId: sinceId) + case .home: return Timelines.home(sinceId: sinceId) + case let .hashtag(tag, accountId): + if let accountId { + return Accounts.statuses(id: accountId, sinceId: nil, tag: tag) + } else { + return Timelines.hashtag(tag: tag, sinceId: sinceId) + } } } } diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 03d0d638..4237a60f 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -16,7 +16,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { Task { await fetchStatuses() switch timeline { - case let .hashtag(tag): + case let .hashtag(tag, _): await fetchTag(id: tag) default: break