mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 02:01:02 +00:00
Following / Followers page 1
This commit is contained in:
parent
d01bbda5dc
commit
e4e2b2ab8b
10 changed files with 211 additions and 23 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
|
@ -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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue