Files
andros 2631b0d942 Auto-refresh own profile and timeline after any feed mutation
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.
2026-04-21 11:09:03 +02:00

98 lines
3.7 KiB
Swift

import SwiftUI
import OrgSocialKit
/// Posts a `:MIGRATION:` entry to the user's feed, telling followers the feed has moved.
/// Per the Org Social spec, a migration post has body `"<old-url> <new-url>"` and is
/// indistinguishable from a regular post except that clients highlight it and should
/// prefer following the new URL going forward.
struct MigrationSheet: View {
@Environment(\.dismiss) private var dismiss
@AppStorage("publicFeedURL") private var publicFeedURL = ""
@State private var newURL = ""
@State private var isPosting = false
@State private var errorMessage: String?
@State private var finished = false
var body: some View {
NavigationStack {
Form {
Section {
HStack {
Text("Current URL").foregroundStyle(.secondary)
Spacer()
Text(publicFeedURL)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
}
Section("New feed URL") {
TextField("https://new.example.com/social.org", text: $newURL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.URL)
}
if let errorMessage {
Section {
Label(errorMessage, systemImage: "exclamationmark.triangle")
.font(.footnote)
.foregroundStyle(.red)
}
}
if finished {
Section {
Label("Migration announced", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
}
}
}
.navigationTitle("Migrate account")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
if isPosting {
ProgressView()
} else {
Button("Post") { Task { await post() } }
.disabled(!canPost)
}
}
}
}
}
private var canPost: Bool {
!publicFeedURL.isEmpty
&& URL(string: publicFeedURL) != nil
&& URL(string: newURL) != nil
&& !isPosting
}
private func post() async {
isPosting = true
errorMessage = nil
defer { isPosting = false }
guard let feedURL = URL(string: publicFeedURL),
let uploader = UploaderFactory.makeUploader() else {
errorMessage = "Configure your vfile (or other upload method) first."
return
}
do {
let content = try await FeedFetcher().fetch(from: feedURL, bypassCache: true)
let options = NewPostOptions.migration(from: publicFeedURL, to: newURL)
let (updated, _) = try PostWriter().appendPost(to: content, feedURL: feedURL, options: options)
try await uploader.upload(content: updated)
FollowCoordinator.shared.updateCachedContent(updated)
finished = true
try? await Task.sleep(for: .seconds(1))
dismiss()
} catch {
errorMessage = error.localizedDescription
}
}
}