a190d507ed
Library: - PostWriter.deletePost and editPost (by heading or :ID: timestamp) - PostWriterError.postNotFound - RelayClient.searchPostURLs for /search/?q= and /search/?tag= - 15 new tests (132 total, all passing) App: - Swipe left on own posts reveals Delete (with confirmation) and Edit actions - EditPostView sheet pre-filled with current body; preserves all properties - PostActionsViewModel handles fetch→mutate→upload flow - SearchView + SearchViewModel: full-text and tag search via relay - Search accessible via magnifying glass in Timeline toolbar - NotificationsView: summary count header, View Thread and Reply buttons per row - PostRowView: mood emoji shown on regular posts, client name shown as "via …" - ProfileView: groups section from #+GROUP: entries - GroupsView: compose button for group posts - ComposeView/ViewModel: group parameter support, "Group Post" title
72 lines
2.6 KiB
Swift
72 lines
2.6 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OrgSocialKit
|
|
|
|
@Observable @MainActor
|
|
final class SearchViewModel {
|
|
|
|
var query = ""
|
|
var isTagSearch = false
|
|
var results: [OrgSocialPost] = []
|
|
var isLoading = false
|
|
var errorMessage: String?
|
|
|
|
private let relayClient = RelayClient()
|
|
private let fetcher = FeedFetcher()
|
|
|
|
var relayURL: URL {
|
|
let raw = UserDefaults.standard.string(forKey: "relayURL") ?? "https://relay.org-social.org"
|
|
return URL(string: raw) ?? URL(string: "https://relay.org-social.org")!
|
|
}
|
|
|
|
func search() async {
|
|
let trimmed = query.trimmingCharacters(in: .whitespaces)
|
|
guard !trimmed.isEmpty else { return }
|
|
isLoading = true
|
|
errorMessage = nil
|
|
results = []
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let postURLs = try await relayClient.searchPostURLs(query: trimmed, isTag: isTagSearch, from: relayURL)
|
|
results = try await resolvePosts(from: postURLs)
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func resolvePosts(from postURLs: [String]) async throws -> [OrgSocialPost] {
|
|
var byFeed: [String: [String]] = [:]
|
|
for postURL in postURLs {
|
|
guard let hashIdx = postURL.lastIndex(of: "#") else { continue }
|
|
let feedStr = String(postURL[..<hashIdx])
|
|
byFeed[feedStr, default: []].append(postURL)
|
|
}
|
|
|
|
var resolved: [OrgSocialPost] = []
|
|
try await withThrowingTaskGroup(of: [OrgSocialPost].self) { group in
|
|
for (feedStr, urls) in byFeed {
|
|
guard let feedURL = URL(string: feedStr) else { continue }
|
|
let timestamps = Set(urls.compactMap { $0.components(separatedBy: "#").last })
|
|
group.addTask { [fetcher] in
|
|
guard let content = try? await fetcher.fetch(from: feedURL) else { return [] }
|
|
var profile = OrgSocialParser().parse(content)
|
|
profile.feedURL = feedURL
|
|
return profile.posts.filter { timestamps.contains($0.timestamp) }.map { post in
|
|
var p = post
|
|
p.authorNick = profile.nick
|
|
p.authorURL = feedURL
|
|
p.authorAvatar = profile.avatar
|
|
p.feedURL = feedURL
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
for try await posts in group {
|
|
resolved.append(contentsOf: posts)
|
|
}
|
|
}
|
|
return resolved.sorted { $0.date > $1.date }
|
|
}
|
|
}
|