mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 07:36:48 +00:00
Explore: Added suggested accounts to follow
This commit is contained in:
parent
c598a4ab1d
commit
6e8ed998d4
8 changed files with 233 additions and 72 deletions
|
@ -24,12 +24,10 @@ struct IceCubesApp: App {
|
|||
.tabItem {
|
||||
Label("Notifications", systemImage: "bell")
|
||||
}
|
||||
}
|
||||
ExploreTab()
|
||||
.tabItem {
|
||||
Label("Explore", systemImage: "magnifyingglass")
|
||||
}
|
||||
if appAccountsManager.currentClient.isAuth {
|
||||
ExploreTab()
|
||||
.tabItem {
|
||||
Label("Explore", systemImage: "magnifyingglass")
|
||||
}
|
||||
AccountTab()
|
||||
.tabItem {
|
||||
Label("Profile", systemImage: "person.circle")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Network
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Status
|
||||
import Shimmer
|
||||
|
||||
public struct ExploreView: View {
|
||||
@EnvironmentObject private var client: Client
|
||||
|
@ -16,80 +17,132 @@ public struct ExploreView: View {
|
|||
|
||||
public var body: some View {
|
||||
List {
|
||||
Section("Trending Tags") {
|
||||
ForEach(viewModel.trendingTags
|
||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
||||
TagRowView(tag: tag)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingTags) { tag in
|
||||
TagRowView(tag: tag)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Tags")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.foregroundColor(.brand)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Trending Posts") {
|
||||
ForEach(viewModel.trendingStatuses
|
||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
||||
if !viewModel.isLoaded {
|
||||
ForEach(Status.placeholders()) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||
.padding(.vertical, 8)
|
||||
.redacted(reason: .placeholder)
|
||||
.shimmering()
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingStatuses) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Posts")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.foregroundColor(.brand)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Trending Links") {
|
||||
ForEach(viewModel.trendingLinks
|
||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||
StatusCardView(card: card)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingLinks) { card in
|
||||
StatusCardView(card: card)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Links")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.foregroundColor(.brand)
|
||||
}
|
||||
} 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
|
||||
TagRowView(tag: tag)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingTags) { tag in
|
||||
TagRowView(tag: tag)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Tags")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.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
|
||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingStatuses) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Posts")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.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
|
||||
StatusCardView(card: card)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingLinks) { card in
|
||||
StatusCardView(card: card)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Trending Links")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Text("See more")
|
||||
.foregroundColor(.brand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
83
Packages/Explore/Sources/Explore/SuggestedAccountRow.swift
Normal file
83
Packages/Explore/Sources/Explore/SuggestedAccountRow.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue