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.
98 lines
3.7 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|