import Foundation import Observation import OrgSocialKit @Observable @MainActor final class PostActionsViewModel { var isDeleting = false var isEditing = false var errorMessage: String? var didDelete = false private let writer = PostWriter() private let fetcher = FeedFetcher() func deletePost(_ post: OrgSocialPost) async { guard let feedURL = ownFeedURL, let uploader = ownUploader else { return } isDeleting = true errorMessage = nil defer { isDeleting = false } do { // bypassCache: raw Git host CDNs (raw.githubusercontent, codeberg.org/raw, // host.org-social.org) can serve stale content after a recent upload; without // this the post lookup would fail with "No post with that timestamp was found". let content = try await fetcher.fetch(from: feedURL, bypassCache: true) var updated = try writer.deletePost(timestamp: post.timestamp, from: content) // Clear #+PINNED if it still points at the post we just deleted, // otherwise other clients render a broken pin reference. if OrgSocialParser().parse(content).pinned == post.timestamp { updated = ProfileWriter().removeKeyword("PINNED", from: updated) } try await uploader.upload(content: updated) FollowCoordinator.shared.updateCachedContent(updated) didDelete = true } catch { errorMessage = error.localizedDescription } } func editPost(_ post: OrgSocialPost, newText: String) async { await editPost(post, newText: newText, newTimestamp: nil, lang: nil, tags: nil, mood: nil, visibility: .public) } /// Full-fidelity edit that updates text plus LANG, TAGS, MOOD, VISIBILITY and optionally the timestamp. func editPost( _ post: OrgSocialPost, newText: String, newTimestamp: String?, lang: String?, tags: String?, mood: String?, visibility: NewPostOptions.Visibility ) async { guard let feedURL = ownFeedURL, let uploader = ownUploader else { return } isEditing = true errorMessage = nil defer { isEditing = false } do { // bypassCache: raw Git host CDNs (raw.githubusercontent, codeberg.org/raw, // host.org-social.org) can serve stale content after a recent upload; without // this the post lookup would fail with "No post with that timestamp was found". let content = try await fetcher.fetch(from: feedURL, bypassCache: true) var updated = try writer.editPost( timestamp: post.timestamp, in: content, newText: newText, newTimestamp: newTimestamp, lang: lang, tags: tags, mood: mood, visibility: visibility ) // If the edit moved the post to a new timestamp, retarget PINNED // so it still points at the edited post instead of a ghost. if let newTs = newTimestamp, OrgSocialParser().parse(content).pinned == post.timestamp { updated = ProfileWriter().setKeyword("PINNED", value: newTs, in: updated) } try await uploader.upload(content: updated) FollowCoordinator.shared.updateCachedContent(updated) } catch { errorMessage = error.localizedDescription } } /// Sets `#+PINNED:` to the given post's timestamp, replacing any previous pin. func pinPost(_ post: OrgSocialPost) async { guard let feedURL = ownFeedURL, let uploader = ownUploader else { return } errorMessage = nil do { let content = try await fetcher.fetch(from: feedURL, bypassCache: true) let updated = ProfileWriter().setKeyword("PINNED", value: post.timestamp, in: content) try await uploader.upload(content: updated) FollowCoordinator.shared.updateCachedContent(updated) } catch { errorMessage = error.localizedDescription } } /// Removes `#+PINNED:` from the feed. func unpinPost() async { guard let feedURL = ownFeedURL, let uploader = ownUploader else { return } errorMessage = nil do { let content = try await fetcher.fetch(from: feedURL, bypassCache: true) let updated = ProfileWriter().removeKeyword("PINNED", from: content) try await uploader.upload(content: updated) FollowCoordinator.shared.updateCachedContent(updated) } catch { errorMessage = error.localizedDescription } } private var ownFeedURL: URL? { guard let raw = UserDefaults.standard.string(forKey: "publicFeedURL"), let url = URL(string: raw), url.scheme?.hasPrefix("http") == true else { return nil } return url } private var ownUploader: (any FeedUploader)? { UploaderFactory.makeUploader() } }