Explore: Added suggested accounts to follow

This commit is contained in:
Thomas Ricouard 2022-12-23 15:28:22 +01:00
parent c598a4ab1d
commit 6e8ed998d4
8 changed files with 233 additions and 72 deletions

View file

@ -24,12 +24,10 @@ struct IceCubesApp: App {
.tabItem {
Label("Notifications", systemImage: "bell")
}
}
ExploreTab()
.tabItem {
Label("Explore", systemImage: "magnifyingglass")
}
if appAccountsManager.currentClient.isAuth {
AccountTab()
.tabItem {
Label("Profile", systemImage: "person.circle")

View file

@ -112,6 +112,9 @@ struct AccountDetailHeaderView: View {
Text(account.note.asSafeAttributedString)
.font(.body)
.padding(.top, 8)
.environment(\.openURL, OpenURLAction { url in
routeurPath.handle(url: url)
})
}
.padding(.horizontal, DS.Constants.layoutPadding)
.offset(y: -40)

View file

@ -80,7 +80,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
do {
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
async let followedTags: [Tag] = client.get(endpoint: Accounts.followedTags)
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(id: accountId))
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(ids: [accountId]))
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
let loadedAccount = try await account

View file

@ -41,4 +41,13 @@ public class RouterPath: ObservableObject {
}
return .systemAction
}
public func handle(url: URL) -> OpenURLAction.Result {
if url.pathComponents.contains(where: { $0 == "tags" }),
let tag = url.pathComponents.last {
navigate(to: .hashTag(tag: tag, account: nil))
return .handled
}
return .systemAction
}
}

View file

@ -4,6 +4,7 @@ import Network
import DesignSystem
import Models
import Status
import Shimmer
public struct ExploreView: View {
@EnvironmentObject private var client: Client
@ -16,6 +17,62 @@ public struct ExploreView: View {
public var body: some View {
List {
if !viewModel.isLoaded {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, isEmbed: false))
.padding(.vertical, 8)
.redacted(reason: .placeholder)
.shimmering()
}
} else {
trendingTagsSection
suggestedAccountsSection
trendingPostsSection
trendingLinksSection
}
}
.task {
viewModel.client = client
guard !viewModel.isLoaded else { return }
await viewModel.fetchTrending()
}
.refreshable {
Task {
await viewModel.fetchTrending()
}
}
.listStyle(.grouped)
.navigationTitle("Explore")
.searchable(text: $searchQuery)
}
private var suggestedAccountsSection: some View {
Section("Suggested Users") {
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))
}
}
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))
}
}
}
.listStyle(.plain)
.navigationTitle("Suggested Users")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("See more")
.foregroundColor(.brand)
}
}
}
private var trendingTagsSection: some View {
Section("Trending Tags") {
ForEach(viewModel.trendingTags
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
@ -37,7 +94,9 @@ public struct ExploreView: View {
.foregroundColor(.brand)
}
}
}
private var trendingPostsSection: some View {
Section("Trending Posts") {
ForEach(viewModel.trendingStatuses
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
@ -60,7 +119,9 @@ public struct ExploreView: View {
.foregroundColor(.brand)
}
}
}
private var trendingLinksSection: some View {
Section("Trending Links") {
ForEach(viewModel.trendingLinks
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
@ -83,13 +144,5 @@ public struct ExploreView: View {
}
}
}
.task {
viewModel.client = client
await viewModel.fetchTrending()
}
.listStyle(.grouped)
.navigationTitle("Explore")
.searchable(text: $searchQuery)
}
}

View file

@ -6,6 +6,9 @@ import Network
class ExploreViewModel: ObservableObject {
var client: Client?
@Published var isLoaded = false
@Published var suggestedAccounts: [Account] = []
@Published var suggestedAccountsRelationShips: [Relationshionship] = []
@Published var trendingTags: [Tag] = []
@Published var trendingStatuses: [Status] = []
@Published var trendingLinks: [Card] = []
@ -13,13 +16,20 @@ class ExploreViewModel: ObservableObject {
func fetchTrending() async {
guard let client else { return }
do {
isLoaded = false
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses)
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
self.suggestedAccounts = try await suggestedAccounts
self.trendingTags = try await trendingTags
self.trendingStatuses = try await trendingStatuses
self.trendingLinks = try await trendingLinks
self.suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: self.suggestedAccounts.map{ $0.id }))
isLoaded = true
} catch { }
}
}

View file

@ -0,0 +1,83 @@
import SwiftUI
import Models
import Network
import DesignSystem
import Env
@MainActor
class SuggestedAccountViewModel: ObservableObject {
var client: Client?
@Published var account: Account
@Published var relationShip: Relationshionship
init(account: Account, relationShip: Relationshionship) {
self.account = account
self.relationShip = relationShip
}
func follow() async {
guard let client else { return }
do {
self.relationShip = try await client.post(endpoint: Accounts.follow(id: account.id))
} catch {}
}
func unfollow() async {
guard let client else { return }
do {
self.relationShip = try await client.post(endpoint: Accounts.unfollow(id: account.id))
} catch {}
}
}
struct SuggestedAccountRow: View {
@EnvironmentObject private var routeurPath: RouterPath
@EnvironmentObject private var client: Client
@StateObject var viewModel: SuggestedAccountViewModel
var body: some View {
HStack(alignment: .top) {
AvatarView(url: viewModel.account.avatar, size: .status)
VStack(alignment: .leading, spacing: 2) {
viewModel.account.displayNameWithEmojis
.font(.subheadline)
.fontWeight(.semibold)
Text("@\(viewModel.account.acct)")
.font(.footnote)
.foregroundColor(.gray)
Text(viewModel.account.note.asSafeAttributedString)
.font(.callout)
.environment(\.openURL, OpenURLAction { url in
routeurPath.handle(url: url)
})
}
Spacer()
Button {
Task {
if viewModel.relationShip.following {
await viewModel.unfollow()
} else {
await viewModel.follow()
}
}
} label: {
if viewModel.relationShip.requested {
Text("Requested")
.font(.callout)
} else {
Text(viewModel.relationShip.following ? "Unfollow" : "Follow")
.font(.callout)
}
}
.buttonStyle(.bordered)
}
.onAppear {
viewModel.client = client
}
.onTapGesture {
routeurPath.navigate(to: .accountDetailWithAccount(account: viewModel.account))
}
}
}

View file

@ -7,10 +7,11 @@ public enum Accounts: Endpoint {
case featuredTags(id: String)
case verifyCredentials
case statuses(id: String, sinceId: String?, tag: String?)
case relationships(id: String)
case relationships(ids: [String])
case follow(id: String)
case unfollow(id: String)
case familiarFollowers(withAccount: String)
case suggestions
public func path() -> String {
switch self {
@ -34,6 +35,8 @@ public enum Accounts: Endpoint {
return "accounts/\(id)/unfollow"
case .familiarFollowers:
return "accounts/familiar_followers"
case .suggestions:
return "suggestions"
}
}
@ -48,8 +51,10 @@ public enum Accounts: Endpoint {
params.append(.init(name: "max_id", value: sinceId))
}
return params
case let .relationships(id):
return [.init(name: "id", value: id)]
case let .relationships(ids):
return ids.map {
URLQueryItem(name: "id[]", value: $0)
}
case let .familiarFollowers(withAccount):
return [.init(name: "id[]", value: withAccount)]
default: