Let Subscribe On know its memberships and refuse a doomed destination

MAC5: membership lives in an observable loader started from ChannelView's
task, outside the NSMenu content SwiftUI never runs lifecycle for; rows are
Toggles so the mark renders on macOS, and writes toast Subscribed or
Unsubscribed with the destination name.
MAC13: a destination whose context fails validation is disabled, the same
gate the plain Subscribe button uses.
This commit is contained in:
Arkadiusz Fal
2026-09-11 08:20:24 +02:00
parent e661635079
commit b0b95195b6
2 changed files with 233 additions and 117 deletions

View File

@@ -34,6 +34,10 @@ struct ChannelView: View {
/// previous one is dropped instead of answering for the channel on screen.
@State private var subscriptionGeneration = 0
@State private var showingUnsubscribeConfirmation = false
/// What the destination rows know, kept here rather than inside the menu:
/// nothing in a platform menu's content is ever given a lifecycle to load
/// from.
@State private var destinationMembership = SubscriptionDestinationMembershipLoader()
#if os(tvOS)
@State private var showingSubscriptionDestinations = false
#endif
@@ -192,6 +196,15 @@ struct ChannelView: View {
.task {
await loadChannel()
}
// Asked for here, where the answers arrive before the menu is built:
// the rows themselves live inside a platform menu, which runs no
// lifecycle of its own, and a mark that is not known by the time the
// menu opens is a mark the user never sees.
.task(id: channelToSubscribe?.id) {
guard let channelToSubscribe,
SubscriptionDestinations.areOffered(in: appEnvironment) else { return }
await destinationMembership.load(for: channelToSubscribe, in: appEnvironment)
}
.onAppear {
// Update Handoff activity for this specific channel
appEnvironment?.handoffManager.updateActivity(for: .channel(channelID, source))
@@ -825,7 +838,10 @@ struct ChannelView: View {
}
.buttonStyle(.bordered)
.fullScreenCover(isPresented: $showingSubscriptionDestinations) {
SubscriptionDestinationPicker(channel: channelToSubscribe) { context, subscribed in
SubscriptionDestinationPicker(
channel: channelToSubscribe,
membership: destinationMembership
) { context, subscribed in
destinationDidToggle(context, subscribed: subscribed)
}
}
@@ -1249,7 +1265,10 @@ struct ChannelView: View {
// never presented there.
#if !os(tvOS)
if let channelToSubscribe {
SubscriptionDestinationMenu(channel: channelToSubscribe) { context, subscribed in
SubscriptionDestinationMenu(
channel: channelToSubscribe,
membership: destinationMembership
) { context, subscribed in
destinationDidToggle(context, subscribed: subscribed)
}
}
@@ -1275,6 +1294,7 @@ struct ChannelView: View {
} label: {
Image(systemName: channelMenuIcon)
}
.accessibilityLabel(Text(String(localized: "channel.menu.title")))
}
// MARK: - Subscription Actions