diff --git a/Modules/Sources/Favorites/PersistanceKey+Favorites.swift b/Modules/Sources/Favorites/PersistanceKey+Favorites.swift index d458c86..14842a4 100644 --- a/Modules/Sources/Favorites/PersistanceKey+Favorites.swift +++ b/Modules/Sources/Favorites/PersistanceKey+Favorites.swift @@ -1,6 +1,9 @@ import Foundation import ComposableArchitecture import BsuirCore +import ScheduleCore + +// MARK: - High Score extension PersistenceReaderKey where Self == PersistenceKeyDefault> { public static var freeLoveHighScore: Self { @@ -15,3 +18,56 @@ extension PersistenceReaderKey where Self == PersistenceKeyDefault, + CloudSyncableScheduleSource + > +> { + public static var pinnedSchedule: Self { + let syncableDictionary = CloudSyncablePersistenceKey<[String: Any]>.cloudSyncable( + key: "pinned-schedule", + cloudKey: "cloud-pinned-schedule", + shouldSyncInitialLocalValue: true, + isEqual: { $0 as NSDictionary? == $1 as NSDictionary? } + ) + + let syncableCloudSource = PersistenceKeyTransform( + base: syncableDictionary, + coding: CloudSyncableScheduleSource.self + ) + + return PersistenceKeyDefault(syncableCloudSource, .nothing) + } +} + +// MARK: - Favorites + +extension PersistenceReaderKey where Self == PersistenceKeyDefault> { + public static var favoriteGroupNames: Self { + PersistenceKeyDefault( + .cloudSyncable( + key: "favorite-group-names", + cloudKey: "cloud-favorite-group-names", + shouldSyncInitialLocalValue: true + ), + [] + ) + } +} + +extension PersistenceReaderKey where Self == PersistenceKeyDefault> { + public static var favoriteLecturerIDs: Self { + PersistenceKeyDefault( + .cloudSyncable( + key: "favorite-lector-ids", + cloudKey: "cloud-favorite-lector-ids", + shouldSyncInitialLocalValue: true + ), + [] + ) + } +} diff --git a/Modules/Sources/GroupsFeature/GroupsFeature.swift b/Modules/Sources/GroupsFeature/GroupsFeature.swift index 38660c3..e71882f 100644 --- a/Modules/Sources/GroupsFeature/GroupsFeature.swift +++ b/Modules/Sources/GroupsFeature/GroupsFeature.swift @@ -23,8 +23,10 @@ public struct GroupsFeature { var groupPresentationMode: GroupPresentationMode = .initial // MARK: Placeholder - var hasPinnedPlaceholder: Bool = false - var favoritesPlaceholderCount: Int = 0 + var hasPinnedPlaceholder: Bool { pinnedSchedule.source?.is(\.group) ?? false } + var favoritesPlaceholderCount: Int { favoriteGroupNames.count } + @SharedReader(.favoriteGroupNames) var favoriteGroupNames + @SharedReader(.pinnedSchedule) var pinnedSchedule // MARK: Groups var groups: LoadingState = .initial @@ -41,7 +43,6 @@ public struct GroupsFeature { } case task - case onAppear case forceAddGroupButtonTapped case forceAddAlert(PresentationAction) @@ -51,19 +52,12 @@ public struct GroupsFeature { } @Dependency(\.apiClient) var apiClient - @Dependency(\.favorites.currentGroupNames) var favoriteGroupNames - @Dependency(\.pinnedScheduleService.currentSchedule) var pinnedSchedule public init() {} public var body: some ReducerOf { Reduce { state, action in switch action { - case .onAppear: - state.hasPinnedPlaceholder = pinnedSchedule()?.groupName != nil - state.favoritesPlaceholderCount = favoriteGroupNames.count - return .none - case .task: // Very important to do in task if done in `onAppear` // schedule screen is not properly loaded and got stuck in placeholder state @@ -99,11 +93,7 @@ public struct GroupsFeature { } .load(state: \.groups, action: \.groups) { _, isRefresh in let groups = try await apiClient.groups(isRefresh) - return LoadedGroupsFeature.State( - groups: groups, - favoritesNames: favoriteGroupNames, - pinnedName: pinnedSchedule()?.groupName - ) + return LoadedGroupsFeature.State(groups: groups) } loaded: { LoadedGroupsFeature() } diff --git a/Modules/Sources/GroupsFeature/GroupsFeatureView.swift b/Modules/Sources/GroupsFeature/GroupsFeatureView.swift index d46a1bb..1abcf3e 100644 --- a/Modules/Sources/GroupsFeature/GroupsFeatureView.swift +++ b/Modules/Sources/GroupsFeature/GroupsFeatureView.swift @@ -44,7 +44,6 @@ public struct GroupsFeatureView: View { } destination: { store in EntityScheduleFeatureViewV2(store: store) } - .onAppear { store.send(.onAppear) } .task { await store.send(.task).finish() } .forceAddAlert(store: $store.scope(state: \.forceAddAlert, action: \.forceAddAlert)) } diff --git a/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeature.swift b/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeature.swift index cdaa1d0..e1e3d2f 100644 --- a/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeature.swift +++ b/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeature.swift @@ -25,18 +25,18 @@ public struct LoadedGroupsFeature { updateVisibleRows() } - private mutating func updatePinnedRows() { + fileprivate mutating func updatePinnedRows(groupName: String?? = nil) { pinnedRows = IdentifiedArray( - uniqueElements: [pinnedName] + uniqueElements: [groupName ?? pinnedSchedule.source?.groupName] .compacted() .filter { $0.matches(query: searchQuery) } .map { groupRows[id: $0].or(GroupsRow.State(groupName: $0)) } ) } - private mutating func updateFavoriteRows() { + fileprivate mutating func updateFavoriteRows(favoritesNames: [String]? = nil) { favoriteRows = IdentifiedArray( - uniqueElements: favoritesNames + uniqueElements: (favoritesNames ?? self.favoritesNames) .filter { $0.matches(query: searchQuery) } .map { groupRows[id: $0].or(GroupsRow.State(groupName: $0)) } ) @@ -58,17 +58,11 @@ public struct LoadedGroupsFeature { var visibleRows: IdentifiedArrayOf = [] // MARK: State - fileprivate var favoritesNames: OrderedSet - fileprivate var pinnedName: String? + @SharedReader(.favoriteGroupNames) var favoritesNames + @SharedReader(.pinnedSchedule) var pinnedSchedule fileprivate var groupRows: IdentifiedArrayOf - init( - groups: [StudentGroup], - favoritesNames: OrderedSet, - pinnedName: String? - ) { - self.favoritesNames = favoritesNames - self.pinnedName = pinnedName + init(groups: [StudentGroup]) { self.groupRows = IdentifiedArray( uniqueElements: groups .sorted(by: { $0.name < $1.name }) @@ -89,16 +83,13 @@ public struct LoadedGroupsFeature { case favoriteRows(IdentifiedActionOf) case visibleRows(IdentifiedActionOf) - case _favoritesUpdate(OrderedSet) + case _favoritesUpdate([String]) case _pinnedUpdate(String?) case delegate(Delegate) case binding(BindingAction) } - @Dependency(\.favorites.groupNames) var favoriteGroupNames - @Dependency(\.pinnedScheduleService.schedule) var pinnedSchedule - public var body: some ReducerOf { BindingReducer() .onChange(of: \.searchQuery) { _, query in @@ -116,16 +107,24 @@ public struct LoadedGroupsFeature { switch action { case .task: return .merge( - listenToFavoriteUpdates(), - listenToPinnedUpdates() + .publisher { + state.$favoritesNames.publisher + .map(Action._favoritesUpdate) + }, + .publisher { + state.$pinnedSchedule.publisher + .map { $0.source?.groupName } + .removeDuplicates() + .map(Action._pinnedUpdate) + } ) - case ._favoritesUpdate(let value): - state.favoritesNames = value + case ._favoritesUpdate(let newValue): + state.updateFavoriteRows(favoritesNames: newValue) return .none - case ._pinnedUpdate(let value): - state.pinnedName = value + case ._pinnedUpdate(let newValue): + state.updatePinnedRows(groupName: newValue) return .none case .pinnedRows(.element(_, .mark(.delegate(let action)))), @@ -149,80 +148,6 @@ public struct LoadedGroupsFeature { .forEach(\.visibleRows, action: \.visibleRows) { GroupsRow() } - .onChange(of: \.pinnedName) { oldPinned, newPinned in - Reduce { state, _ in - return updatePinned(state: &state, oldPinned: oldPinned, newPinned: newPinned) - } - } - .onChange(of: \.favoritesNames) { oldFavorites, newFavorites in - Reduce { state, _ in - return updateFavorites(state: &state, oldFavorites: oldFavorites, newFavorites: newFavorites) - } - } - } - - private func listenToFavoriteUpdates() -> Effect { - return .run { send in - for await value in favoriteGroupNames.removeDuplicates().values { - await send(._favoritesUpdate(value), animation: .default) - } - } - } - - private func listenToPinnedUpdates() -> Effect { - return .run { send in - for await value in pinnedSchedule().map(\.?.groupName).removeDuplicates().values { - await send(._pinnedUpdate(value), animation: .default) - } - } - } - - private func updatePinned( - state: inout State, - oldPinned: String?, - newPinned: String? - ) -> Effect { - if let newPinned { - state.favoriteRows[id: newPinned]?.mark.isPinned = true - state.visibleRows[id: newPinned]?.mark.isPinned = true - state.groupRows[id: newPinned]?.mark.isPinned = true - if newPinned.matches(query: state.searchQuery) { - state.pinnedRows[id: newPinned] = state.groupRows[id: newPinned] ?? GroupsRow.State(groupName: newPinned) - } - } - - if let oldPinned { - state.favoriteRows[id: oldPinned]?.mark.isPinned = false - state.visibleRows[id: oldPinned]?.mark.isPinned = false - state.groupRows[id: oldPinned]?.mark.isPinned = false - state.pinnedRows.remove(id: oldPinned) - } - - return .none - } - - private func updateFavorites( - state: inout State, - oldFavorites: OrderedSet, - newFavorites: OrderedSet - ) -> Effect { - for difference in newFavorites.difference(from: oldFavorites) { - switch difference { - case .insert(_, let groupName, _): - state.pinnedRows[id: groupName]?.mark.isFavorite = true - state.visibleRows[id: groupName]?.mark.isFavorite = true - state.groupRows[id: groupName]?.mark.isFavorite = true - if groupName.matches(query: state.searchQuery) { - state.favoriteRows[id: groupName] = state.groupRows[id: groupName] ?? GroupsRow.State(groupName: groupName) - } - case .remove(_, let groupName, _): - state.pinnedRows[id: groupName]?.mark.isFavorite = false - state.visibleRows[id: groupName]?.mark.isFavorite = false - state.groupRows[id: groupName]?.mark.isFavorite = false - state.favoriteRows.remove(id: groupName) - } - } - return .none } } diff --git a/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeatureView.swift b/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeatureView.swift index a9a43bd..24eb235 100644 --- a/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeatureView.swift +++ b/Modules/Sources/GroupsFeature/Loaded/LoadedGroupsFeatureView.swift @@ -43,6 +43,9 @@ struct LoadedGroupsFeatureView: View { .dismissSearch(store.searchDismiss) .searchable(text: $store.searchQuery, prompt: "screen.groups.search.placeholder") .task { await store.send(.task).finish() } + .animation(.default, value: store.pinnedRows.ids) + .animation(.default, value: store.favoriteRows.ids) + .animation(.default, value: store.visibleRows.ids) } } } diff --git a/Modules/Sources/LecturersFeature/LecturersFeature.swift b/Modules/Sources/LecturersFeature/LecturersFeature.swift index 265126b..a8a6e55 100644 --- a/Modules/Sources/LecturersFeature/LecturersFeature.swift +++ b/Modules/Sources/LecturersFeature/LecturersFeature.swift @@ -23,11 +23,13 @@ public struct LecturersFeature { var lectorPresentationMode: LectorPresentationMode = .initial // MARK: Placeholder - var hasPinnedPlaceholder: Bool = false - var favoritesPlaceholderCount: Int = 0 + var hasPinnedPlaceholder: Bool { pinnedSchedule.source?.is(\.lector) ?? false } + var favoritesPlaceholderCount: Int { favoriteLecturerIDs.count } // MARK: Lecturers var lecturers: LoadingState = .initial + @SharedReader(.favoriteLecturerIDs) var favoriteLecturerIDs + @SharedReader(.pinnedSchedule) var pinnedSchedule public init() {} } @@ -37,8 +39,6 @@ public struct LecturersFeature { case showPremiumClubPinned } - case onAppear - case path(StackAction) case lecturers(LoadingActionOf) @@ -46,19 +46,12 @@ public struct LecturersFeature { } @Dependency(\.apiClient) var apiClient - @Dependency(\.favorites.currentLectorIds) var favoriteLectorIds - @Dependency(\.pinnedScheduleService.currentSchedule) var pinnedSchedule public init() {} public var body: some ReducerOf { Reduce { state, action in switch action { - case .onAppear: - state.hasPinnedPlaceholder = pinnedSchedule()?.lector != nil - state.favoritesPlaceholderCount = favoriteLectorIds.count - return .none - case .lecturers(.fetchFinished): state.presentDeferredLectorIfNeeded() return .none @@ -88,11 +81,7 @@ public struct LecturersFeature { } .load(state: \.lecturers, action: \.lecturers) { _, isRefresh in let lecturers = try await apiClient.lecturers(isRefresh) - return LoadedLecturersFeature.State( - lecturers: lecturers, - favoritesIds: favoriteLectorIds, - pinnedId: pinnedSchedule()?.lector?.id - ) + return LoadedLecturersFeature.State(lecturers: lecturers) } loaded: { LoadedLecturersFeature() } diff --git a/Modules/Sources/LecturersFeature/LecturersFeatureView.swift b/Modules/Sources/LecturersFeature/LecturersFeatureView.swift index 7cfcd6e..5b431d9 100644 --- a/Modules/Sources/LecturersFeature/LecturersFeatureView.swift +++ b/Modules/Sources/LecturersFeature/LecturersFeatureView.swift @@ -36,7 +36,6 @@ public struct LecturersFeatureView: View { } destination: { store in EntityScheduleFeatureViewV2(store: store) } - .onAppear { store.send(.onAppear) } } } } diff --git a/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeature.swift b/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeature.swift index 1e2c5cd..3f2d3bd 100644 --- a/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeature.swift +++ b/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeature.swift @@ -2,6 +2,7 @@ import Foundation import ComposableArchitecture import Collections import BsuirApi +import Favorites @Reducer public struct LoadedLecturersFeature { @@ -24,7 +25,10 @@ public struct LoadedLecturersFeature { } var pinnedRows: IdentifiedArrayOf { - guard let pinnedId, let row = visibleRows[id: pinnedId] else { return [] } + guard + let pinnedId = pinnedSchedule.source?.lector?.id, + let row = visibleRows[id: pinnedId] + else { return [] } return [row] } @@ -35,21 +39,17 @@ public struct LoadedLecturersFeature { var visibleRows: IdentifiedArrayOf = [] // MARK: State + func lector(withId id: Int) -> Employee? { lecturerRows[id: id]?.lector } - fileprivate var favoritesIds: OrderedSet - fileprivate var pinnedId: Int? + @SharedReader(.favoriteLecturerIDs) var favoritesIds + @SharedReader(.pinnedSchedule) var pinnedSchedule + fileprivate var lecturerRows: IdentifiedArrayOf = [] - init( - lecturers: [Employee], - favoritesIds: OrderedSet, - pinnedId: Int? - ) { - self.favoritesIds = favoritesIds - self.pinnedId = pinnedId + init(lecturers: [Employee]) { self.lecturerRows = IdentifiedArray( uniqueElements: lecturers .map { LecturersRow.State(lector: $0) } @@ -59,16 +59,10 @@ public struct LoadedLecturersFeature { } public enum Action: BindableAction { - case task case lecturerRows(IdentifiedActionOf) - - case _favoritesUpdate(OrderedSet) - case _pinnedUpdate(Int?) - case binding(BindingAction) } - @Dependency(\.favorites.lecturerIds) var lecturerIds @Dependency(\.pinnedScheduleService.schedule) var pinnedSchedule public var body: some ReducerOf { @@ -85,20 +79,6 @@ public struct LoadedLecturersFeature { } Reduce { state, action in switch action { - case .task: - return .merge( - listenToFavoriteUpdates(), - listenToPinnedUpdates() - ) - - case ._favoritesUpdate(let value): - state.favoritesIds = value - return .none - - case ._pinnedUpdate(let value): - state.pinnedId = value - return .none - case .lecturerRows, .binding: return .none } @@ -109,50 +89,6 @@ public struct LoadedLecturersFeature { .forEach(\.visibleRows, action: \.lecturerRows) { LecturersRow() } - .onChange(of: \.pinnedId) { oldPinned, newPinned in - Reduce { state, _ in - if let oldPinned { - state.lecturerRows[id: oldPinned]?.mark.isPinned = false - state.visibleRows[id: oldPinned]?.mark.isPinned = false - } - if let newPinned { - state.lecturerRows[id: newPinned]?.mark.isPinned = true - state.visibleRows[id: newPinned]?.mark.isPinned = true - } - return .none - } - } - .onChange(of: \.favoritesIds) { oldFavorites, newFavorites in - Reduce { state, _ in - for difference in newFavorites.difference(from: oldFavorites) { - switch difference { - case .insert(_, let id, _): - state.lecturerRows[id: id]?.mark.isFavorite = true - state.visibleRows[id: id]?.mark.isFavorite = true - case .remove(_, let id, _): - state.lecturerRows[id: id]?.mark.isFavorite = false - state.visibleRows[id: id]?.mark.isFavorite = false - } - } - return .none - } - } - } - - private func listenToFavoriteUpdates() -> Effect { - return .run { send in - for await value in lecturerIds.removeDuplicates().values { - await send(._favoritesUpdate(value), animation: .default) - } - } - } - - private func listenToPinnedUpdates() -> Effect { - return .run { send in - for await value in pinnedSchedule().map(\.?.lector?.id).removeDuplicates().values { - await send(._pinnedUpdate(value), animation: .default) - } - } } } diff --git a/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeatureView.swift b/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeatureView.swift index c9e134f..64ff632 100644 --- a/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeatureView.swift +++ b/Modules/Sources/LecturersFeature/Loaded/LoadedLecturersFeatureView.swift @@ -9,18 +9,22 @@ struct LoadedLecturersFeatureView: View { var body: some View { WithPerceptionTracking { List { - if !store.pinnedRows.isEmpty { - Section("screen.lecturers.pinned.section.header") { - ForEach(store.scope(state: \.pinnedRows, action: \.lecturerRows)) { store in - LecturersRowView(store: store) + WithPerceptionTracking { + if !store.pinnedRows.isEmpty { + Section("screen.lecturers.pinned.section.header") { + ForEach(store.scope(state: \.pinnedRows, action: \.lecturerRows)) { store in + LecturersRowView(store: store) + } } } } - if !store.favoriteRows.isEmpty { - Section("screen.lecturers.favorites.section.header") { - ForEach(store.scope(state: \.favoriteRows, action: \.lecturerRows)) { store in - LecturersRowView(store: store) + WithPerceptionTracking { + if !store.favoriteRows.isEmpty { + Section("screen.lecturers.favorites.section.header") { + ForEach(store.scope(state: \.favoriteRows, action: \.lecturerRows)) { store in + LecturersRowView(store: store) + } } } } @@ -42,7 +46,9 @@ struct LoadedLecturersFeatureView: View { } .dismissSearch(store.searchDismiss) .searchable(text: $store.searchQuery, prompt: "screen.lecturers.search.placeholder") - .task { await store.send(.task).finish() } + .animation(.default, value: store.favoritesIds) + .animation(.default, value: store.pinnedSchedule) + .animation(.default, value: store.visibleRows) } } } diff --git a/Modules/Sources/ScheduleCore/PinnedScheduleService.swift b/Modules/Sources/ScheduleCore/PinnedScheduleService.swift index 95c3116..43d8e6e 100644 --- a/Modules/Sources/ScheduleCore/PinnedScheduleService.swift +++ b/Modules/Sources/ScheduleCore/PinnedScheduleService.swift @@ -77,7 +77,7 @@ extension PinnedScheduleService { /// at some point it sends notification that pinned was removed even if new non-nil value was set after /// to prevent such situation I prefer to always store `something` even if it is garbage dictionary, it seems to work well @CasePathable -private enum CloudSyncableScheduleSource: Codable { +public enum CloudSyncableScheduleSource: Equatable, Codable { case source(ScheduleSource) case nothing @@ -89,7 +89,14 @@ private enum CloudSyncableScheduleSource: Codable { } } - init(from decoder: any Decoder) throws { + public var source: ScheduleSource? { + guard case .source(let source) = self else { + return nil + } + return source + } + + public init(from decoder: any Decoder) throws { do { self = .source(try ScheduleSource(from: decoder)) } catch { @@ -97,7 +104,7 @@ private enum CloudSyncableScheduleSource: Codable { } } - func encode(to encoder: any Encoder) throws { + public func encode(to encoder: any Encoder) throws { switch self { case .source(let source): try source.encode(to: encoder) diff --git a/Modules/Sources/ScheduleFeature/Marked/MarkedScheduleRowFeature.swift b/Modules/Sources/ScheduleFeature/Marked/MarkedScheduleRowFeature.swift index 5b5c551..c721b96 100644 --- a/Modules/Sources/ScheduleFeature/Marked/MarkedScheduleRowFeature.swift +++ b/Modules/Sources/ScheduleFeature/Marked/MarkedScheduleRowFeature.swift @@ -2,20 +2,35 @@ import Foundation import ComposableArchitecture import ScheduleCore +// TODO: Sync state with Shared + @Reducer public struct MarkedScheduleRowFeature { @ObservableState public struct State: Equatable { let source: ScheduleSource - public var isFavorite: Bool - public var isPinned: Bool + + public var isFavorite: Bool { + switch source { + case .group(let name): + favoriteGroupNames.contains(name) + case .lector(let employee): + favoriteLecturerIDs.contains(employee.id) + } + } + + public var isPinned: Bool { + pinnedSchedule.source == source + } + @Presents var alert: AlertState? + @SharedReader(.pinnedSchedule) var pinnedSchedule + @SharedReader(.favoriteGroupNames) var favoriteGroupNames + @SharedReader(.favoriteLecturerIDs) var favoriteLecturerIDs + public init(source: ScheduleSource) { self.source = source - @Dependency(\.scheduleMarkingService) var scheduleMarkingService - self.isFavorite = scheduleMarkingService.isCurrentlyFavorite(source) - self.isPinned = scheduleMarkingService.isCurrentlyPinned(source) } } @@ -28,9 +43,6 @@ public struct MarkedScheduleRowFeature { case togglePinnedTapped case removeButtonTapped - case _setIsFavorite(Bool) - case _setIsPinned(Bool) - case delegate(DelegateAction) case alert(PresentationAction) } @@ -44,11 +56,10 @@ public struct MarkedScheduleRowFeature { Reduce { state, action in switch action { case .toggleFavoriteTapped: - state.isFavorite.toggle() return .run { [isFavorite = state.isFavorite, source = state.source] _ in await isFavorite - ? scheduleMarkingService.favorite(source) - : scheduleMarkingService.unfavorite(source) + ? scheduleMarkingService.unfavorite(source) + : scheduleMarkingService.favorite(source) } case .togglePinnedTapped: @@ -57,29 +68,18 @@ public struct MarkedScheduleRowFeature { return .none } - state.isPinned.toggle() return .run { [isPinned = state.isPinned, source = state.source] _ in await isPinned - ? scheduleMarkingService.pin(source) - : scheduleMarkingService.unpin(source) + ? scheduleMarkingService.unpin(source) + : scheduleMarkingService.pin(source) } case .removeButtonTapped: - state.isFavorite = false - state.isPinned = false return .run { [source = state.source] _ in await scheduleMarkingService.unfavorite(source) await scheduleMarkingService.unpin(source) } - case ._setIsFavorite(let value): - state.isFavorite = value - return .none - - case ._setIsPinned(let value): - state.isPinned = value - return .none - case .alert(.presented(.learnAboutPremiumClubButtonTapped)): return .send(.delegate(.showPremiumClub))