mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Keep destination checkmarks in sync with the main subscribe button
The main button's subscribe/unsubscribe never told the destination membership loader, so "Subscribe On…" kept a stale checkmark for the current account — and tapping that row would do the opposite of what the mark promised. Also: the menu item and success toast now name the account the write lands on when more than one destination is offered, and "Subscribe On…" no longer waits for the channel request — the cached header stands in until the load answers.
This commit is contained in:
@@ -952,6 +952,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channel.menu.subscribe.withAccount" : {
|
||||
"comment" : "Menu item; %@ is the account or destination the subscribe writes to",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Subscribe - %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"channel.menu.subscribeOn" : {
|
||||
"comment" : "Submenu listing the accounts a channel can be subscribed on",
|
||||
"localizations" : {
|
||||
@@ -984,6 +995,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channel.menu.unsubscribe.withAccount" : {
|
||||
"comment" : "Menu item; %@ is the account or destination the unsubscribe writes to",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Unsubscribe - %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"channel.noDescription" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -1242,14 +1242,13 @@ struct ChannelView: View {
|
||||
/// Toolbar menu for subscribe/unsubscribe and notification actions
|
||||
private var channelMenu: some View {
|
||||
Menu {
|
||||
// Subscribe/Unsubscribe button
|
||||
// Subscribe/Unsubscribe button, naming the account it writes to —
|
||||
// the same answer "Subscribe On…" would give for the same target.
|
||||
Button {
|
||||
toggleSubscription()
|
||||
} label: {
|
||||
Label(
|
||||
isSubscribed
|
||||
? String(localized: "channel.menu.unsubscribe")
|
||||
: String(localized: "channel.menu.subscribe"),
|
||||
mainSubscribeTitle,
|
||||
systemImage: isSubscribed ? "person.fill.xmark" : "person.badge.plus"
|
||||
)
|
||||
}
|
||||
@@ -1308,6 +1307,24 @@ struct ChannelView: View {
|
||||
/// Local always may. A server account may only while it is usable: a
|
||||
/// subscribe that cannot reach its account must be refused rather than
|
||||
/// land in local data the user did not choose.
|
||||
/// "Subscribe - Yattee iCloud" rather than a bare "Subscribe" — but only
|
||||
/// while there is more than one place the write could land. With a single
|
||||
/// destination the name answers a question nobody has.
|
||||
private var mainSubscribeTitle: String {
|
||||
guard let appEnvironment, SubscriptionDestinations.areOffered(in: appEnvironment) else {
|
||||
return String(localized: isSubscribed ? "channel.menu.unsubscribe" : "channel.menu.subscribe")
|
||||
}
|
||||
|
||||
let key: String.LocalizationValue = isSubscribed
|
||||
? "channel.menu.unsubscribe.withAccount"
|
||||
: "channel.menu.subscribe.withAccount"
|
||||
let title = DataContextSwitcherLabels.title(
|
||||
for: appEnvironment.accountsManager.currentContext,
|
||||
in: appEnvironment
|
||||
)
|
||||
return String(format: String(localized: key), title)
|
||||
}
|
||||
|
||||
private var allowsCurrentContextMutation: Bool {
|
||||
guard let appEnvironment else { return false }
|
||||
guard !appEnvironment.accountsManager.currentContext.isLocal else { return true }
|
||||
@@ -1316,13 +1333,28 @@ struct ChannelView: View {
|
||||
|
||||
/// The channel as it must be stored: the id carries the source this view
|
||||
/// was opened for, which an extracted channel does not report itself.
|
||||
///
|
||||
/// Before the load answers, the cached header stands in — the id is already
|
||||
/// known and the header carries the name a subscription stores — so
|
||||
/// "Subscribe On…" is usable as soon as the main button is, not only once
|
||||
/// the channel request finishes. External channels keep waiting: their real
|
||||
/// id only exists after extraction.
|
||||
private var channelToSubscribe: Channel? {
|
||||
guard let channel else { return nil }
|
||||
if let channel {
|
||||
return Channel(
|
||||
id: ChannelID(source: source, channelID: channel.id.channelID),
|
||||
name: channel.name,
|
||||
subscriberCount: channel.subscriberCount,
|
||||
thumbnailURL: channel.thumbnailURL
|
||||
)
|
||||
}
|
||||
|
||||
guard !isExternalChannel, let cachedHeader, !cachedHeader.name.isEmpty else { return nil }
|
||||
return Channel(
|
||||
id: ChannelID(source: source, channelID: channel.id.channelID),
|
||||
name: channel.name,
|
||||
subscriberCount: channel.subscriberCount,
|
||||
thumbnailURL: channel.thumbnailURL
|
||||
id: ChannelID(source: source, channelID: channelID),
|
||||
name: cachedHeader.name,
|
||||
subscriberCount: cachedHeader.subscriberCount,
|
||||
thumbnailURL: cachedHeader.thumbnailURL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1349,7 +1381,9 @@ struct ChannelView: View {
|
||||
}
|
||||
|
||||
isSubscribed = true
|
||||
noteMainButtonToggled(subscribed: true)
|
||||
refreshSubscription()
|
||||
showSubscriptionToast(subscribed: true)
|
||||
} catch {
|
||||
appEnvironment?.toastManager.showError(
|
||||
String(localized: "channel.subscribe.error.title"),
|
||||
@@ -1358,6 +1392,38 @@ struct ChannelView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The same toast a destination row shows, for the main button's write:
|
||||
/// title says what happened, subtitle names the account it landed on —
|
||||
/// under the same rule as the menu label, only when there is more than one
|
||||
/// account it could have landed on.
|
||||
private func showSubscriptionToast(subscribed: Bool) {
|
||||
guard let appEnvironment else { return }
|
||||
|
||||
let subtitle: String? = SubscriptionDestinations.areOffered(in: appEnvironment)
|
||||
? DataContextSwitcherLabels.title(
|
||||
for: appEnvironment.accountsManager.currentContext,
|
||||
in: appEnvironment
|
||||
)
|
||||
: nil
|
||||
|
||||
appEnvironment.toastManager.showSuccess(
|
||||
String(
|
||||
localized: subscribed
|
||||
? "channel.subscribe.destination.subscribed"
|
||||
: "channel.subscribe.destination.unsubscribed"
|
||||
),
|
||||
subtitle: subtitle
|
||||
)
|
||||
}
|
||||
|
||||
/// The main button wrote to the currently selected context; the destination
|
||||
/// row naming that context still carries the mark from before the write, so
|
||||
/// it is told — otherwise its next tap does the opposite of what it shows.
|
||||
private func noteMainButtonToggled(subscribed: Bool) {
|
||||
guard let context = appEnvironment?.accountsManager.currentContext else { return }
|
||||
destinationMembership.noteToggled(context, subscribed: subscribed)
|
||||
}
|
||||
|
||||
/// A destination row toggled something. Only the account on screen changes
|
||||
/// what the main button says, and notification preferences are local and
|
||||
/// keyed by channel, so they follow the same rule.
|
||||
@@ -1385,6 +1451,8 @@ struct ChannelView: View {
|
||||
do {
|
||||
try await appEnvironment?.subscriptionService.unsubscribe(from: channel.id.channelID)
|
||||
isSubscribed = false
|
||||
noteMainButtonToggled(subscribed: false)
|
||||
showSubscriptionToast(subscribed: false)
|
||||
} catch {
|
||||
appEnvironment?.toastManager.showError(
|
||||
String(localized: "channel.unsubscribe.error.title"),
|
||||
|
||||
@@ -85,6 +85,14 @@ final class SubscriptionDestinationMembershipLoader {
|
||||
|
||||
func isSubscribed(on context: DataContext) -> Bool? { membership.isSubscribed(on: context) }
|
||||
|
||||
/// The main subscribe button wrote to `context` behind these rows' back;
|
||||
/// without being told, the row for that context keeps the mark from before
|
||||
/// the write — and a tap on it would then do the opposite of what the mark
|
||||
/// promises.
|
||||
func noteToggled(_ context: DataContext, subscribed: Bool) {
|
||||
membership.record(subscribed, for: context, generation: membership.generation)
|
||||
}
|
||||
|
||||
func isBusy(_ context: DataContext) -> Bool { busy.contains(context) }
|
||||
|
||||
/// Asks about `channel`, from a place the SwiftUI lifecycle reaches.
|
||||
|
||||
Reference in New Issue
Block a user