diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift index ec1a106c..33c1b9e0 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift @@ -150,7 +150,7 @@ public enum AccountsListMode { state = .loading try await Task.sleep(for: .milliseconds(250)) var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, - type: "accounts", + type: .accounts, offset: nil, following: true), forceVersion: .v2) diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 065fba9e..0f76def0 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -260,7 +260,7 @@ public enum SettingsStartingPoint { public func navigateToAccountFrom(acct: String, url: URL) async { guard let client else { return } let results: SearchResults? = try? await client.get(endpoint: Search.search(query: acct, - type: "accounts", + type: .accounts, offset: nil, following: nil), forceVersion: .v2) @@ -274,7 +274,7 @@ public enum SettingsStartingPoint { public func navigateToAccountFrom(url: URL) async { guard let client else { return } let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString, - type: "accounts", + type: .accounts, offset: nil, following: nil), forceVersion: .v2) diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index 0675cde5..4a02ac70 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -181,6 +181,9 @@ public struct ExploreView: View { #endif } } + if viewModel.searchScope == .people { + makeNextPageView(for: .accounts) + } } } if !results.hashtags.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .hashtags { @@ -196,6 +199,9 @@ public struct ExploreView: View { #endif .padding(.vertical, 4) } + if viewModel.searchScope == .hashtags { + makeNextPageView(for: .hashtags) + } } } if !results.statuses.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .posts { @@ -211,6 +217,9 @@ public struct ExploreView: View { #endif .padding(.vertical, 8) } + if viewModel.searchScope == .posts { + makeNextPageView(for: .statuses) + } } } } @@ -345,4 +354,14 @@ public struct ExploreView: View { viewModel.scrollToTopVisible = false } } + + private func makeNextPageView(for type: Search.EntityType) -> some View { + NextPageView { + await viewModel.fetchNextPage(of: type) + } + .padding(.horizontal, .layoutPadding) + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif + } } diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index f9f314ee..cb36f77c 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -115,4 +115,42 @@ import SwiftUI isSearching = false } } + + func fetchNextPage(of type: Search.EntityType) async { + guard let client, !searchQuery.isEmpty, + let results = results[searchQuery] else { return } + do { + let offset = switch type { + case .accounts: + results.accounts.count + case .hashtags: + results.hashtags.count + case .statuses: + results.statuses.count + } + + var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, + type: type, + offset: offset, + following: nil), + forceVersion: .v2) + if type == .accounts { + let relationships: [Relationship] = + try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id))) + newPageResults.relationships = relationships + } + + switch type { + case .accounts: + self.results[searchQuery]?.accounts.append(contentsOf: newPageResults.accounts) + self.results[searchQuery]?.relationships.append(contentsOf: newPageResults.relationships) + case .hashtags: + self.results[searchQuery]?.hashtags.append(contentsOf: newPageResults.hashtags) + case .statuses: + self.results[searchQuery]?.statuses.append(contentsOf: newPageResults.statuses) + } + } catch { + + } + } } diff --git a/Packages/Models/Sources/Models/SearchResults.swift b/Packages/Models/Sources/Models/SearchResults.swift index 6c8514c2..69855074 100644 --- a/Packages/Models/Sources/Models/SearchResults.swift +++ b/Packages/Models/Sources/Models/SearchResults.swift @@ -5,10 +5,10 @@ public struct SearchResults: Decodable { case accounts, statuses, hashtags } - public let accounts: [Account] + public var accounts: [Account] public var relationships: [Relationship] = [] - public let statuses: [Status] - public let hashtags: [Tag] + public var statuses: [Status] + public var hashtags: [Tag] public var isEmpty: Bool { accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty diff --git a/Packages/Network/Sources/Network/Endpoint/Search.swift b/Packages/Network/Sources/Network/Endpoint/Search.swift index 69664578..1b1b5a4a 100644 --- a/Packages/Network/Sources/Network/Endpoint/Search.swift +++ b/Packages/Network/Sources/Network/Endpoint/Search.swift @@ -1,8 +1,12 @@ import Foundation public enum Search: Endpoint { - case search(query: String, type: String?, offset: Int?, following: Bool?) - case accountsSearch(query: String, type: String?, offset: Int?, following: Bool?) + public enum EntityType: String, Sendable { + case accounts, hashtags, statuses + } + + case search(query: String, type: EntityType?, offset: Int?, following: Bool?) + case accountsSearch(query: String, type: EntityType?, offset: Int?, following: Bool?) public func path() -> String { switch self { @@ -19,7 +23,7 @@ public enum Search: Endpoint { let .accountsSearch(query, type, offset, following): var params: [URLQueryItem] = [.init(name: "q", value: query)] if let type { - params.append(.init(name: "type", value: type)) + params.append(.init(name: "type", value: type.rawValue)) } if let offset { params.append(.init(name: "offset", value: String(offset))) diff --git a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift index 76f9c1c7..9fcbdfe2 100644 --- a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift @@ -58,7 +58,7 @@ import SwiftUI private func fetchRemoteStatus() async -> Bool { guard let client, let remoteStatusURL else { return false } let results: SearchResults? = try? await client.get(endpoint: Search.search(query: remoteStatusURL.absoluteString, - type: "statuses", + type: .statuses, offset: nil, following: nil), forceVersion: .v2) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index f0070665..5f570f39 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -591,7 +591,7 @@ public extension StatusEditor { showRecentsTagsInline = false query.removeFirst() results = try await client.get(endpoint: Search.search(query: query, - type: "hashtags", + type: .hashtags, offset: 0, following: nil), forceVersion: .v2) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index eceb66cc..cac0d16e 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -280,7 +280,7 @@ import SwiftUI embed = try await client.get(endpoint: Statuses.status(id: String(id))) } else { let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString, - type: "statuses", + type: .statuses, offset: 0, following: nil), forceVersion: .v2) @@ -449,7 +449,7 @@ import SwiftUI guard isRemote, let remoteStatusURL = URL(string: finalStatus.url ?? "") else { return false } isLoadingRemoteContent = true let results: SearchResults? = try? await client.get(endpoint: Search.search(query: remoteStatusURL.absoluteString, - type: "statuses", + type: .statuses, offset: nil, following: nil), forceVersion: .v2)