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 instances: [InstanceSocial] = []
@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>()
@ -93,28 +96,39 @@ struct AddAccountView: View {
}
.onAppear {
isInstanceURLFieldFocused = true
let client = InstanceSocialClient()
Task {
let instances = await client.fetchInstances()
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
}
isSigninIn = false
}
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in
// let newValue = newValue
// .replacingOccurrences(of: "http://", with: "")
// .replacingOccurrences(of: "https://", with: "")
let client = Client(server: sanitizedName)
Task {
.onChange(of: instanceName) {
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
}
}
getInstanceDetailTask.cancel()
getInstanceDetailTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
do {
// bare bones preflight for domain validity
if client.server.contains("."), client.server.last != "." {
let instance: Instance = try await client.get(endpoint: Instances.instance)
let instanceDetailClient = Client(server: sanitizedName)
if
instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation {
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
@ -178,7 +192,7 @@ struct AddAccountView: View {
if instances.isEmpty {
placeholderRow
} else {
ForEach(sanitizedName.isEmpty ? instances : instances.filter { $0.name.contains(sanitizedName.lowercased()) }) { instance in
ForEach(instances) { instance in
Button {
instanceName = instance.name
} label: {
@ -221,13 +235,8 @@ struct AddAccountView: View {
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
.listRowSeparator(.hidden)
.clipShape(RoundedRectangle(cornerRadius: 5))
.clipShape(RoundedRectangle(cornerRadius: 4))
#endif
.overlay {
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 1)
.fill(theme.tintColor)
}
}
}
}

View file

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

View file

@ -3,7 +3,8 @@ import Models
public struct InstanceSocialClient {
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 {
let instances: [InstanceSocial]
@ -11,17 +12,62 @@ public struct InstanceSocialClient {
public init() {}
public func fetchInstances() async -> [InstanceSocial] {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
var request: URLRequest = .init(url: endpoint)
request.setValue(authorization, forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: data)
return response.instances
} catch {
return []
}
public func fetchInstances(keyword: String) async -> [InstanceSocial] {
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()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let response = try? decoder.decode(Response.self, from: data) else { return [] }
let result = response.instances.sorted(by: keyword)
return result
}
}
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
}
}