mirror of
https://github.com/yattee/yattee.git
synced 2026-04-10 01:26:57 +00:00
Yattee v2 rewrite
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// InvidiousSubscriptionProvider.swift
|
||||
// Yattee
|
||||
//
|
||||
// Invidious subscription provider that manages subscriptions on an Invidious instance.
|
||||
// Uses in-memory cache only - does NOT persist to SwiftData to avoid corrupting local subscriptions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Invidious subscription provider that syncs with an Invidious instance account.
|
||||
/// Subscriptions are managed on the Invidious server and kept in memory only.
|
||||
/// This provider does NOT write to SwiftData to preserve local subscriptions.
|
||||
@MainActor
|
||||
final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
// MARK: - Properties
|
||||
|
||||
let accountType: SubscriptionAccountType = .invidious
|
||||
|
||||
private let invidiousAPI: InvidiousAPI
|
||||
private let credentialsManager: InvidiousCredentialsManager
|
||||
private let instancesManager: InstancesManager
|
||||
private let settingsManager: SettingsManager
|
||||
|
||||
/// In-memory cache of subscribed channel IDs for fast lookup.
|
||||
private var cachedChannelIDs: Set<String> = []
|
||||
|
||||
/// In-memory cache of full channel data for display.
|
||||
private var cachedChannels: [Channel] = []
|
||||
|
||||
/// Whether the cache has been populated from the server.
|
||||
private var cachePopulated = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
invidiousAPI: InvidiousAPI,
|
||||
credentialsManager: InvidiousCredentialsManager,
|
||||
instancesManager: InstancesManager,
|
||||
settingsManager: SettingsManager
|
||||
) {
|
||||
self.invidiousAPI = invidiousAPI
|
||||
self.credentialsManager = credentialsManager
|
||||
self.instancesManager = instancesManager
|
||||
self.settingsManager = settingsManager
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionProvider
|
||||
|
||||
func fetchSubscriptions() async throws -> [Channel] {
|
||||
let (instance, sid) = try await getAuthenticatedInstance()
|
||||
|
||||
// Fetch subscriptions from Invidious
|
||||
let subscriptions = try await invidiousAPI.subscriptions(instance: instance, sid: sid)
|
||||
|
||||
// Update in-memory cache
|
||||
cachedChannelIDs = Set(subscriptions.map(\.authorId))
|
||||
cachedChannels = subscriptions.map { $0.toChannel(baseURL: instance.url) }
|
||||
cachePopulated = true
|
||||
|
||||
LoggingService.shared.info("Fetched \(subscriptions.count) Invidious subscriptions", category: .api)
|
||||
|
||||
return cachedChannels
|
||||
}
|
||||
|
||||
func subscribe(to channel: Channel) async throws {
|
||||
let (instance, sid) = try await getAuthenticatedInstance()
|
||||
|
||||
// Subscribe on Invidious server
|
||||
try await invidiousAPI.subscribe(to: channel.id.channelID, instance: instance, sid: sid)
|
||||
|
||||
// Update in-memory cache only
|
||||
cachedChannelIDs.insert(channel.id.channelID)
|
||||
if !cachedChannels.contains(where: { $0.id.channelID == channel.id.channelID }) {
|
||||
cachedChannels.append(channel)
|
||||
}
|
||||
|
||||
LoggingService.shared.info("Subscribed to \(channel.name) on Invidious", category: .api)
|
||||
}
|
||||
|
||||
func unsubscribe(from channelID: String) async throws {
|
||||
let (instance, sid) = try await getAuthenticatedInstance()
|
||||
|
||||
// Unsubscribe on Invidious server
|
||||
try await invidiousAPI.unsubscribe(from: channelID, instance: instance, sid: sid)
|
||||
|
||||
// Update in-memory cache only
|
||||
cachedChannelIDs.remove(channelID)
|
||||
cachedChannels.removeAll { $0.id.channelID == channelID }
|
||||
|
||||
LoggingService.shared.info("Unsubscribed from \(channelID) on Invidious", category: .api)
|
||||
}
|
||||
|
||||
func isSubscribed(to channelID: String) async -> Bool {
|
||||
// If cache isn't populated yet, try to refresh it
|
||||
if !cachePopulated {
|
||||
do {
|
||||
_ = try await fetchSubscriptions()
|
||||
} catch {
|
||||
// Can't determine subscription status without server connection
|
||||
return false
|
||||
}
|
||||
}
|
||||
return cachedChannelIDs.contains(channelID)
|
||||
}
|
||||
|
||||
func refreshCache() async throws {
|
||||
// Simply fetch subscriptions again - this updates in-memory cache
|
||||
_ = try await fetchSubscriptions()
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Invidious instance and session ID from account settings.
|
||||
private func getAuthenticatedInstance() async throws -> (Instance, String) {
|
||||
// Get the instance ID from subscription account settings
|
||||
let account = settingsManager.subscriptionAccount
|
||||
|
||||
let instance: Instance?
|
||||
if let instanceID = account.instanceID {
|
||||
// Use the specific instance from account settings
|
||||
instance = instancesManager.instances.first { $0.id == instanceID && $0.isEnabled }
|
||||
} else {
|
||||
// Fallback to first enabled Invidious instance
|
||||
instance = instancesManager.instances.first { $0.type == .invidious && $0.isEnabled }
|
||||
}
|
||||
|
||||
guard let instance else {
|
||||
throw SubscriptionProviderError.instanceNotConfigured
|
||||
}
|
||||
|
||||
// Get session ID
|
||||
guard let sid = credentialsManager.sid(for: instance) else {
|
||||
throw SubscriptionProviderError.notAuthenticated
|
||||
}
|
||||
|
||||
return (instance, sid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// LocalSubscriptionProvider.swift
|
||||
// Yattee
|
||||
//
|
||||
// Local subscription provider using SwiftData with iCloud sync.
|
||||
// This is the default provider when using "Yattee (iCloud)" subscription account.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Local subscription provider that stores subscriptions in SwiftData.
|
||||
/// Syncs with iCloud via CloudKitSyncEngine.
|
||||
@MainActor
|
||||
final class LocalSubscriptionProvider: SubscriptionProvider {
|
||||
// MARK: - Properties
|
||||
|
||||
let accountType: SubscriptionAccountType = .local
|
||||
private let dataManager: DataManager
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(dataManager: DataManager) {
|
||||
self.dataManager = dataManager
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionProvider
|
||||
|
||||
func fetchSubscriptions() async throws -> [Channel] {
|
||||
// Convert Subscription models to Channel models
|
||||
dataManager.subscriptions().map { subscription in
|
||||
Channel(
|
||||
id: ChannelID(source: subscription.contentSource, channelID: subscription.channelID),
|
||||
name: subscription.name,
|
||||
description: subscription.channelDescription,
|
||||
subscriberCount: subscription.subscriberCount,
|
||||
thumbnailURL: subscription.avatarURL,
|
||||
bannerURL: subscription.bannerURL,
|
||||
isVerified: subscription.isVerified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe(to channel: Channel) async throws {
|
||||
// Check if already subscribed
|
||||
if dataManager.isSubscribed(to: channel.id.channelID) {
|
||||
throw SubscriptionProviderError.alreadySubscribed
|
||||
}
|
||||
dataManager.subscribe(to: channel)
|
||||
}
|
||||
|
||||
func unsubscribe(from channelID: String) async throws {
|
||||
// Check if subscribed
|
||||
guard dataManager.isSubscribed(to: channelID) else {
|
||||
throw SubscriptionProviderError.notSubscribed
|
||||
}
|
||||
dataManager.unsubscribe(from: channelID)
|
||||
}
|
||||
|
||||
func isSubscribed(to channelID: String) async -> Bool {
|
||||
dataManager.isSubscribed(to: channelID)
|
||||
}
|
||||
|
||||
func refreshCache() async throws {
|
||||
// Local provider doesn't need cache refresh - data is already local
|
||||
}
|
||||
}
|
||||
139
Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift
Normal file
139
Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// PipedSubscriptionProvider.swift
|
||||
// Yattee
|
||||
//
|
||||
// Piped subscription provider that manages subscriptions on a Piped instance.
|
||||
// Uses in-memory cache only - does NOT persist to SwiftData to avoid corrupting local subscriptions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Piped subscription provider that syncs with a Piped instance account.
|
||||
/// Subscriptions are managed on the Piped server and kept in memory only.
|
||||
/// This provider does NOT write to SwiftData to preserve local subscriptions.
|
||||
@MainActor
|
||||
final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
// MARK: - Properties
|
||||
|
||||
let accountType: SubscriptionAccountType = .piped
|
||||
|
||||
private let pipedAPI: PipedAPI
|
||||
private let credentialsManager: PipedCredentialsManager
|
||||
private let instancesManager: InstancesManager
|
||||
private let settingsManager: SettingsManager
|
||||
|
||||
/// In-memory cache of subscribed channel IDs for fast lookup.
|
||||
private var cachedChannelIDs: Set<String> = []
|
||||
|
||||
/// In-memory cache of full channel data for display.
|
||||
private var cachedChannels: [Channel] = []
|
||||
|
||||
/// Whether the cache has been populated from the server.
|
||||
private var cachePopulated = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
pipedAPI: PipedAPI,
|
||||
credentialsManager: PipedCredentialsManager,
|
||||
instancesManager: InstancesManager,
|
||||
settingsManager: SettingsManager
|
||||
) {
|
||||
self.pipedAPI = pipedAPI
|
||||
self.credentialsManager = credentialsManager
|
||||
self.instancesManager = instancesManager
|
||||
self.settingsManager = settingsManager
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionProvider
|
||||
|
||||
func fetchSubscriptions() async throws -> [Channel] {
|
||||
let (instance, authToken) = try await getAuthenticatedInstance()
|
||||
|
||||
// Fetch subscriptions from Piped
|
||||
let subscriptions = try await pipedAPI.subscriptions(instance: instance, authToken: authToken)
|
||||
|
||||
// Update in-memory cache
|
||||
cachedChannelIDs = Set(subscriptions.map(\.channelId))
|
||||
cachedChannels = subscriptions.map { $0.toChannel() }
|
||||
cachePopulated = true
|
||||
|
||||
LoggingService.shared.info("Fetched \(subscriptions.count) Piped subscriptions", category: .api)
|
||||
|
||||
return cachedChannels
|
||||
}
|
||||
|
||||
func subscribe(to channel: Channel) async throws {
|
||||
let (instance, authToken) = try await getAuthenticatedInstance()
|
||||
|
||||
// Subscribe on Piped server
|
||||
try await pipedAPI.subscribe(channelID: channel.id.channelID, instance: instance, authToken: authToken)
|
||||
|
||||
// Update in-memory cache only
|
||||
cachedChannelIDs.insert(channel.id.channelID)
|
||||
if !cachedChannels.contains(where: { $0.id.channelID == channel.id.channelID }) {
|
||||
cachedChannels.append(channel)
|
||||
}
|
||||
|
||||
LoggingService.shared.info("Subscribed to \(channel.name) on Piped", category: .api)
|
||||
}
|
||||
|
||||
func unsubscribe(from channelID: String) async throws {
|
||||
let (instance, authToken) = try await getAuthenticatedInstance()
|
||||
|
||||
// Unsubscribe on Piped server
|
||||
try await pipedAPI.unsubscribe(channelID: channelID, instance: instance, authToken: authToken)
|
||||
|
||||
// Update in-memory cache only
|
||||
cachedChannelIDs.remove(channelID)
|
||||
cachedChannels.removeAll { $0.id.channelID == channelID }
|
||||
|
||||
LoggingService.shared.info("Unsubscribed from \(channelID) on Piped", category: .api)
|
||||
}
|
||||
|
||||
func isSubscribed(to channelID: String) async -> Bool {
|
||||
// If cache isn't populated yet, try to refresh it
|
||||
if !cachePopulated {
|
||||
do {
|
||||
_ = try await fetchSubscriptions()
|
||||
} catch {
|
||||
// Can't determine subscription status without server connection
|
||||
return false
|
||||
}
|
||||
}
|
||||
return cachedChannelIDs.contains(channelID)
|
||||
}
|
||||
|
||||
func refreshCache() async throws {
|
||||
// Simply fetch subscriptions again - this updates in-memory cache
|
||||
_ = try await fetchSubscriptions()
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Piped instance and auth token from account settings.
|
||||
private func getAuthenticatedInstance() async throws -> (Instance, String) {
|
||||
// Get the instance ID from subscription account settings
|
||||
let account = settingsManager.subscriptionAccount
|
||||
|
||||
let instance: Instance?
|
||||
if let instanceID = account.instanceID {
|
||||
// Use the specific instance from account settings
|
||||
instance = instancesManager.instances.first { $0.id == instanceID && $0.isEnabled }
|
||||
} else {
|
||||
// Fallback to first enabled Piped instance
|
||||
instance = instancesManager.instances.first { $0.type == .piped && $0.isEnabled }
|
||||
}
|
||||
|
||||
guard let instance else {
|
||||
throw SubscriptionProviderError.instanceNotConfigured
|
||||
}
|
||||
|
||||
// Get auth token
|
||||
guard let authToken = credentialsManager.credential(for: instance) else {
|
||||
throw SubscriptionProviderError.notAuthenticated
|
||||
}
|
||||
|
||||
return (instance, authToken)
|
||||
}
|
||||
}
|
||||
226
Yattee/Services/Subscriptions/SubscriptionAccountValidator.swift
Normal file
226
Yattee/Services/Subscriptions/SubscriptionAccountValidator.swift
Normal file
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// SubscriptionAccountValidator.swift
|
||||
// Yattee
|
||||
//
|
||||
// Validates subscription account selection and handles auto-correction
|
||||
// when the selected account becomes unavailable.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Validates subscription account selection and provides available accounts for the picker.
|
||||
/// Automatically corrects to a valid account if the current selection becomes invalid.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class SubscriptionAccountValidator {
|
||||
// MARK: - Dependencies
|
||||
|
||||
private let settingsManager: SettingsManager
|
||||
private let instancesManager: InstancesManager
|
||||
private let invidiousCredentialsManager: InvidiousCredentialsManager
|
||||
private let pipedCredentialsManager: PipedCredentialsManager
|
||||
private let toastManager: ToastManager
|
||||
private let feedCache: SubscriptionFeedCache
|
||||
|
||||
// MARK: - Observation State
|
||||
|
||||
private var observationTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
settingsManager: SettingsManager,
|
||||
instancesManager: InstancesManager,
|
||||
invidiousCredentialsManager: InvidiousCredentialsManager,
|
||||
pipedCredentialsManager: PipedCredentialsManager,
|
||||
toastManager: ToastManager,
|
||||
feedCache: SubscriptionFeedCache
|
||||
) {
|
||||
self.settingsManager = settingsManager
|
||||
self.instancesManager = instancesManager
|
||||
self.invidiousCredentialsManager = invidiousCredentialsManager
|
||||
self.pipedCredentialsManager = pipedCredentialsManager
|
||||
self.toastManager = toastManager
|
||||
self.feedCache = feedCache
|
||||
|
||||
startObserving()
|
||||
}
|
||||
|
||||
/// Stops observation. Call before releasing the validator.
|
||||
func stopObserving() {
|
||||
observationTask?.cancel()
|
||||
observationTask = nil
|
||||
}
|
||||
|
||||
// MARK: - Available Accounts
|
||||
|
||||
/// Available subscription accounts based on current configuration.
|
||||
/// Only includes accounts that are properly configured and authenticated.
|
||||
var availableAccounts: [SubscriptionAccount] {
|
||||
var accounts: [SubscriptionAccount] = []
|
||||
|
||||
// Add local if Yattee Server is configured and enabled
|
||||
let hasYatteeServer = instancesManager.instances.contains {
|
||||
$0.type == .yatteeServer && $0.isEnabled
|
||||
}
|
||||
if hasYatteeServer {
|
||||
accounts.append(.local)
|
||||
}
|
||||
|
||||
// Add logged-in and enabled Invidious instances
|
||||
for instance in instancesManager.instances {
|
||||
if instance.type == .invidious &&
|
||||
instance.isEnabled &&
|
||||
invidiousCredentialsManager.isLoggedIn(for: instance) {
|
||||
accounts.append(.invidious(instance.id))
|
||||
}
|
||||
}
|
||||
|
||||
// Add logged-in and enabled Piped instances
|
||||
for instance in instancesManager.instances {
|
||||
if instance.type == .piped &&
|
||||
instance.isEnabled &&
|
||||
pipedCredentialsManager.isLoggedIn(for: instance) {
|
||||
accounts.append(.piped(instance.id))
|
||||
}
|
||||
}
|
||||
|
||||
return accounts
|
||||
}
|
||||
|
||||
/// Whether any subscription accounts are available.
|
||||
var hasAvailableAccounts: Bool {
|
||||
!availableAccounts.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Validation
|
||||
|
||||
/// Whether the current subscription account is valid.
|
||||
var isCurrentAccountValid: Bool {
|
||||
let current = settingsManager.subscriptionAccount
|
||||
return availableAccounts.contains(current)
|
||||
}
|
||||
|
||||
/// Validates the current account and auto-corrects if invalid.
|
||||
/// Call this on app launch and when configuration changes.
|
||||
func validateAndCorrectIfNeeded() {
|
||||
guard !isCurrentAccountValid else {
|
||||
LoggingService.shared.debug(
|
||||
"Current subscription account is valid: \(settingsManager.subscriptionAccount.type)",
|
||||
category: .general
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let previousAccount = settingsManager.subscriptionAccount
|
||||
|
||||
// Find first available account to switch to
|
||||
if let newAccount = availableAccounts.first {
|
||||
settingsManager.subscriptionAccount = newAccount
|
||||
feedCache.handleAccountChange()
|
||||
|
||||
let newName = displayName(for: newAccount)
|
||||
LoggingService.shared.info(
|
||||
"Auto-corrected subscription account from \(previousAccount.type) to \(newAccount.type)",
|
||||
category: .general
|
||||
)
|
||||
|
||||
toastManager.showInfo(
|
||||
String(localized: "subscriptions.accountSwitched.title"),
|
||||
subtitle: newName
|
||||
)
|
||||
} else {
|
||||
// No valid accounts available - set to local anyway
|
||||
// The feed will show an appropriate error state
|
||||
settingsManager.subscriptionAccount = .local
|
||||
feedCache.handleAccountChange()
|
||||
|
||||
LoggingService.shared.warning(
|
||||
"No valid subscription accounts available, defaulting to local",
|
||||
category: .general
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display Names
|
||||
|
||||
/// Returns a display name for the given subscription account.
|
||||
func displayName(for account: SubscriptionAccount) -> String {
|
||||
switch account.type {
|
||||
case .local:
|
||||
return String(localized: "subscriptions.account.local")
|
||||
|
||||
case .invidious:
|
||||
guard let instanceID = account.instanceID,
|
||||
let instance = instancesManager.instances.first(where: { $0.id == instanceID }) else {
|
||||
return String(localized: "subscriptions.account.invidious")
|
||||
}
|
||||
// Use instance name if available, otherwise use hostname
|
||||
let instanceName: String
|
||||
if let name = instance.name, !name.isEmpty {
|
||||
instanceName = name
|
||||
} else {
|
||||
instanceName = instance.url.host ?? "Invidious"
|
||||
}
|
||||
return String(localized: "subscriptions.account.invidiousInstance \(instanceName)")
|
||||
|
||||
case .piped:
|
||||
guard let instanceID = account.instanceID,
|
||||
let instance = instancesManager.instances.first(where: { $0.id == instanceID }) else {
|
||||
return String(localized: "subscriptions.account.piped")
|
||||
}
|
||||
// Use instance name if available, otherwise use hostname
|
||||
let instanceName: String
|
||||
if let name = instance.name, !name.isEmpty {
|
||||
instanceName = name
|
||||
} else {
|
||||
instanceName = instance.url.host ?? "Piped"
|
||||
}
|
||||
return String(localized: "subscriptions.account.pipedInstance \(instanceName)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the instance for an Invidious or Piped account, if available.
|
||||
func instance(for account: SubscriptionAccount) -> Instance? {
|
||||
guard account.type == .invidious || account.type == .piped,
|
||||
let instanceID = account.instanceID else {
|
||||
return nil
|
||||
}
|
||||
return instancesManager.instances.first { $0.id == instanceID }
|
||||
}
|
||||
|
||||
// MARK: - Observation
|
||||
|
||||
/// Starts observing changes to instances and credentials.
|
||||
private func startObserving() {
|
||||
observationTask?.cancel()
|
||||
|
||||
observationTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
while !Task.isCancelled {
|
||||
// Wait for changes using withObservationTracking
|
||||
await withCheckedContinuation { continuation in
|
||||
withObservationTracking {
|
||||
// Access the observed properties to register for tracking
|
||||
_ = self.instancesManager.instances
|
||||
_ = self.invidiousCredentialsManager.loggedInInstanceIDs
|
||||
_ = self.pipedCredentialsManager.loggedInInstanceIDs
|
||||
} onChange: {
|
||||
// Resume when a change is detected
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
guard !Task.isCancelled else { break }
|
||||
|
||||
// Small delay to batch rapid changes
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
guard !Task.isCancelled else { break }
|
||||
|
||||
// Validate after changes
|
||||
self.validateAndCorrectIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Yattee/Services/Subscriptions/SubscriptionProvider.swift
Normal file
79
Yattee/Services/Subscriptions/SubscriptionProvider.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// SubscriptionProvider.swift
|
||||
// Yattee
|
||||
//
|
||||
// Protocol defining subscription management operations.
|
||||
// Implementations handle different subscription sources (local, Invidious, Piped).
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Protocol for subscription management providers.
|
||||
/// Each provider implementation handles a specific subscription source.
|
||||
@MainActor
|
||||
protocol SubscriptionProvider: Sendable {
|
||||
/// The type of subscription account this provider handles.
|
||||
var accountType: SubscriptionAccountType { get }
|
||||
|
||||
/// Fetches all subscriptions from the provider.
|
||||
/// - Returns: Array of channels the user is subscribed to.
|
||||
func fetchSubscriptions() async throws -> [Channel]
|
||||
|
||||
/// Subscribes to a channel.
|
||||
/// - Parameter channel: The channel to subscribe to.
|
||||
func subscribe(to channel: Channel) async throws
|
||||
|
||||
/// Unsubscribes from a channel.
|
||||
/// - Parameter channelID: The channel ID to unsubscribe from.
|
||||
func unsubscribe(from channelID: String) async throws
|
||||
|
||||
/// Checks if subscribed to a channel.
|
||||
/// - Parameter channelID: The channel ID to check.
|
||||
/// - Returns: `true` if subscribed, `false` otherwise.
|
||||
func isSubscribed(to channelID: String) async -> Bool
|
||||
|
||||
/// Refreshes the local cache of subscriptions from the remote source.
|
||||
/// For local provider, this is a no-op.
|
||||
func refreshCache() async throws
|
||||
}
|
||||
|
||||
// MARK: - Default Implementations
|
||||
|
||||
extension SubscriptionProvider {
|
||||
/// Default implementation for providers that don't need cache refresh.
|
||||
func refreshCache() async throws {
|
||||
// No-op by default
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription Provider Error
|
||||
|
||||
/// Errors that can occur during subscription provider operations.
|
||||
enum SubscriptionProviderError: Error, LocalizedError, Equatable, Sendable {
|
||||
case notAuthenticated
|
||||
case networkError(String)
|
||||
case channelNotFound
|
||||
case alreadySubscribed
|
||||
case notSubscribed
|
||||
case instanceNotConfigured
|
||||
case operationFailed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notAuthenticated:
|
||||
return String(localized: "subscription.error.notAuthenticated")
|
||||
case .networkError(let message):
|
||||
return String(localized: "subscription.error.network \(message)")
|
||||
case .channelNotFound:
|
||||
return String(localized: "subscription.error.channelNotFound")
|
||||
case .alreadySubscribed:
|
||||
return String(localized: "subscription.error.alreadySubscribed")
|
||||
case .notSubscribed:
|
||||
return String(localized: "subscription.error.notSubscribed")
|
||||
case .instanceNotConfigured:
|
||||
return String(localized: "subscription.error.instanceNotConfigured")
|
||||
case .operationFailed(let message):
|
||||
return String(localized: "subscription.error.operationFailed \(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user