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:
parent
3ac1bf362b
commit
f326bbefe6
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ struct AddRemoteTimelineView: View {
|
|||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
instances = await client.fetchInstances()
|
||||
instances = await client.fetchInstances(keyword: instanceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue