diff --git a/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift b/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift index d5af1df8..f1ff5846 100644 --- a/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift +++ b/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift @@ -201,10 +201,11 @@ private extension TodoListFeature { func fetchEffect( query: TodoQuery, cursor: TodoCursor?, - resetsPagination: Bool = true + resetsPagination: Bool = true, + showsIndicator: Bool = true ) -> Effect { .concatenate( - .send(.loading(.begin(target: .default, mode: .delayed))), + showsIndicator ? .send(.loading(.begin(target: .default, mode: .delayed))) : .none, .run { [fetchTodosUseCase] send in do { let page = try await fetchTodosUseCase.execute(query, cursor: cursor) @@ -216,12 +217,16 @@ private extension TodoListFeature { nextCursor: page.nextCursor ))) await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch is CancellationError { return } catch { await send(.store(.setAlert(true))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } } ) @@ -277,7 +282,9 @@ private extension TodoListFeature { state: inout State ) -> Effect { switch action { - case .refresh, .onAppear: + case .refresh: + return fetchEffect(query: state.query, cursor: nil, showsIndicator: false) + case .onAppear: return fetchEffect(query: state.query, cursor: nil) case .swipeTodo(let todo): return swipeTodoEffect(todo, state: &state) diff --git a/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift index 9e5f9bf2..99fd0a28 100644 --- a/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift @@ -171,7 +171,7 @@ struct TodoListView: View { } .offset(y: headerOffset) } - .refreshable { store.send(.view(.refresh)) } + .refreshable { await store.send(.view(.refresh)).finish() } .scrollDisabled(visibleTodos.isEmpty || store.state.isLoading) if store.state.isLoading { diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift b/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift index 7250fafe..01164db2 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift @@ -108,10 +108,11 @@ struct ProfileFeature { if !settings.isEmpty { state.selectedActivityKinds = settings } + let showsIndicator = if case .fetchData = action { true } else { false } if let selectedQuarterStart = state.selectedQuarterStart { return .merge( fetchUserDataEffect(), - fetchActivityQuarterEffect(selectedQuarterStart) + fetchActivityQuarterEffect(selectedQuarterStart, showsIndicator: showsIndicator) ) } return fetchUserDataEffect() @@ -220,9 +221,14 @@ private extension ProfileFeature { } } - func fetchActivityQuarterEffect(_ quarterStart: Date) -> Effect { + func fetchActivityQuarterEffect( + _ quarterStart: Date, + showsIndicator: Bool = true + ) -> Effect { .run { [fetchTodosUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { let data = try await ProfileHeatmapBuilder.fetchQuarterActivityData( from: quarterStart, @@ -237,9 +243,13 @@ private extension ProfileFeature { ) ) ) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.setAlert(true)) } } diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift index 911f3340..6cbde9d3 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift @@ -58,6 +58,7 @@ struct PushNotificationListFeature { case loading(LoadingFeature.Action) enum ViewAction: Equatable { + case refresh case fetchNotifications case loadNextPage case deleteNotification(PushNotificationItem) @@ -83,7 +84,6 @@ struct PushNotificationListFeature { case syncNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?, hasMore: Bool) case setNotificationHidden(String, Bool) case setNotificationRead(String, Bool) - case observeNotifications(PushNotificationQuery, Int) } } @@ -151,6 +151,14 @@ private extension PushNotificationListFeature { state: inout State ) -> Effect { switch action { + case .refresh: + state.nextCursor = nil + return fetchNotificationsEffect( + query: state.query, + cursor: nil, + existingCount: 0, + showsIndicator: false + ) case .fetchNotifications: state.nextCursor = nil return fetchNotificationsEffect(query: state.query, cursor: nil, existingCount: 0) @@ -252,8 +260,6 @@ private extension PushNotificationListFeature { if let index = state.notifications.firstIndex(where: { $0.id == notificationId }) { state.notifications[index].isRead = isRead } - case .observeNotifications(let query, let limit): - return observeNotificationsEffect(query: query, limit: limit) } return .none @@ -269,11 +275,36 @@ private extension PushNotificationListFeature { func fetchNotificationsEffect( query: PushNotificationQuery, cursor: PushNotificationCursor?, - existingCount: Int + existingCount: Int, + showsIndicator: Bool = true ) -> Effect { let limit = max(query.pageSize, existingCount) - let fetchEffect: Effect = .run { [fetchPushNotificationsUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + let fetchEffect = fetchNotificationsPageEffect(query: query, cursor: cursor, showsIndicator: showsIndicator) + let observeEffect = observeNotificationsEffect( + query: query, + limit: max(limit, existingCount + query.pageSize) + ) + + if cursor == nil { + return .concatenate( + .cancel(id: CancelID.observeNotifications), + fetchEffect, + observeEffect + ) + } + + return fetchEffect + } + + func fetchNotificationsPageEffect( + query: PushNotificationQuery, + cursor: PushNotificationCursor?, + showsIndicator: Bool = true + ) -> Effect { + .run { [fetchPushNotificationsUseCase] send in + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { let page = try await fetchPushNotificationsUseCase.execute(query, cursor: cursor) if cursor == nil { @@ -286,23 +317,17 @@ private extension PushNotificationListFeature { )) ) await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil))) - await send(.store(.observeNotifications(query, max(limit, existingCount + page.items.count)))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.store(.setAlert)) } } .cancellable(id: CancelID.fetchNotifications, cancelInFlight: true) - - if cursor == nil { - return .concatenate( - .cancel(id: CancelID.observeNotifications), - fetchEffect - ) - } - - return fetchEffect } func observeNotificationsEffect( diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift index d6810f7c..d3c83677 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift @@ -38,7 +38,7 @@ struct PushNotificationListView: View { headerOffset = max(0, -offset) } .safeAreaInset(edge: .top) { safeAreaHeader } - .refreshable { store.send(.view(.fetchNotifications)) } + .refreshable { await store.send(.view(.refresh)).finish() } .navigationTitle(String(localized: "nav_push_notifications")) .listStyle(.plain) } diff --git a/Application/DevLogPresentation/Sources/Today/TodayFeature.swift b/Application/DevLogPresentation/Sources/Today/TodayFeature.swift index 3c804c05..272b74b2 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayFeature.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayFeature.swift @@ -185,7 +185,9 @@ struct TodayFeature { return updateDisplayOptionsEffect(state.displayOptions) case .binding: break - case .refresh, .fetchData: + case .refresh: + return fetchTodosEffect(showsIndicator: false) + case .fetchData: return fetchTodosEffect() case .setSectionScope(let scope): if state.selectedSectionScope == scope, scope != .all { @@ -255,9 +257,11 @@ private enum UpdateTodayDisplayOptionsUseCaseKey: DependencyKey { } private extension TodayFeature { - func fetchTodosEffect() -> Effect { + func fetchTodosEffect(showsIndicator: Bool = true) -> Effect { .run { [fetchTodosUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { async let todosWithDueDatePage = fetchTodosUseCase.execute( TodoQuery( @@ -284,9 +288,13 @@ private extension TodayFeature { let todosWithDueDate = try await todosWithDueDatePage.items.compactMap(TodayTodoItem.init(from:)) let todosWithoutDueDate = try await todosWithoutDueDatePage.items.compactMap(TodayTodoItem.init(from:)) await send(.store(.setTodos(todosWithDueDate + todosWithoutDueDate))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.store(.setAlert)) } } diff --git a/Application/DevLogPresentation/Sources/Today/TodayView.swift b/Application/DevLogPresentation/Sources/Today/TodayView.swift index 834894d4..29eba6d7 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayView.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayView.swift @@ -39,7 +39,7 @@ struct TodayView: View { .navigationTitle(String(localized: "nav_today")) .toolbar { toolbarContent } .background(NavigationBarConfigurator()) - .refreshable { store.send(.refresh) } + .refreshable { await store.send(.refresh).finish() } .alert($store.scope(state: \.alert, action: \.alert)) .overlay { if store.isLoading {