// // 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? @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? 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() 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) } } } }