Explore: Search
This commit is contained in:
parent
a84d3da19a
commit
816e1d5e7d
|
@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue