Fix: Search Instances Feature (#1766)

* fix: search logic and performance

* Remove overlay

---------

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Thai D. V 2023-12-26 19:31:22 +07:00 committed by GitHub
parent 3ac1bf362b
commit f326bbefe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 34 deletions

View file

@ -28,6 +28,9 @@ struct AddAccountView: View {
@State private var signInClient: Client? @State private var signInClient: Client?
@State private var instances: [InstanceSocial] = [] @State private var instances: [InstanceSocial] = []
@State private var instanceFetchError: LocalizedStringKey? @State private var instanceFetchError: LocalizedStringKey?
@State private var instanceSocialClient = InstanceSocialClient()
@State private var searchingTask = Task<Void, Never> {}
@State private var getInstanceDetailTask = Task<Void, Never> {}
private let instanceNamePublisher = PassthroughSubject<String, Never>() private let instanceNamePublisher = PassthroughSubject<String, Never>()
@ -93,28 +96,39 @@ struct AddAccountView: View {
} }
.onAppear { .onAppear {
isInstanceURLFieldFocused = true isInstanceURLFieldFocused = true
let client = InstanceSocialClient()
Task { Task {
let instances = await client.fetchInstances() let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation { withAnimation {
self.instances = instances self.instances = instances
} }
} }
isSigninIn = false isSigninIn = false
} }
.onChange(of: instanceName) { _, newValue in .onChange(of: instanceName) {
instanceNamePublisher.send(newValue) searchingTask.cancel()
searchingTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
} }
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in }
// let newValue = newValue
// .replacingOccurrences(of: "http://", with: "") getInstanceDetailTask.cancel()
// .replacingOccurrences(of: "https://", with: "") getInstanceDetailTask = Task {
let client = Client(server: sanitizedName) try? await Task.sleep(for: .seconds(0.1))
Task { guard !Task.isCancelled else { return }
do { do {
// bare bones preflight for domain validity // bare bones preflight for domain validity
if client.server.contains("."), client.server.last != "." { let instanceDetailClient = Client(server: sanitizedName)
let instance: Instance = try await client.get(endpoint: Instances.instance) if
instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation { withAnimation {
self.instance = instance self.instance = instance
instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
@ -178,7 +192,7 @@ struct AddAccountView: View {
if instances.isEmpty { if instances.isEmpty {
placeholderRow placeholderRow
} else { } else {
ForEach(sanitizedName.isEmpty ? instances : instances.filter { $0.name.contains(sanitizedName.lowercased()) }) { instance in ForEach(instances) { instance in
Button { Button {
instanceName = instance.name instanceName = instance.name
} label: { } label: {
@ -221,13 +235,8 @@ struct AddAccountView: View {
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)) .listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.clipShape(RoundedRectangle(cornerRadius: 5)) .clipShape(RoundedRectangle(cornerRadius: 4))
#endif #endif
.overlay {
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 1)
.fill(theme.tintColor)
}
} }
} }
} }

View file

@ -75,7 +75,7 @@ struct AddRemoteTimelineView: View {
isInstanceURLFieldFocused = true isInstanceURLFieldFocused = true
let client = InstanceSocialClient() let client = InstanceSocialClient()
Task { Task {
instances = await client.fetchInstances() instances = await client.fetchInstances(keyword: instanceName)
} }
} }
} }

View file

@ -3,7 +3,8 @@ import Models
public struct InstanceSocialClient { public struct InstanceSocialClient {
private let authorization = "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML" private let authorization = "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML"
private let endpoint = URL(string: "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500")! private let listEndpoint = "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500"
private let searchEndpoint = "https://instances.social/api/1.0/instances/search"
struct Response: Decodable { struct Response: Decodable {
let instances: [InstanceSocial] let instances: [InstanceSocial]
@ -11,17 +12,62 @@ public struct InstanceSocialClient {
public init() {} public init() {}
public func fetchInstances() async -> [InstanceSocial] { public func fetchInstances(keyword: String) async -> [InstanceSocial] {
do { let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
let endpoint = keyword.isEmpty ? listEndpoint : searchEndpoint + "?q=\(keyword)"
guard let url = URL(string: endpoint) else { return [] }
var request = URLRequest(url: url)
request.setValue(authorization, forHTTPHeaderField: "Authorization")
guard let (data, _) = try? await URLSession.shared.data(for: request) else { return [] }
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
var request: URLRequest = .init(url: endpoint)
request.setValue(authorization, forHTTPHeaderField: "Authorization") guard let response = try? decoder.decode(Response.self, from: data) else { return [] }
let (data, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: data) let result = response.instances.sorted(by: keyword)
return response.instances return result
} catch { }
return [] }
}
extension Array where Self.Element == InstanceSocial {
fileprivate func sorted(by keyword: String) -> Self {
let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
var newArray = self
newArray.sort { (lhs: InstanceSocial, rhs: InstanceSocial) in
guard
let lhsNumber = Int(lhs.users),
let rhsNumber = Int(rhs.users)
else { return false }
return lhsNumber > rhsNumber
}
newArray.sort { (lhs: InstanceSocial, rhs: InstanceSocial) in
guard
let lhsNumber = Int(lhs.statuses),
let rhsNumber = Int(rhs.statuses)
else { return false }
return lhsNumber > rhsNumber
}
if !keyword.isEmpty {
newArray.sort { (lhs: InstanceSocial, rhs: InstanceSocial) in
if
lhs.name.contains(keyword),
!rhs.name.contains(keyword)
{ return true }
return false
}
}
return newArray
} }
} }