mirror of
https://github.com/yattee/yattee.git
synced 2026-10-29 07:57:17 +00:00
Serialize playlist mutations and re-resolve on credential change
Two changes to a server-side playlist could overlap: the second planned its write from the snapshot the first had already invalidated, and on Piped — where a row is a position — its preflight then passed over whatever had shifted into the slot. Mutations now hold a per-playlist queue from the write to the end of the refetch that reconciles it. Creation queues on the account's list instead: it depends on no playlist's contents, and one queue per account would only make unrelated changes wait. The playlist detail view kept the authenticated snapshot in its own state, so signing in as somebody else — or a change of Apple account — left the previous login's rows, title and destructive affordances on screen. It now discards them the moment either lands and loads again, which re-resolves ownership and falls back to the public endpoint. Also documents two residuals rather than pretending to close them: equal rows are indistinguishable to a positional API, so verify-before-remove cannot protect a particular occurrence of a repeated video; and the credential recheck is a fence, not a lock, because Keychain state cannot be made atomic with a remote send.
This commit is contained in:
@@ -287,6 +287,17 @@ struct UnifiedPlaylistDetailView: View {
|
||||
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
|
||||
loadWatchEntries()
|
||||
}
|
||||
// Signing in again keeps the account, the source and usually the status,
|
||||
// so nothing else this view watches would move — but the login behind an
|
||||
// account-loaded playlist may now be somebody else.
|
||||
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
|
||||
discardAccountLoadedContentAndReload()
|
||||
}
|
||||
// The same, for a change of Apple account: every credential on the
|
||||
// device belongs to an identity, and this one's are no longer readable.
|
||||
.onReceive(NotificationCenter.default.publisher(for: .credentialIdentityScopeDidChange)) { _ in
|
||||
discardAccountLoadedContentAndReload()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.batchDownload(coordinator: downloadCoordinator)
|
||||
.onAppear {
|
||||
@@ -823,6 +834,37 @@ struct UnifiedPlaylistDetailView: View {
|
||||
loadWatchEntries()
|
||||
}
|
||||
|
||||
/// Throws away everything an account fetched here, and loads again.
|
||||
///
|
||||
/// Cleared first and synchronously, because the reload awaits a server:
|
||||
/// until it answers, a playlist that may be private would otherwise still
|
||||
/// be on screen — with its rows, its title and its destructive affordances
|
||||
/// — under a credential that no longer belongs to the person who fetched
|
||||
/// it. The reload resolves ownership again from scratch, and falls back to
|
||||
/// the public endpoint when the account can no longer read the playlist.
|
||||
///
|
||||
/// Only for a playlist an account loaded: a public one shows nothing that
|
||||
/// a login could make private, and reloading it would be a flash for
|
||||
/// nothing.
|
||||
private func discardAccountLoadedContentAndReload() {
|
||||
guard case .remote = source, entries != nil || owningAccount != nil else { return }
|
||||
|
||||
entries = nil
|
||||
entriesByRow = [:]
|
||||
owningAccount = nil
|
||||
remoteIsListed = nil
|
||||
remotePlaylist = nil
|
||||
title = ""
|
||||
descriptionText = nil
|
||||
thumbnailURL = nil
|
||||
videos = []
|
||||
videoCount = 0
|
||||
errorMessage = nil
|
||||
isLoading = true
|
||||
|
||||
Task { await loadPlaylist() }
|
||||
}
|
||||
|
||||
private func loadWatchEntries() {
|
||||
watchEntriesMap = dataManager?.watchEntriesMap() ?? [:]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user