See your followed tags on account screen

This commit is contained in:
Thomas Ricouard 2022-12-21 09:42:05 +01:00
parent 34df37a58c
commit effa895eac
4 changed files with 116 additions and 24 deletions

View File

@ -4,20 +4,25 @@ import Network
import Status
import Shimmer
import DesignSystem
import Routeur
public struct AccountDetailView: View {
@Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var client: Client
@EnvironmentObject private var routeurPath: RouterPath
@StateObject private var viewModel: AccountDetailViewModel
@State private var scrollOffset: CGFloat = 0
private let isCurrentUser: Bool
/// When coming from a URL like a mention tap in a status.
public init(accountId: String) {
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
isCurrentUser = false
}
/// When the account is already fetched by the parent caller.
public init(account: Account, isCurrentUser: Bool = false) {
_viewModel = StateObject(wrappedValue: .init(account: account,
isCurrentUser: isCurrentUser))
@ -44,7 +49,12 @@ public struct AccountDetailView: View {
.offset(y: -20)
}
StatusesListView(fetcher: viewModel)
switch viewModel.tabState {
case .statuses:
StatusesListView(fetcher: viewModel)
case let .followedTags(tags):
makeTagsListView(tags: tags)
}
}
}
.task {
@ -67,7 +77,7 @@ public struct AccountDetailView: View {
@ViewBuilder
private var headerView: some View {
switch viewModel.state {
switch viewModel.accountState {
case .loading:
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
account: .placeholder(),
@ -94,6 +104,28 @@ public struct AccountDetailView: View {
Text("Error: \(error.localizedDescription)")
}
}
private func makeTagsListView(tags: [Tag]) -> some View {
Group {
ForEach(tags) { tag in
HStack {
VStack(alignment: .leading) {
Text("#\(tag.name)")
.font(.headline)
Text("\(tag.totalUses) mentions from \(tag.totalAccounts) users in the last few days")
.font(.footnote)
.foregroundColor(.gray)
}
Spacer()
}
.padding(.horizontal, DS.Constants.layoutPadding)
.padding(.vertical, 8)
.onTapGesture {
routeurPath.navigate(to: .hashTag(tag: tag.name))
}
}
}
}
}
struct AccountDetailView_Previews: PreviewProvider {

View File

@ -8,33 +8,55 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
let accountId: String
var client: Client?
enum State {
enum AccountState {
case loading, data(account: Account), error(error: Error)
}
enum Tab: Int, CaseIterable {
case statuses, favourites
case statuses, favourites, followedTags
var title: String {
switch self {
case .statuses: return "Posts"
case .favourites: return "Favourites"
case .followedTags: return "Followed Tags"
}
}
}
@Published var state: State = .loading
enum TabState {
case followedTags(tags: [Tag])
case statuses(statusesState: StatusesState)
}
@Published var accountState: AccountState = .loading
@Published var tabState: TabState = .statuses(statusesState: .loading) {
didSet {
/// Forward viewModel tabState related to statusesState to statusesState property
/// for `StatusesFetcher` conformance as we wrap StatusesState in TabState
switch tabState {
case let .statuses(statusesState):
self.statusesState = statusesState
default:
break
}
}
}
@Published var statusesState: StatusesState = .loading
@Published var title: String = ""
@Published var relationship: Relationshionship?
@Published var favourites: [Status] = []
@Published var followedTags: [Tag] = []
@Published var selectedTab = Tab.statuses {
didSet {
switch selectedTab {
case .statuses:
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
case .favourites:
statusesState = .display(statuses: favourites, nextPageState: .none)
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .none))
case .followedTags:
tabState = .followedTags(tags: followedTags)
}
}
}
@ -44,14 +66,16 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
private(set) var statuses: [Status] = []
private let isCurrentUser: Bool
/// When coming from a URL like a mention tap in a status.
init(accountId: String) {
self.accountId = accountId
self.isCurrentUser = false
}
/// When the account is already fetched by the parent caller.
init(account: Account, isCurrentUser: Bool) {
self.accountId = account.id
self.state = .data(account: account)
self.accountState = .data(account: account)
self.isCurrentUser = isCurrentUser
}
@ -59,31 +83,37 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
guard let client else { return }
do {
let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId))
if !isCurrentUser {
if isCurrentUser {
self.followedTags = try await client.get(endpoint: Accounts.followedTags)
} else {
let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId))
self.relationship = relationships.first
}
self.title = account.displayName
state = .data(account: account)
accountState = .data(account: account)
} catch {
state = .error(error: error)
accountState = .error(error: error)
}
}
func fetchStatuses() async {
guard let client else { return }
do {
statusesState = .loading
tabState = .statuses(statusesState: .loading)
statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil))
favourites = try await client.get(endpoint: Accounts.favourites(sinceId: nil))
if isCurrentUser {
favourites = try await client.get(endpoint: Accounts.favourites)
}
switch selectedTab {
case .statuses:
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
tabState = .statuses(statusesState:.display(statuses: statuses, nextPageState: .hasNextPage))
case .favourites:
statusesState = .display(statuses: favourites, nextPageState: .none)
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .none))
case .followedTags:
tabState = .followedTags(tags: followedTags)
}
} catch {
statusesState = .error(error: error)
tabState = .statuses(statusesState: .error(error: error))
}
}
@ -93,15 +123,15 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
switch selectedTab {
case .statuses:
guard let lastId = statuses.last?.id else { return }
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))
statuses.append(contentsOf: newStatuses)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
case .favourites:
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
case .favourites, .followedTags:
break
}
} catch {
statusesState = .error(error: error)
tabState = .statuses(statusesState: .error(error: error))
}
}

View File

@ -0,0 +1,27 @@
import Foundation
public struct Tag: Codable, Identifiable {
public struct History: Codable {
public let day: String
public let accounts: String
public let uses: String
}
public var id: String {
name
}
public let name: String
public let url: String
public let following: Bool
public let history: [History]
public var totalUses: Int {
history.compactMap{ Int($0.uses) }.reduce(0, +)
}
public var totalAccounts: Int {
history.compactMap{ Int($0.accounts) }.reduce(0, +)
}
}

View File

@ -2,7 +2,9 @@ import Foundation
public enum Accounts: Endpoint {
case accounts(id: String)
case favourites(sinceId: String?)
case favourites
case followedTags
case featuredTags
case verifyCredentials
case statuses(id: String, sinceId: String?)
case relationships(id: String)
@ -15,6 +17,10 @@ public enum Accounts: Endpoint {
return "accounts/\(id)"
case .favourites:
return "favourites"
case .followedTags:
return "followed_tags"
case .featuredTags:
return "featured_tags"
case .verifyCredentials:
return "accounts/verify_credentials"
case .statuses(let id, _):
@ -33,9 +39,6 @@ public enum Accounts: Endpoint {
case .statuses(_, let sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
case .favourites(let sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
case let .relationships(id):
return [.init(name: "id", value: id)]
default: