Files
andros a190d507ed Add delete/edit post, search, notification actions, mood/client display, profile groups, group compose
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
2026-04-20 07:57:10 +02:00

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 }
}
}