Following / Followers page 1
This commit is contained in:
parent
d01bbda5dc
commit
e4e2b2ab8b
|
@ -17,6 +17,10 @@ extension View {
|
|||
StatusDetailView(statusId: id)
|
||||
case let .hashTag(tag, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,8 +79,12 @@ struct AccountDetailHeaderView: View {
|
|||
Spacer()
|
||||
Group {
|
||||
makeCustomInfoLabel(title: "Posts", count: account.statusesCount)
|
||||
makeCustomInfoLabel(title: "Following", count: account.followingCount)
|
||||
makeCustomInfoLabel(title: "Followers", count: account.followersCount)
|
||||
NavigationLink(value: RouteurDestinations.following(id: account.id)) {
|
||||
makeCustomInfoLabel(title: "Following", count: account.followingCount)
|
||||
}
|
||||
NavigationLink(value: RouteurDestinations.followers(id: account.id)) {
|
||||
makeCustomInfoLabel(title: "Followers", count: account.followersCount)
|
||||
}
|
||||
}.offset(y: 20)
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +121,7 @@ struct AccountDetailHeaderView: View {
|
|||
VStack {
|
||||
Text("\(count)")
|
||||
.font(.headline)
|
||||
.foregroundColor(.brand)
|
||||
Text(title)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
|
|
|
@ -3,28 +3,31 @@ import Models
|
|||
import Network
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Account
|
||||
|
||||
@MainActor
|
||||
class SuggestedAccountViewModel: ObservableObject {
|
||||
public class AccountsListRowViewModel: ObservableObject {
|
||||
var client: Client?
|
||||
|
||||
@Published var account: Account
|
||||
@Published var relationShip: Relationshionship
|
||||
|
||||
init(account: Account, relationShip: Relationshionship) {
|
||||
public init(account: Account, relationShip: Relationshionship) {
|
||||
self.account = account
|
||||
self.relationShip = relationShip
|
||||
}
|
||||
}
|
||||
|
||||
struct SuggestedAccountRow: View {
|
||||
public struct AccountsListRow: View {
|
||||
@EnvironmentObject private var routeurPath: RouterPath
|
||||
@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) {
|
||||
AvatarView(url: viewModel.account.avatar, size: .status)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,28 +44,32 @@ public struct AvatarView: View {
|
|||
AsyncImage(url: url) { phase in
|
||||
switch phase {
|
||||
case .empty:
|
||||
if size == .badge {
|
||||
Circle()
|
||||
.fill(.gray)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
.shimmering()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
||||
.fill(.gray)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
.shimmering()
|
||||
}
|
||||
placeholderView
|
||||
.shimmering()
|
||||
case let .success(image):
|
||||
image.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.cornerRadius(size.cornerRadius)
|
||||
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
||||
case .failure:
|
||||
EmptyView()
|
||||
placeholderView
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ public enum RouteurDestinations: Hashable {
|
|||
case accountDetailWithAccount(account: Account)
|
||||
case statusDetail(id: String)
|
||||
case hashTag(tag: String, account: String?)
|
||||
case followers(id: String)
|
||||
case following(id: String)
|
||||
}
|
||||
|
||||
public enum SheetDestinations: Identifiable {
|
||||
|
|
|
@ -5,6 +5,7 @@ import DesignSystem
|
|||
import Models
|
||||
import Status
|
||||
import Shimmer
|
||||
import Account
|
||||
|
||||
public struct ExploreView: View {
|
||||
@EnvironmentObject private var client: Client
|
||||
|
@ -51,14 +52,14 @@ public struct ExploreView: View {
|
|||
ForEach(viewModel.suggestedAccounts
|
||||
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
||||
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 {
|
||||
List {
|
||||
ForEach(viewModel.suggestedAccounts) { account in
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
|||
locked: false,
|
||||
emojis: [])
|
||||
}
|
||||
|
||||
public static func placeholders() -> [Account] {
|
||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
|
||||
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||
}
|
||||
}
|
||||
|
||||
public struct FamilliarAccounts: Codable {
|
||||
|
|
|
@ -12,6 +12,8 @@ public enum Accounts: Endpoint {
|
|||
case unfollow(id: String)
|
||||
case familiarFollowers(withAccount: String)
|
||||
case suggestions
|
||||
case followers(id: String, sinceId: String?)
|
||||
case following(id: String, sinceId: String?)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
|
@ -37,6 +39,10 @@ public enum Accounts: Endpoint {
|
|||
return "accounts/familiar_followers"
|
||||
case .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):
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue