Following / Followers page 1

This commit is contained in:
Thomas Ricouard 2022-12-23 18:47:19 +01:00
parent d01bbda5dc
commit e4e2b2ab8b
10 changed files with 211 additions and 23 deletions

View file

@ -17,6 +17,10 @@ extension View {
StatusDetailView(statusId: id) StatusDetailView(statusId: id)
case let .hashTag(tag, accountId): case let .hashTag(tag, accountId):
TimelineView(timeline: .hashtag(tag: tag, accountId: accountId)) TimelineView(timeline: .hashtag(tag: tag, accountId: accountId))
case let .following(id):
AccountsListView(accountId: id, mode: .following)
case let .followers(id):
AccountsListView(accountId: id, mode: .followers)
} }
} }
} }

View file

@ -79,8 +79,12 @@ struct AccountDetailHeaderView: View {
Spacer() Spacer()
Group { Group {
makeCustomInfoLabel(title: "Posts", count: account.statusesCount) makeCustomInfoLabel(title: "Posts", count: account.statusesCount)
NavigationLink(value: RouteurDestinations.following(id: account.id)) {
makeCustomInfoLabel(title: "Following", count: account.followingCount) makeCustomInfoLabel(title: "Following", count: account.followingCount)
}
NavigationLink(value: RouteurDestinations.followers(id: account.id)) {
makeCustomInfoLabel(title: "Followers", count: account.followersCount) makeCustomInfoLabel(title: "Followers", count: account.followersCount)
}
}.offset(y: 20) }.offset(y: 20)
} }
} }
@ -117,6 +121,7 @@ struct AccountDetailHeaderView: View {
VStack { VStack {
Text("\(count)") Text("\(count)")
.font(.headline) .font(.headline)
.foregroundColor(.brand)
Text(title) Text(title)
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)

View file

@ -3,28 +3,31 @@ import Models
import Network import Network
import DesignSystem import DesignSystem
import Env import Env
import Account
@MainActor @MainActor
class SuggestedAccountViewModel: ObservableObject { public class AccountsListRowViewModel: ObservableObject {
var client: Client? var client: Client?
@Published var account: Account @Published var account: Account
@Published var relationShip: Relationshionship @Published var relationShip: Relationshionship
init(account: Account, relationShip: Relationshionship) { public init(account: Account, relationShip: Relationshionship) {
self.account = account self.account = account
self.relationShip = relationShip self.relationShip = relationShip
} }
} }
struct SuggestedAccountRow: View { public struct AccountsListRow: View {
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@StateObject var viewModel: SuggestedAccountViewModel @StateObject var viewModel: AccountsListRowViewModel
var body: some View { public init(viewModel: AccountsListRowViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
public var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
AvatarView(url: viewModel.account.avatar, size: .status) AvatarView(url: viewModel.account.avatar, size: .status)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {

View file

@ -0,0 +1,70 @@
import SwiftUI
import Network
import Models
import Env
import Shimmer
public struct AccountsListView: View {
@EnvironmentObject private var client: Client
@StateObject private var viewModel: AccountsListViewModel
@State private var didAppear: Bool = false
public init(accountId: String, mode: AccountsListMode) {
_viewModel = StateObject(wrappedValue: .init(accountId: accountId, mode: mode))
}
public var body: some View {
List {
switch viewModel.state {
case .loading:
ForEach(Account.placeholders()) { account in
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
.redacted(reason: .placeholder)
.shimmering()
}
case let .display(accounts, relationships, nextPageState):
ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account,
relationShip: relationship))
}
}
switch nextPageState {
case .hasNextPage:
loadingRow
.onAppear {
Task {
await viewModel.fetchNextPage()
}
}
case .loadingNextPage:
loadingRow
case .none:
EmptyView()
}
case let .error(error):
Text(error.localizedDescription)
}
}
.listStyle(.plain)
.navigationTitle(viewModel.mode.rawValue.capitalized)
.navigationBarTitleDisplayMode(.inline)
.task {
viewModel.client = client
guard !didAppear else { return}
didAppear = true
await viewModel.fetch()
}
}
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
}
}

View file

@ -0,0 +1,82 @@
import SwiftUI
import Models
import Network
public enum AccountsListMode: String {
case following, followers
}
@MainActor
class AccountsListViewModel: ObservableObject {
var client: Client?
let accountId: String
let mode: AccountsListMode
public enum State {
public enum PagingState {
case hasNextPage, loadingNextPage, none
}
case loading
case display(accounts: [Account],
relationships: [Relationshionship],
nextPageState: PagingState)
case error(error: Error)
}
private var accounts: [Account] = []
private var relationships: [Relationshionship] = []
@Published var state = State.loading
init(accountId: String, mode: AccountsListMode) {
self.accountId = accountId
self.mode = mode
}
func fetch() async {
guard let client else { return }
do {
state = .loading
switch mode {
case .followers:
accounts = try await client.get(endpoint: Accounts.followers(id: accountId,
sinceId: nil))
case .following:
accounts = try await client.get(endpoint: Accounts.following(id: accountId,
sinceId: nil))
}
relationships = try await client.get(endpoint:
Accounts.relationships(ids: accounts.map{ $0.id }))
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: .hasNextPage)
} catch { }
}
func fetchNextPage() async {
guard let client else { return }
do {
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage)
let newAccounts: [Account]
switch mode {
case .followers:
newAccounts = try await client.get(endpoint: Accounts.followers(id: accountId,
sinceId: accounts.last?.id))
case .following:
newAccounts = try await client.get(endpoint: Accounts.following(id: accountId,
sinceId: accounts.last?.id))
}
accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationshionship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id }))
relationships.append(contentsOf: newRelationships)
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: .hasNextPage)
} catch {
print(error)
}
}
}

View file

@ -44,28 +44,32 @@ public struct AvatarView: View {
AsyncImage(url: url) { phase in AsyncImage(url: url) { phase in
switch phase { switch phase {
case .empty: case .empty:
if size == .badge { placeholderView
Circle()
.fill(.gray)
.frame(width: size.size.width, height: size.size.height)
.shimmering() .shimmering()
} else {
RoundedRectangle(cornerRadius: size.cornerRadius)
.fill(.gray)
.frame(width: size.size.width, height: size.size.height)
.shimmering()
}
case let .success(image): case let .success(image):
image.resizable() image.resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.cornerRadius(size.cornerRadius) .cornerRadius(size.cornerRadius)
.frame(maxWidth: size.size.width, maxHeight: size.size.height) .frame(maxWidth: size.size.width, maxHeight: size.size.height)
case .failure: case .failure:
EmptyView() placeholderView
@unknown default: @unknown default:
EmptyView() placeholderView
} }
} }
} }
} }
@ViewBuilder
private var placeholderView: some View {
if size == .badge {
Circle()
.fill(.gray)
.frame(width: size.size.width, height: size.size.height)
} else {
RoundedRectangle(cornerRadius: size.cornerRadius)
.fill(.gray)
.frame(width: size.size.width, height: size.size.height)
}
}
} }

View file

@ -7,6 +7,8 @@ public enum RouteurDestinations: Hashable {
case accountDetailWithAccount(account: Account) case accountDetailWithAccount(account: Account)
case statusDetail(id: String) case statusDetail(id: String)
case hashTag(tag: String, account: String?) case hashTag(tag: String, account: String?)
case followers(id: String)
case following(id: String)
} }
public enum SheetDestinations: Identifiable { public enum SheetDestinations: Identifiable {

View file

@ -5,6 +5,7 @@ import DesignSystem
import Models import Models
import Status import Status
import Shimmer import Shimmer
import Account
public struct ExploreView: View { public struct ExploreView: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@ -51,14 +52,14 @@ public struct ExploreView: View {
ForEach(viewModel.suggestedAccounts ForEach(viewModel.suggestedAccounts
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in .prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
SuggestedAccountRow(viewModel: .init(account: account, relationShip: relationship)) AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
} }
} }
NavigationLink { NavigationLink {
List { List {
ForEach(viewModel.suggestedAccounts) { account in ForEach(viewModel.suggestedAccounts) { account in
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
SuggestedAccountRow(viewModel: .init(account: account, relationShip: relationship)) AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
} }
} }
} }

View file

@ -48,6 +48,11 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
locked: false, locked: false,
emojis: []) emojis: [])
} }
public static func placeholders() -> [Account] {
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
}
} }
public struct FamilliarAccounts: Codable { public struct FamilliarAccounts: Codable {

View file

@ -12,6 +12,8 @@ public enum Accounts: Endpoint {
case unfollow(id: String) case unfollow(id: String)
case familiarFollowers(withAccount: String) case familiarFollowers(withAccount: String)
case suggestions case suggestions
case followers(id: String, sinceId: String?)
case following(id: String, sinceId: String?)
public func path() -> String { public func path() -> String {
switch self { switch self {
@ -37,6 +39,10 @@ public enum Accounts: Endpoint {
return "accounts/familiar_followers" return "accounts/familiar_followers"
case .suggestions: case .suggestions:
return "suggestions" return "suggestions"
case .following(let id, _):
return "accounts/\(id)/following"
case .followers(let id, _):
return "accounts/\(id)/followers"
} }
} }
@ -57,6 +63,12 @@ public enum Accounts: Endpoint {
} }
case let .familiarFollowers(withAccount): case let .familiarFollowers(withAccount):
return [.init(name: "id[]", value: withAccount)] return [.init(name: "id[]", value: withAccount)]
case let .followers(_, sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
case let .following(_, sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
default: default:
return nil return nil
} }