Explore: Search

This commit is contained in:
Thomas Ricouard 2022-12-27 10:04:39 +01:00
parent a84d3da19a
commit 816e1d5e7d
6 changed files with 121 additions and 39 deletions

View File

@ -84,7 +84,7 @@ struct SettingsTabs: View {
LabeledContent("Email", value: instanceData.email) LabeledContent("Email", value: instanceData.email)
LabeledContent("Version", value: instanceData.version) LabeledContent("Version", value: instanceData.version)
LabeledContent("Users", value: "\(instanceData.stats.userCount)") LabeledContent("Users", value: "\(instanceData.stats.userCount)")
LabeledContent("Status", value: "\(instanceData.stats.statusCount)") LabeledContent("Posts", value: "\(instanceData.stats.statusCount)")
LabeledContent("Domains", value: "\(instanceData.stats.domainCount)") LabeledContent("Domains", value: "\(instanceData.stats.domainCount)")
} }
} }

View File

@ -18,6 +18,7 @@ public class AccountsListRowViewModel: ObservableObject {
} }
public struct AccountsListRow: View { public struct AccountsListRow: View {
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@ -45,8 +46,10 @@ public struct AccountsListRow: View {
}) })
} }
Spacer() Spacer()
FollowButton(viewModel: .init(accountId: viewModel.account.id, if currentAccount.account?.id != viewModel.account.id {
relationship: viewModel.relationShip)) FollowButton(viewModel: .init(accountId: viewModel.account.id,
relationship: viewModel.relationShip))
}
} }
.onAppear { .onAppear {
viewModel.client = client viewModel.client = client

View File

@ -12,26 +12,29 @@ extension Account {
} }
public var displayNameWithEmojis: some View { public var displayNameWithEmojis: some View {
let splittedDisplayName = displayName.split(separator: ":").map{ Part(value: $0) } let splittedDisplayName = displayName.split(separator: ":").map{ Part(value: $0) }
return HStack(spacing: 0) { return HStack(spacing: 0) {
ForEach(splittedDisplayName, id: \.id) { part in if displayName.isEmpty {
if let emoji = emojis.first(where: { $0.shortcode == part.value }) { Text(" ")
LazyImage(url: emoji.url) { state in }
if let image = state.image { ForEach(splittedDisplayName, id: \.id) { part in
image if let emoji = emojis.first(where: { $0.shortcode == part.value }) {
.resizingMode(.aspectFit) LazyImage(url: emoji.url) { state in
} else if state.isLoading { if let image = state.image {
ProgressView() image
} else { .resizingMode(.aspectFit)
ProgressView() } else if state.isLoading {
} ProgressView()
} } else {
.processors([ImageProcessors.Resize(size: .init(width: 20, height: 20))]) ProgressView()
.frame(width: 20, height: 20) }
} else { }
Text(part.value) .processors([ImageProcessors.Resize(size: .init(width: 20, height: 20))])
} .frame(width: 20, height: 20)
} } else {
} Text(part.value)
} }
}
}
}
} }

View File

@ -18,13 +18,14 @@ public struct ExploreView: View {
public var body: some View { public var body: some View {
List { List {
if !viewModel.isLoaded { if !viewModel.searchQuery.isEmpty {
ForEach(Status.placeholders()) { status in if let results = viewModel.results[viewModel.searchQuery] {
StatusRowView(viewModel: .init(status: status, isEmbed: false)) makeSearchResultsView(results: results)
.padding(.vertical, 8) } else {
.redacted(reason: .placeholder) loadingView
.shimmering()
} }
} else if !viewModel.isLoaded {
loadingView
} else { } else {
trendingTagsSection trendingTagsSection
suggestedAccountsSection suggestedAccountsSection
@ -53,6 +54,44 @@ public struct ExploreView: View {
}) })
} }
private var loadingView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, isEmbed: false))
.padding(.vertical, 8)
.redacted(reason: .placeholder)
.shimmering()
}
}
@ViewBuilder
private func makeSearchResultsView(results: SearchResults) -> some View {
if !results.accounts.isEmpty {
Section("Users") {
ForEach(results.accounts) { account in
if let relationship = results.relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
}
}
}
}
if !results.hashtags.isEmpty {
Section("Tags") {
ForEach(results.hashtags) { tag in
TagRowView(tag: tag)
.padding(.vertical, 4)
}
}
}
if !results.statuses.isEmpty {
Section("Posts") {
ForEach(results.statuses) { status in
StatusRowView(viewModel: .init(status: status))
.padding(.vertical, 8)
}
}
}
}
private var suggestedAccountsSection: some View { private var suggestedAccountsSection: some View {
Section("Suggested Users") { Section("Suggested Users") {
ForEach(viewModel.suggestedAccounts ForEach(viewModel.suggestedAccounts

View File

@ -7,11 +7,24 @@ class ExploreViewModel: ObservableObject {
var client: Client? var client: Client?
enum Token: String, Identifiable { enum Token: String, Identifiable {
case user = "@user", tag = "#hasgtag" case user = "@user"
case statuses = "@posts"
case tag = "#hasgtag"
var id: String { var id: String {
rawValue rawValue
} }
var apiType: String {
switch self {
case .user:
return "accounts"
case .tag:
return "hashtags"
case .statuses:
return "statuses"
}
}
} }
@Published var tokens: [Token] = [] @Published var tokens: [Token] = []
@ -19,11 +32,14 @@ class ExploreViewModel: ObservableObject {
@Published var searchQuery = "" { @Published var searchQuery = "" {
didSet { didSet {
if searchQuery.starts(with: "@") { if searchQuery.starts(with: "@") {
suggestedToken = [.user] suggestedToken = [.user, .statuses]
} else if searchQuery.starts(with: "#") { } else if searchQuery.starts(with: "#") {
suggestedToken = [.tag] suggestedToken = [.tag]
} else if tokens.isEmpty { } else if !tokens.isEmpty {
suggestedToken = [] suggestedToken = []
search()
} else {
search()
} }
} }
} }
@ -35,10 +51,13 @@ class ExploreViewModel: ObservableObject {
@Published var trendingStatuses: [Status] = [] @Published var trendingStatuses: [Status] = []
@Published var trendingLinks: [Card] = [] @Published var trendingLinks: [Card] = []
private var searchTask: Task<Void, Never>?
func fetchTrending() async { func fetchTrending() async {
guard let client else { return } guard let client else { return }
do { do {
isLoaded = false isLoaded = false
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions) async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags) async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses) async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses)
@ -55,10 +74,23 @@ class ExploreViewModel: ObservableObject {
} catch { } } catch { }
} }
func search() async { func search() {
guard let client else { return } guard !searchQuery.isEmpty else { return }
do { searchTask?.cancel()
results[searchQuery] = try await client.get(endpoint: Search.search(query: searchQuery, type: nil, offset: nil), forceVersion: .v2) searchTask = nil
} catch { } searchTask = Task {
guard let client else { return }
do {
let apiType = tokens.first?.apiType
var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
type: apiType,
offset: nil),
forceVersion: .v2)
let relationships: [Relationshionship] =
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map{ $0.id }))
results.relationships = relationships
self.results[searchQuery] = results
} catch { }
}
} }
} }

View File

@ -1,7 +1,12 @@
import Foundation import Foundation
public struct SearchResults: Decodable { public struct SearchResults: Decodable {
enum CodingKeys: String, CodingKey {
case accounts, statuses, hashtags
}
public let accounts: [Account] public let accounts: [Account]
public var relationships: [Relationshionship] = []
public let statuses: [Status] public let statuses: [Status]
public let hashtags: [Tag] public let hashtags: [Tag]
} }