Featured tags

This commit is contained in:
Thomas Ricouard 2022-12-21 20:26:38 +01:00
parent 07188a6818
commit 0f2c2df624
8 changed files with 71 additions and 23 deletions

View file

@ -15,8 +15,8 @@ extension View {
AccountDetailView(account: account) AccountDetailView(account: account)
case let .statusDetail(id): case let .statusDetail(id):
StatusDetailView(statusId: id) StatusDetailView(statusId: id)
case let .hashTag(tag): case let .hashTag(tag, accountId):
TimelineView(timeline: .hashtag(tag: tag)) TimelineView(timeline: .hashtag(tag: tag, accountId: accountId))
} }
} }
} }

View file

@ -35,6 +35,8 @@ public struct AccountDetailView: View {
} content: { } content: {
LazyVStack { LazyVStack {
headerView headerView
featuredTagsView
.offset(y: -36)
if isCurrentUser { if isCurrentUser {
Picker("", selection: $viewModel.selectedTab) { Picker("", selection: $viewModel.selectedTab) {
ForEach(AccountDetailViewModel.Tab.allCases, id: \.self) { tab in 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 { private func makeTagsListView(tags: [Tag]) -> some View {
Group { Group {
ForEach(tags) { tag in ForEach(tags) { tag in
@ -123,7 +148,7 @@ public struct AccountDetailView: View {
.padding(.horizontal, DS.Constants.layoutPadding) .padding(.horizontal, DS.Constants.layoutPadding)
.padding(.vertical, 8) .padding(.vertical, 8)
.onTapGesture { .onTapGesture {
routeurPath.navigate(to: .hashTag(tag: tag.name)) routeurPath.navigate(to: .hashTag(tag: tag.name, account: nil))
} }
} }
} }

View file

@ -48,6 +48,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
@Published var relationship: Relationshionship? @Published var relationship: Relationshionship?
@Published var favourites: [Status] = [] @Published var favourites: [Status] = []
@Published var followedTags: [Tag] = [] @Published var followedTags: [Tag] = []
@Published var featuredTags: [FeaturedTag] = []
@Published var selectedTab = Tab.statuses { @Published var selectedTab = Tab.statuses {
didSet { didSet {
reloadTabState() reloadTabState()
@ -82,6 +83,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId)) let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId))
self.relationship = relationships.first 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 self.title = account.displayName
accountState = .data(account: account) accountState = .data(account: account)
} catch { } catch {
@ -93,7 +96,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
guard let client else { return } guard let client else { return }
do { do {
tabState = .statuses(statusesState: .loading) 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 { if isCurrentUser {
favourites = try await client.get(endpoint: Accounts.favourites) favourites = try await client.get(endpoint: Accounts.favourites)
} }
@ -110,7 +113,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
case .statuses: case .statuses:
guard let lastId = statuses.last?.id else { return } guard let lastId = statuses.last?.id else { return }
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage)) 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) statuses.append(contentsOf: newStatuses)
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
case .favourites, .followedTags: case .favourites, .followedTags:

View file

@ -25,3 +25,13 @@ public struct Tag: Codable, Identifiable {
history.compactMap{ Int($0.accounts) }.reduce(0, +) 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
}
}

View file

@ -4,9 +4,9 @@ public enum Accounts: Endpoint {
case accounts(id: String) case accounts(id: String)
case favourites case favourites
case followedTags case followedTags
case featuredTags case featuredTags(id: String)
case verifyCredentials case verifyCredentials
case statuses(id: String, sinceId: String?) case statuses(id: String, sinceId: String?, tag: String?)
case relationships(id: String) case relationships(id: String)
case follow(id: String) case follow(id: String)
case unfollow(id: String) case unfollow(id: String)
@ -19,11 +19,11 @@ public enum Accounts: Endpoint {
return "favourites" return "favourites"
case .followedTags: case .followedTags:
return "followed_tags" return "followed_tags"
case .featuredTags: case .featuredTags(let id):
return "featured_tags" return "accounts/\(id)/featured_tags"
case .verifyCredentials: case .verifyCredentials:
return "accounts/verify_credentials" return "accounts/verify_credentials"
case .statuses(let id, _): case .statuses(let id, _, _):
return "accounts/\(id)/statuses" return "accounts/\(id)/statuses"
case .relationships: case .relationships:
return "accounts/relationships" return "accounts/relationships"
@ -36,9 +36,15 @@ public enum Accounts: Endpoint {
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case .statuses(_, let sinceId): case .statuses(_, let sinceId, let tag):
guard let sinceId else { return nil } var params: [URLQueryItem] = []
return [.init(name: "max_id", value: sinceId)] 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): case let .relationships(id):
return [.init(name: "id", value: id)] return [.init(name: "id", value: id)]
default: default:

View file

@ -6,7 +6,7 @@ public enum RouteurDestinations: Hashable {
case accountDetail(id: String) case accountDetail(id: String)
case accountDetailWithAccount(account: Account) case accountDetailWithAccount(account: Account)
case statusDetail(id: String) case statusDetail(id: String)
case hashTag(tag: String) case hashTag(tag: String, account: String?)
} }
public enum SheetDestinations: Identifiable { public enum SheetDestinations: Identifiable {
@ -33,7 +33,7 @@ public class RouterPath: ObservableObject {
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result { public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
if url.pathComponents.contains(where: { $0 == "tags" }), if url.pathComponents.contains(where: { $0 == "tags" }),
let tag = url.pathComponents.last { let tag = url.pathComponents.last {
navigate(to: .hashTag(tag: tag)) navigate(to: .hashTag(tag: tag, account: nil))
return .handled return .handled
} else if let mention = status.mentions.first(where: { $0.url == url }) { } else if let mention = status.mentions.first(where: { $0.url == url }) {
navigate(to: .accountDetail(id: mention.id)) navigate(to: .accountDetail(id: mention.id))

View file

@ -4,7 +4,7 @@ import Network
public enum TimelineFilter: Hashable, Equatable { public enum TimelineFilter: Hashable, Equatable {
case pub, home case pub, home
case hashtag(tag: String) case hashtag(tag: String, accountId: String?)
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(title()) hasher.combine(title())
@ -20,17 +20,21 @@ public enum TimelineFilter: Hashable, Equatable {
return "Public" return "Public"
case .home: case .home:
return "Home" return "Home"
case let .hashtag(tag): case let .hashtag(tag, _):
return "#\(tag)" return "#\(tag)"
} }
} }
func endpoint(sinceId: String?) -> Timelines { func endpoint(sinceId: String?) -> Endpoint {
switch self { switch self {
case .pub: return .pub(sinceId: sinceId) case .pub: return Timelines.pub(sinceId: sinceId)
case .home: return .home(sinceId: sinceId) case .home: return Timelines.home(sinceId: sinceId)
case let .hashtag(tag): case let .hashtag(tag, accountId):
return .hashtag(tag: tag, sinceId: sinceId) if let accountId {
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag)
} else {
return Timelines.hashtag(tag: tag, sinceId: sinceId)
}
} }
} }
} }

View file

@ -16,7 +16,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
Task { Task {
await fetchStatuses() await fetchStatuses()
switch timeline { switch timeline {
case let .hashtag(tag): case let .hashtag(tag, _):
await fetchTag(id: tag) await fetchTag(id: tag)
default: default:
break break