mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-01 18:52:20 +00:00
See your followed tags on account screen
This commit is contained in:
parent
34df37a58c
commit
effa895eac
4 changed files with 116 additions and 24 deletions
|
@ -4,20 +4,25 @@ import Network
|
||||||
import Status
|
import Status
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Routeur
|
||||||
|
|
||||||
public struct AccountDetailView: View {
|
public struct AccountDetailView: View {
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
@StateObject private var viewModel: AccountDetailViewModel
|
@StateObject private var viewModel: AccountDetailViewModel
|
||||||
@State private var scrollOffset: CGFloat = 0
|
@State private var scrollOffset: CGFloat = 0
|
||||||
|
|
||||||
private let isCurrentUser: Bool
|
private let isCurrentUser: Bool
|
||||||
|
|
||||||
|
/// When coming from a URL like a mention tap in a status.
|
||||||
public init(accountId: String) {
|
public init(accountId: String) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
|
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
|
||||||
isCurrentUser = false
|
isCurrentUser = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When the account is already fetched by the parent caller.
|
||||||
public init(account: Account, isCurrentUser: Bool = false) {
|
public init(account: Account, isCurrentUser: Bool = false) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(account: account,
|
_viewModel = StateObject(wrappedValue: .init(account: account,
|
||||||
isCurrentUser: isCurrentUser))
|
isCurrentUser: isCurrentUser))
|
||||||
|
@ -44,7 +49,12 @@ public struct AccountDetailView: View {
|
||||||
.offset(y: -20)
|
.offset(y: -20)
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusesListView(fetcher: viewModel)
|
switch viewModel.tabState {
|
||||||
|
case .statuses:
|
||||||
|
StatusesListView(fetcher: viewModel)
|
||||||
|
case let .followedTags(tags):
|
||||||
|
makeTagsListView(tags: tags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
|
@ -67,7 +77,7 @@ public struct AccountDetailView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var headerView: some View {
|
private var headerView: some View {
|
||||||
switch viewModel.state {
|
switch viewModel.accountState {
|
||||||
case .loading:
|
case .loading:
|
||||||
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
|
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
|
||||||
account: .placeholder(),
|
account: .placeholder(),
|
||||||
|
@ -94,6 +104,28 @@ public struct AccountDetailView: View {
|
||||||
Text("Error: \(error.localizedDescription)")
|
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 {
|
struct AccountDetailView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -8,33 +8,55 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
let accountId: String
|
let accountId: String
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
enum State {
|
enum AccountState {
|
||||||
case loading, data(account: Account), error(error: Error)
|
case loading, data(account: Account), error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tab: Int, CaseIterable {
|
enum Tab: Int, CaseIterable {
|
||||||
case statuses, favourites
|
case statuses, favourites, followedTags
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .statuses: return "Posts"
|
case .statuses: return "Posts"
|
||||||
case .favourites: return "Favourites"
|
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 statusesState: StatusesState = .loading
|
||||||
|
|
||||||
@Published var title: String = ""
|
@Published var title: String = ""
|
||||||
@Published var relationship: Relationshionship?
|
@Published var relationship: Relationshionship?
|
||||||
@Published var favourites: [Status] = []
|
@Published var favourites: [Status] = []
|
||||||
|
@Published var followedTags: [Tag] = []
|
||||||
@Published var selectedTab = Tab.statuses {
|
@Published var selectedTab = Tab.statuses {
|
||||||
didSet {
|
didSet {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses:
|
case .statuses:
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
|
||||||
case .favourites:
|
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(set) var statuses: [Status] = []
|
||||||
private let isCurrentUser: Bool
|
private let isCurrentUser: Bool
|
||||||
|
|
||||||
|
/// When coming from a URL like a mention tap in a status.
|
||||||
init(accountId: String) {
|
init(accountId: String) {
|
||||||
self.accountId = accountId
|
self.accountId = accountId
|
||||||
self.isCurrentUser = false
|
self.isCurrentUser = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When the account is already fetched by the parent caller.
|
||||||
init(account: Account, isCurrentUser: Bool) {
|
init(account: Account, isCurrentUser: Bool) {
|
||||||
self.accountId = account.id
|
self.accountId = account.id
|
||||||
self.state = .data(account: account)
|
self.accountState = .data(account: account)
|
||||||
self.isCurrentUser = isCurrentUser
|
self.isCurrentUser = isCurrentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,31 +83,37 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId))
|
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))
|
let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId))
|
||||||
self.relationship = relationships.first
|
self.relationship = relationships.first
|
||||||
}
|
}
|
||||||
self.title = account.displayName
|
self.title = account.displayName
|
||||||
state = .data(account: account)
|
accountState = .data(account: account)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
accountState = .error(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStatuses() async {
|
func fetchStatuses() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
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))
|
||||||
favourites = try await client.get(endpoint: Accounts.favourites(sinceId: nil))
|
if isCurrentUser {
|
||||||
|
favourites = try await client.get(endpoint: Accounts.favourites)
|
||||||
|
}
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses:
|
case .statuses:
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
tabState = .statuses(statusesState:.display(statuses: statuses, nextPageState: .hasNextPage))
|
||||||
case .favourites:
|
case .favourites:
|
||||||
statusesState = .display(statuses: favourites, nextPageState: .none)
|
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .none))
|
||||||
|
case .followedTags:
|
||||||
|
tabState = .followedTags(tags: followedTags)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
statusesState = .error(error: error)
|
tabState = .statuses(statusesState: .error(error: error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,15 +123,15 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses:
|
case .statuses:
|
||||||
guard let lastId = statuses.last?.id else { return }
|
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))
|
let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
|
||||||
case .favourites:
|
case .favourites, .followedTags:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
statusesState = .error(error: error)
|
tabState = .statuses(statusesState: .error(error: error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
27
Packages/Models/Sources/Models/Tag.swift
Normal file
27
Packages/Models/Sources/Models/Tag.swift
Normal 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, +)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ import Foundation
|
||||||
|
|
||||||
public enum Accounts: Endpoint {
|
public enum Accounts: Endpoint {
|
||||||
case accounts(id: String)
|
case accounts(id: String)
|
||||||
case favourites(sinceId: String?)
|
case favourites
|
||||||
|
case followedTags
|
||||||
|
case featuredTags
|
||||||
case verifyCredentials
|
case verifyCredentials
|
||||||
case statuses(id: String, sinceId: String?)
|
case statuses(id: String, sinceId: String?)
|
||||||
case relationships(id: String)
|
case relationships(id: String)
|
||||||
|
@ -15,6 +17,10 @@ public enum Accounts: Endpoint {
|
||||||
return "accounts/\(id)"
|
return "accounts/\(id)"
|
||||||
case .favourites:
|
case .favourites:
|
||||||
return "favourites"
|
return "favourites"
|
||||||
|
case .followedTags:
|
||||||
|
return "followed_tags"
|
||||||
|
case .featuredTags:
|
||||||
|
return "featured_tags"
|
||||||
case .verifyCredentials:
|
case .verifyCredentials:
|
||||||
return "accounts/verify_credentials"
|
return "accounts/verify_credentials"
|
||||||
case .statuses(let id, _):
|
case .statuses(let id, _):
|
||||||
|
@ -33,9 +39,6 @@ public enum Accounts: Endpoint {
|
||||||
case .statuses(_, let sinceId):
|
case .statuses(_, let sinceId):
|
||||||
guard let sinceId else { return nil }
|
guard let sinceId else { return nil }
|
||||||
return [.init(name: "max_id", value: sinceId)]
|
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):
|
case let .relationships(id):
|
||||||
return [.init(name: "id", value: id)]
|
return [.init(name: "id", value: id)]
|
||||||
default:
|
default:
|
||||||
|
|
Loading…
Reference in a new issue