Bubble/Threaded/Views/DiscoveryView.swift

232 lines
9.2 KiB
Swift

//Made by Lumaa
import SwiftUI
struct DiscoveryView: View {
@Environment(AccountManager.self) private var accountManager: AccountManager
@State private var navigator: Navigator = Navigator()
@State private var searchQuery: String = ""
@State private var results: [String : SearchResults] = [:]
@State private var querying: Bool = false
let allTokens = [Token(id: "accounts", name: String(localized: "discovery.search.users"), image: "person.3.fill"), Token(id: "statuses", name: String(localized: "discovery.search.posts"), image: "note.text"), Token(id: "hashtags", name: String(localized: "discovery.search.tags"), image: "tag.fill")]
@State private var currentTokens = [Token]()
@State private var suggestedAccounts: [Account] = []
@State private var suggestedAccountsRelationShips: [Relationship] = []
@State private var trendingTags: [Tag] = []
@State private var trendingStatuses: [Status] = []
@State private var trendingLinks: [Card] = []
// TODO: "Read" button
var body: some View {
NavigationStack(path: $navigator.path) {
ScrollView {
if results != [:] && !querying {
SearchResultView(searchResults: results[searchQuery] ?? .init(accounts: [], statuses: [], hashtags: []), query: searchQuery)
.environmentObject(navigator)
Spacer()
.foregroundStyle(Color.white)
.padding()
} else if querying {
ProgressView()
.progressViewStyle(.circular)
}
VStack(alignment: .leading) {
Text("discovery.suggested.users")
.multilineTextAlignment(.leading)
.font(.title.bold())
.padding(.horizontal)
accountsView
Text("discovery.trending.tags")
.multilineTextAlignment(.leading)
.font(.title.bold())
.padding(.horizontal)
tagsView
Text("discovery.trending.posts")
.multilineTextAlignment(.leading)
.font(.title.bold())
.padding(.horizontal)
statusView
}
}
.listThreaded()
.submitLabel(.search)
.task(id: searchQuery) {
if !searchQuery.isEmpty {
querying = true
await search()
querying = false
} else {
querying = false
results = [:]
}
}
.onChange(of: currentTokens) { old, new in
guard new.count > 1 else { return }
let newToken = new.last ?? allTokens[1]
currentTokens = [newToken]
}
.searchable(text: $searchQuery, tokens: $currentTokens, suggestedTokens: .constant(allTokens), prompt: Text("discovery.search.prompt")) { token in
Label(token.name, systemImage: token.image)
}
.withAppRouter(navigator)
.navigationTitle(Text("discovery"))
}
.task {
await fetchTrending()
}
}
var accountsView: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 10) {
ForEach(suggestedAccounts) { account in
VStack {
ProfilePicture(url: account.avatar, size: 64)
Text(account.displayName?.replacing(/:.+:/, with: "") ?? account.username)
.font(.subheadline.bold())
.foregroundStyle(Color(uiColor: UIColor.label))
.lineLimit(1)
Text("@\(account.username)")
.font(.caption)
.foregroundStyle(Color.gray)
Spacer()
Button {
guard let client = accountManager.getClient() else { return }
Task {
do {
let better: Account = try await client.get(endpoint: Accounts.accounts(id: account.id))
navigator.navigate(to: .account(acc: better))
} catch {
print(error)
}
}
} label: {
Text("account.view")
}
.buttonStyle(LargeButton(filled: true, height: 7.5))
}
.padding(.vertical)
.frame(width: 200)
.background(Color.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 15.0))
}
.scrollTargetLayout()
}
}
.padding(.horizontal)
.scrollClipDisabled()
.defaultScrollAnchor(.leading)
}
var tagsView: some View {
VStack(spacing: 7.5) {
ForEach(trendingTags) { tag in
HStack {
VStack(alignment: .leading) {
Text("#\(tag.name)")
.multilineTextAlignment(.leading)
.lineLimit(1)
.bold()
Text("tag.posts-\(tag.totalUses)")
.multilineTextAlignment(.leading)
.lineLimit(1)
.foregroundStyle(Color.gray)
}
Spacer()
Button {
navigator.navigate(to: .timeline(timeline: .hashtag(tag: tag.name, accountId: nil)))
} label: {
Text("tag.read")
}
.buttonStyle(LargeButton(filled: true, height: 7.5, disabled: true))
.disabled(true)
}
.padding()
}
}
}
var statusView: some View {
VStack(spacing: 7.5) {
ForEach(trendingStatuses) { status in
CompactPostView(status: status)
.padding(.vertical)
.environmentObject(navigator)
}
}
}
private func search() async {
guard let client = accountManager.getClient(), !searchQuery.isEmpty else { return }
do {
try await Task.sleep(for: .milliseconds(250))
let results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, type: currentTokens.first?.id, offset: nil, following: nil), forceVersion: .v2)
// let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id)))
// results.relationships = relationships
withAnimation {
self.results[searchQuery] = results
}
} catch {
print(error)
}
}
private func fetchTrending() async {
guard let client = accountManager.getClient() else { return }
do {
let data = try await fetchTrendingsData(client: client)
suggestedAccounts = data.suggestedAccounts
trendingTags = data.trendingTags
trendingStatuses = data.trendingStatuses
trendingLinks = data.trendingLinks
trendingTags = trendingTags.sorted(by: { $0.totalUses > $1.totalUses })
suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: suggestedAccounts.map(\.id)))
} catch {
print(error)
}
}
private func fetchTrendingsData(client: Client) async throws -> TrendingData {
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(offset: nil))
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
return try await .init(suggestedAccounts: suggestedAccounts, trendingTags: trendingTags, trendingStatuses: trendingStatuses, trendingLinks: trendingLinks)
}
private struct TrendingData {
let suggestedAccounts: [Account]
let trendingTags: [Tag]
let trendingStatuses: [Status]
let trendingLinks: [Card]
}
struct Token: Identifiable, Equatable {
var id: String
var name: String
var image: String
}
}