bc29cda05a
Relay=false degradation:
- Notifications, Discover, Groups and Thread ViewModels now bail with a "X requires a relay. Enable Use Relay in Settings." message instead of firing requests that would fail.
- PostRowView.fetchInteractionData skips the /replies/ and /interactions/ calls when the relay is disabled so timeline rows stop emitting background noise.
- SearchView.onSubmit also respects useRelay. TimelineView already fell back to the follow list via TimelineFetcher.
Profile edit UX:
- ProfileView reloads via a new ProfileViewModel.reload() (bypassCache=true) on the Edit Profile sheet's onDismiss, so header/description edits show up instantly instead of waiting for a tab switch.
- EditProfileViewModel now also bypassCache on both load and save, so the form reflects the authoritative feed and save patches fresh content rather than a stale CDN copy.
- ProfileFetcher.fetch gains a bypassCache parameter forwarded to FeedFetcher.
Pin / Unpin own post:
- Context menu on an own post exposes "Pin to profile" and "Unpin" actions.
- PostActionsViewModel.pinPost/unpinPost rewrite the feed's #+PINNED keyword through ProfileWriter.setKeyword/removeKeyword and upload.
Migration announcement:
- New Account section in Settings with "Announce account migration" button.
- MigrationSheet lets the user enter the new feed URL and posts a :MIGRATION: <old> <new> entry through NewPostOptions.migration and PostWriter.appendPost.
Settings test-connection:
- Each upload method section (VFile / GitHub / Codeberg / WebDAV) shows a "Test connection" button that GETs the Public Feed URL with cache bypassed and parses it. Shows the parsed nick on success; inline error on failure. This catches the common misconfiguration of pasting a wrong URL without exercising a destructive upload.
Miscellaneous:
- Thread and Relay clients get a 10s request timeout so slow or unreachable hosts can't pin the UI on spinner for the 60s URLSession default.
- TimelineView switched to ScrollView + LazyVStack (from List) to avoid SwiftUI List rows auto-firing the first NavigationLink on any tap — the underlying cause of "back from a thread lands on a profile" reports.
- Removed the opacity(0.7) on thread ancestors so they no longer look washed out.
- PostRowView action-bar buttons gained explicit accessibilityLabel ("Reply", "React", "Boost", "Boosted") in place of raw SF Symbol names.
- PostRowView body text is now a NavigationLink into the thread when the post has replies or is itself a reply.
- TODO.org captures the full QA round.
58 lines
2.0 KiB
Swift
58 lines
2.0 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OrgSocialKit
|
|
|
|
@Observable @MainActor
|
|
final class NotificationsViewModel {
|
|
|
|
var notifications: [OrgSocialNotification] = []
|
|
var isLoading = false
|
|
var errorMessage: String?
|
|
|
|
private let client = RelayClient()
|
|
|
|
var feedURL: URL? {
|
|
guard let raw = UserDefaults.standard.string(forKey: "publicFeedURL"),
|
|
let url = URL(string: raw),
|
|
url.scheme?.hasPrefix("http") == true else { return nil }
|
|
return url
|
|
}
|
|
|
|
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")!
|
|
}
|
|
|
|
var useRelay: Bool {
|
|
UserDefaults.standard.object(forKey: "useRelay") as? Bool ?? true
|
|
}
|
|
|
|
func load() async {
|
|
guard !isLoading else { return }
|
|
guard useRelay else {
|
|
errorMessage = "Notifications require a relay. Enable Use Relay in Settings."
|
|
notifications = []
|
|
return
|
|
}
|
|
guard let feedURL else {
|
|
errorMessage = "Configure your Public Feed URL in Settings."
|
|
return
|
|
}
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
do {
|
|
let fetched = try await client.fetchNotifications(for: feedURL, from: relayURL)
|
|
// Deduplicate by ID to prevent ForEach from receiving duplicate keys,
|
|
// which causes UICollectionView item-count mismatches and crashes.
|
|
var seen = Set<String>()
|
|
notifications = fetched.filter { seen.insert($0.id).inserted }
|
|
} catch RelayError.httpError(let code) where code == 404 {
|
|
try? await client.registerFeed(feedURL, with: relayURL)
|
|
errorMessage = "Your feed isn't indexed by the relay yet. It will be available in a few minutes."
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|