125 lines
4.3 KiB
Swift
125 lines
4.3 KiB
Swift
|
//
|
||
|
// AutocompleteHashtagsController.swift
|
||
|
// ComposeUI
|
||
|
//
|
||
|
// Created by Shadowfacts on 4/1/23.
|
||
|
//
|
||
|
|
||
|
import SwiftUI
|
||
|
import Combine
|
||
|
import Pachyderm
|
||
|
|
||
|
class AutocompleteHashtagsController: ViewController {
|
||
|
unowned let composeController: ComposeController
|
||
|
var mastodonController: ComposeMastodonContext { composeController.mastodonController }
|
||
|
|
||
|
private var stateCancellable: AnyCancellable?
|
||
|
private var searchTask: Task<Void, Never>?
|
||
|
|
||
|
@Published var hashtags: [Hashtag] = []
|
||
|
|
||
|
init(composeController: ComposeController) {
|
||
|
self.composeController = composeController
|
||
|
|
||
|
stateCancellable = composeController.$currentInput
|
||
|
.compactMap { $0 }
|
||
|
.flatMap { $0.autocompleteStatePublisher }
|
||
|
.compactMap {
|
||
|
if case .hashtag(let s) = $0 {
|
||
|
return s
|
||
|
} else {
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
|
||
|
.sink { [unowned self] query in
|
||
|
self.searchTask?.cancel()
|
||
|
self.searchTask = Task {
|
||
|
await self.queryChanged(query)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@MainActor
|
||
|
private func queryChanged(_ query: String) async {
|
||
|
guard !query.isEmpty else {
|
||
|
hashtags = []
|
||
|
return
|
||
|
}
|
||
|
|
||
|
let localHashtags = mastodonController.searchCachedHashtags(query: query)
|
||
|
|
||
|
var onlyLocalTagsTask: Task<Void, any Error>?
|
||
|
if !localHashtags.isEmpty {
|
||
|
onlyLocalTagsTask = Task {
|
||
|
// we only want to do the local-only search if the trends API call takes more than .25sec or it fails
|
||
|
try await Task.sleep(nanoseconds: 250 * NSEC_PER_MSEC)
|
||
|
self.updateHashtags(searchResults: [], trendingTags: [], localHashtags: localHashtags, query: query)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async let trendingTags = try? mastodonController.run(Client.getTrendingHashtags()).0
|
||
|
async let searchResults = try? mastodonController.run(Client.search(query: "#\(query)", types: [.hashtags])).0.hashtags
|
||
|
|
||
|
let trends = await trendingTags ?? []
|
||
|
let search = await searchResults ?? []
|
||
|
|
||
|
onlyLocalTagsTask?.cancel()
|
||
|
guard !Task.isCancelled else { return }
|
||
|
|
||
|
updateHashtags(searchResults: search, trendingTags: trends, localHashtags: localHashtags, query: query)
|
||
|
}
|
||
|
|
||
|
@MainActor
|
||
|
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], localHashtags: [Hashtag], query: String) {
|
||
|
var addedHashtags = Set<String>()
|
||
|
var hashtags = [(Hashtag, Int)]()
|
||
|
for group in [searchResults, trendingTags, localHashtags] {
|
||
|
for tag in group where !addedHashtags.contains(tag.name) {
|
||
|
let (matched, score) = FuzzyMatcher.match(pattern: query, str: tag.name)
|
||
|
if matched {
|
||
|
hashtags.append((tag, score))
|
||
|
addedHashtags.insert(tag.name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
self.hashtags = hashtags
|
||
|
.sorted { $0.1 > $1.1 }
|
||
|
.map(\.0)
|
||
|
}
|
||
|
|
||
|
private func autocomplete(with hashtag: Hashtag) {
|
||
|
guard let currentInput = composeController.currentInput else { return }
|
||
|
currentInput.autocomplete(with: "#\(hashtag.name)")
|
||
|
}
|
||
|
|
||
|
var view: some View {
|
||
|
AutocompleteHashtagsView()
|
||
|
}
|
||
|
|
||
|
struct AutocompleteHashtagsView: View {
|
||
|
@EnvironmentObject private var controller: AutocompleteHashtagsController
|
||
|
|
||
|
var body: some View {
|
||
|
ScrollView(.horizontal) {
|
||
|
HStack(spacing: 8) {
|
||
|
ForEach(controller.hashtags, id: \.name) { hashtag in
|
||
|
Button(action: { controller.autocomplete(with: hashtag) }) {
|
||
|
Text(verbatim: "#\(hashtag.name)")
|
||
|
.foregroundColor(Color(uiColor: .label))
|
||
|
}
|
||
|
.frame(height: 30)
|
||
|
.padding(.vertical, 8)
|
||
|
}
|
||
|
|
||
|
Spacer()
|
||
|
}
|
||
|
.padding(.horizontal, 8)
|
||
|
.animation(.linear(duration: 0.2), value: controller.hashtags)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
}
|