//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] = [:] let allTokens = [Token(id: "accounts", name: String(localized: "discovery.search.users")), Token(id: "statuses", name: String(localized: "discovery.search.posts")), Token(id: "hashtags", name: String(localized: "discovery.search.tags"))] @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 + search with scopes var body: some View { NavigationStack(path: $navigator.path) { ScrollView { 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 } } .submitLabel(.search) .onSubmit { Task { await search() } } .onChange(of: currentTokens) { old, new in guard new.count > 1 else { return } let oldToken = old.first ?? allTokens[0] let newToken = new.last ?? allTokens[1] currentTokens.removeAll(where: { $0 == oldToken }) } .searchable(text: $searchQuery, tokens: $currentTokens, suggestedTokens: .constant(allTokens), prompt: Text("discovery.search.prompt")) { token in Text(token.name) } .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 { ScrollView { 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 { // do stuff } label: { Text("tag.read") } .buttonStyle(LargeButton(filled: true, height: 7.5)) } .padding() } } } } private func search() async { guard let client = accountManager.getClient(), !searchQuery.isEmpty else { return } do { try await Task.sleep(for: .milliseconds(250)) var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, type: nil, 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 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 } }