2631b0d942
Own Profile and Timeline kept stale views between tab switches because SwiftUI's TabView preserves state, so .task runs only once. Now every write path (compose, edit, delete, react, boost, vote, follow/unfollow, profile edit, migration, pin/unpin) hands the new feed content to FollowCoordinator, which bumps a feedVersion counter. Profile and Timeline observe that counter and re-fetch. Also align GitHub and Codeberg commit messages with the shortened "via iOS" client tag.
129 lines
4.8 KiB
Swift
129 lines
4.8 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OrgSocialKit
|
|
|
|
@Observable @MainActor
|
|
final class EditProfileViewModel {
|
|
|
|
var nick = ""
|
|
var title = ""
|
|
var description = ""
|
|
var avatar = ""
|
|
var location = ""
|
|
var birthday = ""
|
|
var language = ""
|
|
var linksText = ""
|
|
var contactsText = ""
|
|
|
|
var isSaving = false
|
|
var errorMessage: String?
|
|
var saved = false
|
|
|
|
private let pw = ProfileWriter()
|
|
|
|
var isLoading = false
|
|
|
|
init() { loadFromUserDefaults() }
|
|
|
|
func load() async {
|
|
guard let url = ownFeedURL else { return }
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
// bypassCache so the edit form reflects the real feed, not a stale CDN copy.
|
|
guard let content = try? await FeedFetcher().fetch(from: url, bypassCache: true) else { return }
|
|
loadFromContent(content)
|
|
}
|
|
|
|
func save() async {
|
|
guard let feedURL = ownFeedURL, let uploader = makeUploader() else {
|
|
errorMessage = "Configure your feed in Settings before editing your profile."
|
|
return
|
|
}
|
|
isSaving = true
|
|
errorMessage = nil
|
|
defer { isSaving = false }
|
|
|
|
do {
|
|
// bypassCache so we patch the latest authoritative content, not a stale cache.
|
|
var content = try await FeedFetcher().fetch(from: feedURL, bypassCache: true)
|
|
|
|
content = pw.setKeyword("NICK", value: nick, in: content)
|
|
content = pw.setKeyword("TITLE", value: title, in: content)
|
|
if description.isEmpty {
|
|
content = pw.removeKeyword("DESCRIPTION", from: content)
|
|
} else {
|
|
content = pw.setKeyword("DESCRIPTION", value: description, in: content)
|
|
}
|
|
if avatar.isEmpty {
|
|
content = pw.removeKeyword("AVATAR", from: content)
|
|
} else {
|
|
content = pw.setKeyword("AVATAR", value: avatar, in: content)
|
|
}
|
|
if location.isEmpty {
|
|
content = pw.removeKeyword("LOCATION", from: content)
|
|
} else {
|
|
content = pw.setKeyword("LOCATION", value: location, in: content)
|
|
}
|
|
if birthday.isEmpty {
|
|
content = pw.removeKeyword("BIRTHDAY", from: content)
|
|
} else {
|
|
content = pw.setKeyword("BIRTHDAY", value: birthday, in: content)
|
|
}
|
|
if language.isEmpty {
|
|
content = pw.removeKeyword("LANGUAGE", from: content)
|
|
} else {
|
|
content = pw.setKeyword("LANGUAGE", value: language, in: content)
|
|
}
|
|
|
|
let links = linksText.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
|
|
content = pw.setMultiKeyword("LINK", values: links, in: content)
|
|
|
|
let contacts = contactsText.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
|
|
content = pw.setMultiKeyword("CONTACT", values: contacts, in: content)
|
|
|
|
try await uploader.upload(content: content)
|
|
FollowCoordinator.shared.updateCachedContent(content)
|
|
saved = true
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
// MARK: - Private helpers
|
|
|
|
private func loadFromUserDefaults() {
|
|
nick = UserDefaults.standard.string(forKey: "nick") ?? ""
|
|
}
|
|
|
|
private func loadFromContent(_ content: String) {
|
|
// Quick parse: read header keyword values
|
|
for line in content.components(separatedBy: "\n") {
|
|
let t = line.trimmingCharacters(in: .whitespaces)
|
|
if t == "* Posts" { break }
|
|
guard t.hasPrefix("#+"), let colonIdx = t.firstIndex(of: ":") else { continue }
|
|
let kw = String(t[t.index(t.startIndex, offsetBy: 2)..<colonIdx]).uppercased()
|
|
let val = String(t[t.index(after: colonIdx)...]).trimmingCharacters(in: .whitespaces)
|
|
switch kw {
|
|
case "NICK": nick = val
|
|
case "TITLE": title = val
|
|
case "DESCRIPTION": description = val
|
|
case "AVATAR": avatar = val
|
|
case "LOCATION": location = val
|
|
case "BIRTHDAY": birthday = val
|
|
case "LANGUAGE": language = val
|
|
case "LINK": linksText += (linksText.isEmpty ? "" : "\n") + val
|
|
case "CONTACT": contactsText += (contactsText.isEmpty ? "" : "\n") + val
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
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 func makeUploader() -> (any FeedUploader)? { UploaderFactory.makeUploader() }
|
|
}
|