Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Application/Core/Sources/PushNotificationQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ public struct PushNotificationQuery: Equatable {
public var timeFilter: TimeFilter
public var unreadOnly: Bool
public var pageSize: Int
public var referenceDate: Date

public init(
sortOrder: SortOrder,
timeFilter: TimeFilter,
unreadOnly: Bool,
pageSize: Int
pageSize: Int,
referenceDate: Date = Date()
) {
self.sortOrder = sortOrder
self.timeFilter = timeFilter
self.unreadOnly = unreadOnly
self.pageSize = pageSize
self.referenceDate = referenceDate
}

public static let `default` = PushNotificationQuery(
Expand Down Expand Up @@ -67,14 +70,14 @@ public extension PushNotificationQuery.TimeFilter {
}
}

var thresholdDate: Date? {
func thresholdDate(relativeTo referenceDate: Date) -> Date? {
switch self {
case .none:
return nil
case .hours(let value):
return Date().addingTimeInterval(-Double(value) * 3600.0)
return referenceDate.addingTimeInterval(-Double(value) * 3600.0)
case .days(let value):
return Date().addingTimeInterval(-Double(value) * 86400.0)
return referenceDate.addingTimeInterval(-Double(value) * 86400.0)
}
}
}
47 changes: 21 additions & 26 deletions Application/Infra/Sources/Service/PushNotificationServiceImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,25 +135,12 @@ final class PushNotificationServiceImpl: PushNotificationService {
])
}

let pageLimit = notificationQuery.pageSize
let snapshot = try await firestoreQuery
.limit(to: notificationQuery.pageSize)
.limit(to: pageLimit + 1)
.getDocuments()

let items = snapshot.documents.compactMap { makeResponse(from: $0) }

let nextCursor: PushNotificationCursorDTO? = snapshot.documents.last.map { document in
guard let receivedAt = document.data()[PushNotificationFieldKey.receivedAt.rawValue] as? Timestamp
else {
return nil
}

return PushNotificationCursorDTO(
receivedAt: receivedAt.dateValue(),
documentID: document.documentID
)
} ?? nil

return PushNotificationPageResponse(items: items, nextCursor: nextCursor)
return makePageResponse(from: snapshot.documents, limit: pageLimit)
} catch {
logger.error("Failed to request notifications", error: error)
record(error, code: .requestNotifications)
Expand All @@ -170,7 +157,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
let subject = PassthroughSubject<PushNotificationPageResponse, Error>()
let pageLimit = max(query.pageSize, limit)
let listener = makeQuery(uid: uid, query: query)
.limit(to: pageLimit)
.limit(to: pageLimit + 1)
.addSnapshotListener { [weak self] snapshot, error in
if let error {
Self.record(error, code: .observeNotifications)
Expand All @@ -180,14 +167,7 @@ final class PushNotificationServiceImpl: PushNotificationService {

guard let self, let snapshot else { return }

let items = snapshot.documents.compactMap { self.makeResponse(from: $0) }
let nextCursor = self.makeNextCursor(from: snapshot.documents.last)
subject.send(
PushNotificationPageResponse(
items: items,
nextCursor: nextCursor
)
)
subject.send(self.makePageResponse(from: snapshot.documents, limit: pageLimit))
}

return subject
Expand Down Expand Up @@ -317,7 +297,9 @@ private extension PushNotificationServiceImpl {
var firestoreQuery: Query = store.collection(FirestorePath.notifications(uid))
.whereField(PushNotificationFieldKey.isDeleted.rawValue, isEqualTo: false)

if let thresholdDate = query.timeFilter.thresholdDate {
if let thresholdDate = query.timeFilter.thresholdDate(
relativeTo: query.referenceDate
) {
firestoreQuery = firestoreQuery.whereField(
"receivedAt",
isGreaterThanOrEqualTo: Timestamp(date: thresholdDate)
Expand Down Expand Up @@ -347,6 +329,19 @@ private extension PushNotificationServiceImpl {
)
}

func makePageResponse(
from documents: [QueryDocumentSnapshot],
limit: Int
) -> PushNotificationPageResponse {
let pageDocuments = Array(documents.prefix(limit))
let items = pageDocuments.compactMap { makeResponse(from: $0) }
let nextCursor = limit < documents.count
? makeNextCursor(from: pageDocuments.last)
: nil

return PushNotificationPageResponse(items: items, nextCursor: nextCursor)
}

func makeResponse(from snapshot: QueryDocumentSnapshot) -> PushNotificationResponse? {
let data = snapshot.data()
if (data[PushNotificationFieldKey.isDeleted.rawValue] as? Bool) == true {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ struct PushNotificationListFeature {
@Presents var alert: AlertState<Never>?
@Presents var sheet: SheetState?
var notifications: [PushNotificationItem] = []
var hasMore = false
var nextCursor: PushNotificationCursor?
var query: PushNotificationQuery
var selectedNotificationId: String?
Expand Down Expand Up @@ -58,6 +57,8 @@ struct PushNotificationListFeature {
case loading(LoadingFeature.Action)

enum ViewAction: Equatable {
case stopObserving
case startObserving
case refresh
case fetchNotifications
case loadNextPage
Expand All @@ -79,9 +80,7 @@ struct PushNotificationListFeature {
enum StoreAction: Equatable {
case setAlert
case appendNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?)
case resetPagination
case setHasMore(Bool)
case syncNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?, hasMore: Bool)
case replaceNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?)
case setNotificationHidden(String, Bool)
case setNotificationRead(String, Bool)
}
Expand All @@ -98,6 +97,7 @@ struct PushNotificationListFeature {
@Dependency(\.undoDeletePushNotificationUseCase) var undoDeletePushNotificationUseCase
@Dependency(\.togglePushNotificationReadUseCase) var togglePushNotificationReadUseCase
@Dependency(\.updatePushNotificationQueryUseCase) var updatePushNotificationQueryUseCase
@Dependency(\.date.now) var now

var body: some ReducerOf<Self> {
Scope(state: \.loading, action: \.loading) {
Expand All @@ -116,6 +116,7 @@ struct PushNotificationListFeature {
break
case .binding(\.query.timeFilter):
state.nextCursor = nil
state.query.referenceDate = now
return refreshForQueryChangeEffect(query: state.query)
case .binding:
break
Expand Down Expand Up @@ -151,23 +152,32 @@ private extension PushNotificationListFeature {
state: inout State
) -> Effect<Action> {
switch action {
case .stopObserving:
return .cancel(id: CancelID.observeNotifications)
case .startObserving:
return observeNotificationsEffect(
query: state.query,
limit: state.query.pageSize
)
case .refresh:
state.nextCursor = nil
return fetchNotificationsEffect(
query: state.query,
cursor: nil,
existingCount: 0,
showsIndicator: false
state.query.referenceDate = now
return .concatenate(
.cancel(id: CancelID.observeNotifications),
fetchNotificationsPageEffect(
query: state.query,
cursor: nil,
showsIndicator: false
)
)
case .fetchNotifications:
state.nextCursor = nil
return fetchNotificationsEffect(query: state.query, cursor: nil, existingCount: 0)
return fetchNotificationsPageEffect(query: state.query, cursor: nil)
Comment thread
opficdev marked this conversation as resolved.
case .loadNextPage:
guard state.hasMore, !state.isLoading else { return .none }
return fetchNotificationsEffect(
guard state.nextCursor != nil, !state.isLoading else { return .none }
return fetchNotificationsPageEffect(
query: state.query,
cursor: state.nextCursor,
existingCount: state.notifications.count
cursor: state.nextCursor
)
case .deleteNotification(let item):
guard state.notifications.contains(where: { $0.id == item.id }) else { return .none }
Expand All @@ -193,14 +203,17 @@ private extension PushNotificationListFeature {
}
case .toggleSortOption:
state.query.sortOrder = state.query.sortOrder == .latest ? .oldest : .latest
state.query.referenceDate = now
state.nextCursor = nil
return refreshForQueryChangeEffect(query: state.query)
case .toggleUnreadOnly:
state.query.unreadOnly.toggle()
state.query.referenceDate = now
state.nextCursor = nil
return refreshForQueryChangeEffect(query: state.query)
case .resetFilters:
state.query = .default
state.query.referenceDate = now
state.nextCursor = nil
return refreshForQueryChangeEffect(query: state.query)
case .selectNotification(let notificationId):
Expand Down Expand Up @@ -242,18 +255,12 @@ private extension PushNotificationListFeature {
incomingNotifications: notifications
))
state.nextCursor = nextCursor
case .resetPagination:
state.notifications = []
state.nextCursor = nil
case .setHasMore(let value):
state.hasMore = value
case .syncNotifications(let notifications, let nextCursor, let hasMore):
case .replaceNotifications(let notifications, let nextCursor):
state.notifications = Self.mergedHiddenNotifications(
currentNotifications: state.notifications,
incomingNotifications: notifications
)
state.nextCursor = nextCursor
state.hasMore = hasMore
case .setNotificationHidden(let notificationId, let isHidden):
Self.setNotificationHidden(&state, notificationId: notificationId, isHidden: isHidden)
case .setNotificationRead(let notificationId, let isRead):
Expand All @@ -268,32 +275,12 @@ private extension PushNotificationListFeature {
func refreshForQueryChangeEffect(query: PushNotificationQuery) -> Effect<Action> {
.merge(
updateQueryEffect(query: query),
fetchNotificationsEffect(query: query, cursor: nil, existingCount: 0)
)
}

func fetchNotificationsEffect(
query: PushNotificationQuery,
cursor: PushNotificationCursor?,
existingCount: Int,
showsIndicator: Bool = true
) -> Effect<Action> {
let limit = max(query.pageSize, existingCount)
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
.concatenate(
.send(.view(.stopObserving)),
fetchNotificationsPageEffect(query: query, cursor: nil),
.send(.view(.startObserving))
)
}

return fetchEffect
)
}

func fetchNotificationsPageEffect(
Expand All @@ -307,16 +294,22 @@ private extension PushNotificationListFeature {
}
do {
let page = try await fetchPushNotificationsUseCase.execute(query, cursor: cursor)
let notifications = page.items.map(PushNotificationItem.init(from:))
if cursor == nil {
await send(.store(.resetPagination))
await send(
.store(.replaceNotifications(
notifications,
nextCursor: page.nextCursor
))
)
} else {
await send(
.store(.appendNotifications(
notifications,
nextCursor: page.nextCursor
))
)
}
await send(
.store(.appendNotifications(
page.items.map(PushNotificationItem.init(from:)),
nextCursor: page.nextCursor
))
)
await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil)))
if showsIndicator {
await send(.loading(.end(target: .default, mode: .delayed)))
}
Expand All @@ -339,8 +332,7 @@ private extension PushNotificationListFeature {
let publisher = try fetchPushNotificationsUseCase.observe(query, limit: limit)
for try await page in publisher.values {
let items = page.items.map(PushNotificationItem.init(from:))
let hasMore = items.count == max(query.pageSize, limit) && page.nextCursor != nil
await send(.store(.syncNotifications(items, nextCursor: page.nextCursor, hasMore: hasMore)))
await send(.store(.replaceNotifications(items, nextCursor: page.nextCursor)))
}
} catch is CancellationError {
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public struct PushNotificationListView: View {
headerOffset = max(0, -offset)
}
.safeAreaInset(edge: .top) { safeAreaHeader }
.refreshable { await store.send(.view(.refresh)).finish() }
.refreshable(isEnabled: PullToRefreshAvailability.isEnabled) {
let task = store.send(.view(.refresh))
await task.finish()
store.send(.view(.startObserving))
}
.navigationTitle(String(localized: "nav_push_notifications"))
.listStyle(.plain)
}
Expand Down Expand Up @@ -124,7 +128,7 @@ public struct PushNotificationListView: View {
)
.onAppear {
let lastId = notifications.last?.id
if notification.id == lastId, store.hasMore {
if notification.id == lastId, store.nextCursor != nil {
store.send(.view(.loadNextPage))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public final class PushNotificationListViewCoordinator {
private let container: DIContainer
@ObservationIgnored
private var todoDetailStore: StoreOf<TodoDetailFeature>?
@ObservationIgnored
private var fetchNotificationsTask: Task<Void, Never>?

public init(container: DIContainer) {
self.container = container
Expand All @@ -42,7 +44,15 @@ public final class PushNotificationListViewCoordinator {
}

public func fetchData() {
store.send(.view(.fetchNotifications))
fetchNotificationsTask?.cancel()
store.send(.view(.stopObserving))
let query = store.query
let task = store.send(.view(.fetchNotifications))
fetchNotificationsTask = Task { [store] in
await task.finish()
guard !Task.isCancelled, store.query == query else { return }
store.send(.view(.startObserving))
}
}

public func makeTodoDetailStore(todoId: String) -> StoreOf<TodoDetailFeature> {
Expand Down
Loading
Loading