Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,13 @@ private extension DomainAssembler {
container.register(UpdateProfileHeatmapActivityTypesUseCase.self) {
UpdateProfileHeatmapActivityTypesUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(FetchTodayDisplayOptionsUseCase.self) {
FetchTodayDisplayOptionsUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(UpdateTodayDisplayOptionsUseCase.self) {
UpdateTodayDisplayOptionsUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}
}
}
3 changes: 2 additions & 1 deletion DevLog/Data/DTO/TodoCursorDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

struct TodoCursorDTO {
let orderedAt: Date
let primarySortDate: Date?
let secondarySortDate: Date?
let documentID: String
}
6 changes: 4 additions & 2 deletions DevLog/Data/Mapper/TodoMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ extension TodoResponse {
extension TodoCursorDTO {
func toDomain() -> TodoCursor {
TodoCursor(
orderedAt: orderedAt,
primarySortDate: primarySortDate,
secondarySortDate: secondarySortDate,
documentID: documentID
)
}

static func fromDomain(_ cursor: TodoCursor) -> Self {
TodoCursorDTO(
orderedAt: cursor.orderedAt,
primarySortDate: cursor.primarySortDate,
secondarySortDate: cursor.secondarySortDate,
documentID: cursor.documentID
)
}
Expand Down
21 changes: 21 additions & 0 deletions DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {
static let pushTimeFilter = "PushNotification.timeFilter"
static let pushUnreadOnly = "PushNotification.showUnreadOnly"
static let profileHeatmapActivityTypes = "Profile.heatmap.activityTypes"
static let todayDueDateVisibility = "Today.dueDateVisibility"
static let todayFocusVisibility = "Today.focusVisibility"
}

private let store: UserDefaultsStore
Expand Down Expand Up @@ -101,4 +103,23 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {
func setProfileHeatmapActivityTypes(_ activityTypes: [String]) {
store.setStringArray(activityTypes, forKey: Key.profileHeatmapActivityTypes)
}

func todayDisplayOptions() -> TodayDisplayOptions {
let dueDateVisibilityRawValue = store.string(forKey: Key.todayDueDateVisibility)
let focusVisibilityRawValue = store.string(forKey: Key.todayFocusVisibility)

return TodayDisplayOptions(
dueDateVisibility: TodayDisplayOptions.DueDateVisibility(
rawValue: dueDateVisibilityRawValue ?? ""
) ?? .all,
focusVisibility: TodayDisplayOptions.FocusVisibility(
rawValue: focusVisibilityRawValue ?? ""
) ?? .all
)
}

func setTodayDisplayOptions(_ options: TodayDisplayOptions) {
store.setString(options.dueDateVisibility.rawValue, forKey: Key.todayDueDateVisibility)
store.setString(options.focusVisibility.rawValue, forKey: Key.todayFocusVisibility)
}
}
29 changes: 29 additions & 0 deletions DevLog/Domain/Entity/TodayDisplayOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// TodayDisplayOptions.swift
// DevLog
//
// Created by opfic on 3/6/26.
//

import Foundation

struct TodayDisplayOptions: Equatable {
enum DueDateVisibility: String, CaseIterable, Equatable {
case all
case withDueDateOnly
case withoutDueDateOnly
}

enum FocusVisibility: String, CaseIterable, Equatable {
case all
case focusedOnly
}

var dueDateVisibility: DueDateVisibility
var focusVisibility: FocusVisibility

static let `default` = TodayDisplayOptions(
dueDateVisibility: .all,
focusVisibility: .all
)
}
3 changes: 2 additions & 1 deletion DevLog/Domain/Entity/TodoCursor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

struct TodoCursor {
let orderedAt: Date
let primarySortDate: Date?
let secondarySortDate: Date?
let documentID: String
}
12 changes: 12 additions & 0 deletions DevLog/Domain/Entity/TodoQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ struct TodoQuery: Equatable {
enum SortTarget: Equatable, Hashable {
case createdAt
case updatedAt
case dueDate

var fieldName: String {
switch self {
case .createdAt:
return "createdAt"
case .updatedAt:
return "updatedAt"
case .dueDate:
return "dueDate"
}
}
}
Expand Down Expand Up @@ -48,10 +51,17 @@ struct TodoQuery: Equatable {
}
}

enum DueDateFilter: Equatable, Hashable {
case all
case withDueDate
case withoutDueDate
}

var kind: TodoKind?
var keyword: String?
var isPinned: Bool?
var completionFilter: CompletionFilter
var dueDateFilter: DueDateFilter
var createdAtFrom: Date?
var createdAtTo: Date?
var sortTarget: SortTarget
Expand All @@ -64,6 +74,7 @@ struct TodoQuery: Equatable {
keyword: String? = nil,
isPinned: Bool? = nil,
completionFilter: CompletionFilter = .all,
dueDateFilter: DueDateFilter = .all,
createdAtFrom: Date? = nil,
createdAtTo: Date? = nil,
sortTarget: SortTarget = .createdAt,
Expand All @@ -75,6 +86,7 @@ struct TodoQuery: Equatable {
self.keyword = keyword
self.isPinned = isPinned
self.completionFilter = completionFilter
self.dueDateFilter = dueDateFilter
self.createdAtFrom = createdAtFrom
self.createdAtTo = createdAtTo
self.sortTarget = sortTarget
Expand Down
3 changes: 3 additions & 0 deletions DevLog/Domain/Protocol/UserPreferencesRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ protocol UserPreferencesRepository {

func profileHeatmapActivityTypes() -> [String]
func setProfileHeatmapActivityTypes(_ activityTypes: [String])

func todayDisplayOptions() -> TodayDisplayOptions
func setTodayDisplayOptions(_ options: TodayDisplayOptions)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// FetchTodayDisplayOptionsUseCase.swift
// DevLog
//
// Created by opfic on 3/6/26.
//

protocol FetchTodayDisplayOptionsUseCase {
func execute() -> TodayDisplayOptions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// FetchTodayDisplayOptionsUseCaseImpl.swift
// DevLog
//
// Created by opfic on 3/6/26.
//

final class FetchTodayDisplayOptionsUseCaseImpl: FetchTodayDisplayOptionsUseCase {
private let repository: UserPreferencesRepository

init(_ repository: UserPreferencesRepository) {
self.repository = repository
}

func execute() -> TodayDisplayOptions {
repository.todayDisplayOptions()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// UpdateTodayDisplayOptionsUseCase.swift
// DevLog
//
// Created by opfic on 3/6/26.
//

protocol UpdateTodayDisplayOptionsUseCase {
func execute(_ options: TodayDisplayOptions)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// UpdateTodayDisplayOptionsUseCaseImpl.swift
// DevLog
//
// Created by opfic on 3/6/26.
//

final class UpdateTodayDisplayOptionsUseCaseImpl: UpdateTodayDisplayOptionsUseCase {
private let repository: UserPreferencesRepository

init(_ repository: UserPreferencesRepository) {
self.repository = repository
}

func execute(_ options: TodayDisplayOptions) {
repository.setTodayDisplayOptions(options)
}
}
98 changes: 81 additions & 17 deletions DevLog/Infra/Service/TodoService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class TodoService {
query.kind != nil ? "kind=\(query.kind!.rawValue)" : nil,
query.isPinned != nil ? "pinned=\(query.isPinned!)" : nil,
query.completionFilter.isCompletedValue != nil ? "completed=\(query.completionFilter.isCompletedValue!)" : nil,
query.dueDateFilter != .all ? "dueDateFilter=\(query.dueDateFilter)" : nil,
query.createdAtFrom != nil ? "createdAtFrom=\(query.createdAtFrom!)" : nil,
query.createdAtTo != nil ? "createdAtTo=\(query.createdAtTo!)" : nil,
"pageSize=\(query.pageSize)",
Expand All @@ -35,10 +36,7 @@ final class TodoService {
]
logger.info("Fetching todo page: \(logComponents.compactMap { $0 }.joined(separator: ", "))")

var firestoreQuery: Query = store
.collection("users/\(uid)/todoLists/")
.order(by: query.sortTarget.fieldName, descending: query.sortOrder.isDescending)
.order(by: FieldPath.documentID())
var firestoreQuery: Query = makeOrderedQuery(uid: uid, query: query)

if let kind = query.kind {
firestoreQuery = firestoreQuery.whereField("kind", isEqualTo: kind.rawValue)
Expand All @@ -52,6 +50,18 @@ final class TodoService {
firestoreQuery = firestoreQuery.whereField("isCompleted", isEqualTo: isCompleted)
}

switch query.dueDateFilter {
case .all:
break
case .withDueDate:
firestoreQuery = firestoreQuery.whereField(
"dueDate",
isGreaterThan: Timestamp(date: Date(timeIntervalSince1970: 0))
)
case .withoutDueDate:
firestoreQuery = firestoreQuery.whereField("dueDate", isEqualTo: NSNull())
}

if let createdAtFrom = query.createdAtFrom {
firestoreQuery = firestoreQuery.whereField(
"createdAt",
Expand All @@ -74,10 +84,7 @@ final class TodoService {
while true {
var pageQuery = firestoreQuery
if let pageCursor {
pageQuery = pageQuery.start(after: [
Timestamp(date: pageCursor.orderedAt),
pageCursor.documentID
])
pageQuery = pageQuery.start(after: cursorValues(for: query, cursor: pageCursor))
}

pageQuery = pageQuery.limit(to: query.pageSize)
Expand All @@ -91,7 +98,7 @@ final class TodoService {
guard let lastDocument = snapshot.documents.last,
let nextCursor = makeCursor(
from: lastDocument,
orderField: query.sortTarget.fieldName
query: query
) else {
break
}
Expand All @@ -103,17 +110,14 @@ final class TodoService {
}

if let cursor {
firestoreQuery = firestoreQuery.start(after: [
Timestamp(date: cursor.orderedAt),
cursor.documentID
])
firestoreQuery = firestoreQuery.start(after: cursorValues(for: query, cursor: cursor))
}

firestoreQuery = firestoreQuery.limit(to: query.pageSize)
let snapshot = try await firestoreQuery.getDocuments()
let items = snapshot.documents.compactMap { makeResponse(from: $0) }
let nextCursor = snapshot.documents.last.flatMap {
makeCursor(from: $0, orderField: query.sortTarget.fieldName)
makeCursor(from: $0, query: query)
}

return TodoPageResponse(items: items, nextCursor: nextCursor)
Expand Down Expand Up @@ -195,16 +199,76 @@ final class TodoService {
}

private extension TodoService {
func makeOrderedQuery(uid: String, query: TodoQuery) -> Query {
let collection = store.collection("users/\(uid)/todoLists/")

switch query.sortTarget {
case .dueDate:
return collection
.order(by: query.sortTarget.fieldName, descending: query.sortOrder.isDescending)
.order(by: "updatedAt", descending: true)
.order(by: FieldPath.documentID())
case .createdAt, .updatedAt:
return collection
.order(by: query.sortTarget.fieldName, descending: query.sortOrder.isDescending)
.order(by: FieldPath.documentID())
}
}

func cursorValues(
for query: TodoQuery,
cursor: TodoCursorDTO
) -> [Any] {
let primaryValue: Any = cursor.primarySortDate.map { Timestamp(date: $0) } ?? NSNull()

switch query.sortTarget {
case .dueDate:
guard let secondarySortDate = cursor.secondarySortDate else {
return [primaryValue, cursor.documentID]
}
return [
primaryValue,
Timestamp(date: secondarySortDate),
cursor.documentID
]
Comment thread
opficdev marked this conversation as resolved.
Outdated
case .createdAt, .updatedAt:
return [
primaryValue,
cursor.documentID
]
}
}

func makeCursor(
from document: QueryDocumentSnapshot,
orderField: String
query: TodoQuery
) -> TodoCursorDTO? {
guard let orderedAt = document.data()[orderField] as? Timestamp else {
let data = document.data()
let orderField = query.sortTarget.fieldName
let primarySortDate: Date?
let secondarySortDate: Date?

if let timestamp = data[orderField] as? Timestamp {
primarySortDate = timestamp.dateValue()
} else if data[orderField] is NSNull {
primarySortDate = nil
} else {
return nil
}

switch query.sortTarget {
case .dueDate:
guard let updatedAt = data["updatedAt"] as? Timestamp else {
return nil
}
secondarySortDate = updatedAt.dateValue()
case .createdAt, .updatedAt:
secondarySortDate = nil
}

return TodoCursorDTO(
orderedAt: orderedAt.dateValue(),
primarySortDate: primarySortDate,
secondarySortDate: secondarySortDate,
documentID: document.documentID
)
}
Expand Down
Loading