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
1 change: 1 addition & 0 deletions DevLog/Presentation/Structure/PushNotificationItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

struct PushNotificationItem: Identifiable, Hashable {
let id: String
var isHidden = false
let title: String
let body: String
let receivedAt: Date
Expand Down
1 change: 1 addition & 0 deletions DevLog/Presentation/Structure/Todo/TodoListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

struct TodoListItem: Identifiable, Hashable {
let id: String
var isHidden = false
let number: Int
let title: String
let tags: [String]
Expand Down
1 change: 1 addition & 0 deletions DevLog/Presentation/Structure/WebPageItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI

struct WebPageItem: Identifiable, Hashable {
private let metadata: WebPage
var isHidden = false

init(from metadata: WebPage) {
self.metadata = metadata
Expand Down
74 changes: 36 additions & 38 deletions DevLog/Presentation/ViewModel/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class HomeViewModel: Store {
var preferences: [TodoCategoryItem] = []
var recentTodos: [RecentTodoItem] = []
var webPages: [WebPageItem] = []
var needsWebPageRefresh = false
var isNetworkConnected: Bool = true
var showContentPicker: Bool = false
var showTodoEditor: Bool = false
Expand All @@ -40,7 +41,10 @@ final class HomeViewModel: Store {
case setPresentation(Presentation, Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setToast(isPresented: Bool, type: ToastType? = nil)
case refreshWebPages
case setLoading(LoadingTarget, Bool)
case setWebPageHidden(URL, Bool)
case handleWebPageDeleteFailure(URL)
case tapTodoCategory(TodoCategory)
case orderTodoCategory([TodoCategoryItem])
case setTodoCategory([TodoCategoryItem])
Expand All @@ -51,13 +55,12 @@ final class HomeViewModel: Store {
case deleteWebPage(WebPageItem)
case undoDeleteWebPage
case updateWebPages([WebPageItem])
case restoreWebPage(WebPageItem, Int)
}

enum SideEffect {
case addTodo(Todo)
case addWebPage(String)
case deleteWebPage(WebPageItem, Int)
case deleteWebPage(WebPageItem)
case undoDeleteWebPage(String)
case fetchTodoCategoryPreferences
case updateTodoCategoryPreferences([TodoCategoryItem])
Expand Down Expand Up @@ -140,13 +143,13 @@ final class HomeViewModel: Store {
switch action {
case .networkStatusChanged(let isConnected):
state.isNetworkConnected = isConnected
case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoCategory,
.orderTodoCategory, .addTodo, .updateWebPageURLInput,
case .onAppear, .setPresentation, .setAlert, .setToast, .refreshWebPages,
.tapTodoCategory, .orderTodoCategory, .addTodo, .updateWebPageURLInput,
.addWebPage, .deleteWebPage, .undoDeleteWebPage:
effects = reduceByView(action, state: &state)

case .setLoading, .setTodoCategory, .updateRecentTodos,
.updateWebPages, .restoreWebPage:
case .setLoading, .setWebPageHidden, .handleWebPageDeleteFailure, .setTodoCategory,
.updateRecentTodos, .updateWebPages:
effects = reduceByRun(action, state: &state)
}

Expand Down Expand Up @@ -218,38 +221,24 @@ final class HomeViewModel: Store {
send(.setAlert(isPresented: true, type: .error))
}
}
case .deleteWebPage(let page, let index):
beginLoading(for: .webPage, mode: .delayed)
case .deleteWebPage(let page):
Task {
do {
defer { endLoading(for: .webPage, mode: .delayed) }
try await deleteWebPageUseCase.execute(page.url.absoluteString)
} catch {
send(.restoreWebPage(page, index))
send(.handleWebPageDeleteFailure(page.id))
send(.setAlert(isPresented: true, type: .error))
}
}
case .undoDeleteWebPage(let urlString):
beginLoading(for: .webPage, mode: .delayed)
Task {
defer { endLoading(for: .webPage, mode: .delayed) }

var shouldPresentError = false

do {
try await undoDeleteWebPageUseCase.execute(urlString)
try await addWebPageUseCase.execute(urlString)
} catch {
shouldPresentError = true
}

do {
let pages = try await fetchWebPagesUseCase.execute("")
send(.updateWebPages(pages.map { WebPageItem(from: $0) }))
} catch {
shouldPresentError = true
}

if shouldPresentError {
if let webPageURL = URL(string: urlString) {
send(.setWebPageHidden(webPageURL, true))
}
send(.setAlert(isPresented: true, type: .error))
}
}
Expand Down Expand Up @@ -285,6 +274,8 @@ private extension HomeViewModel {
switch action {
case .onAppear:
return [.fetchTodoCategoryPreferences, .fetchRecentTodos, .fetchWebPages]
case .refreshWebPages:
return [.fetchWebPages]
case .setPresentation(let presentation, let isPresented):
setPresentation(&state, presentation: presentation, isPresented: isPresented)
case .setAlert(let presented, let type):
Expand All @@ -296,6 +287,7 @@ private extension HomeViewModel {
case .setToast(let isPresented, let type):
setToast(&state, isPresented: isPresented, for: type)
if !isPresented {
state.webPages.removeAll { $0.isHidden }
deletedWebPageURLString = nil
}
case .tapTodoCategory(let category):
Expand All @@ -320,12 +312,17 @@ private extension HomeViewModel {
case .deleteWebPage(let page):
if let index = state.webPages.firstIndex(where: { $0.id == page.id }) {
deletedWebPageURLString = page.url.absoluteString
state.webPages.remove(at: index)
state.webPages[index].isHidden = true
setToast(&state, isPresented: true, for: .deleteWebPage)
return [.deleteWebPage(page, index)]
return [.deleteWebPage(page)]
}
case .undoDeleteWebPage:
guard let deletedWebPageURLString else { return [] }
if let index = state.webPages.firstIndex(where: {
$0.url.absoluteString == deletedWebPageURLString
}) {
state.webPages[index].isHidden = false
}
self.deletedWebPageURLString = nil
return [.undoDeleteWebPage(deletedWebPageURLString)]
default:
Expand All @@ -339,23 +336,24 @@ private extension HomeViewModel {
switch action {
case .setLoading(let loadingTarget, let isLoading):
setLoading(&state, loadingTarget: loadingTarget, isLoading: isLoading)
case .setWebPageHidden(let webPageURL, let isHidden):
if let index = state.webPages.firstIndex(where: { $0.id == webPageURL }) {
state.webPages[index].isHidden = isHidden
}
case .handleWebPageDeleteFailure(let webPageURL):
if let index = state.webPages.firstIndex(where: { $0.id == webPageURL }) {
state.webPages[index].isHidden = false
} else {
state.needsWebPageRefresh = true
}
case .setTodoCategory(let preferences):
state.preferences = preferences
state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences)
case .updateRecentTodos(let todos):
state.recentTodos = todos
case .updateWebPages(let pages):
state.webPages = pages
case .restoreWebPage(let page, let index):
if state.webPages.contains(where: { $0.id == page.id }) { break }
if index <= state.webPages.count {
state.webPages.insert(page, at: index)
} else {
state.webPages.append(page)
}
if deletedWebPageURLString == page.url.absoluteString {
deletedWebPageURLString = nil
}
state.needsWebPageRefresh = false
default:
break
}
Expand Down
96 changes: 59 additions & 37 deletions DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class PushNotificationListViewModel: Store {
case resetPagination
case setHasMore(Bool)
case syncNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?, hasMore: Bool)
case restoreNotification(PushNotificationItem, Int)
case setNotificationHidden(String, Bool)
case toggleSortOption
case setTimeFilter(PushNotificationQuery.TimeFilter)
case toggleUnreadOnly
Expand All @@ -48,7 +48,7 @@ final class PushNotificationListViewModel: Store {

enum SideEffect {
case fetchNotifications(PushNotificationQuery, cursor: PushNotificationCursor?)
case delete(PushNotificationItem, Int)
case delete(PushNotificationItem)
case undoDelete(String)
case toggleRead(String)
}
Expand All @@ -61,7 +61,7 @@ final class PushNotificationListViewModel: Store {
private let fetchQueryUseCase: FetchPushNotificationQueryUseCase
private let updateQueryUseCase: UpdatePushNotificationQueryUseCase
private let loadingState = LoadingState()
private var undoDeleteNotificationId: String?
private var undoNotificationId: String?
private var cancellable: AnyCancellable?

init(
Expand Down Expand Up @@ -104,7 +104,7 @@ final class PushNotificationListViewModel: Store {
effects = reduceByView(action, state: &state)

case .setLoading, .appendNotifications, .resetPagination, .setHasMore,
.syncNotifications, .restoreNotification:
.syncNotifications, .setNotificationHidden:
effects = reduceByRun(action, state: &state)
}

Expand Down Expand Up @@ -145,31 +145,23 @@ final class PushNotificationListViewModel: Store {
}

}
case .delete(let item, let index):
beginLoading(.delayed)
case .delete(let item):
Task {
do {
defer { endLoading(.delayed) }
try await deleteUseCase.execute(item.id)
} catch {
send(.restoreNotification(item, index))
send(.setNotificationHidden(item.id, false))
send(.setAlert(isPresented: true))
}
}
case .undoDelete(let notificationId):
beginLoading(.delayed)
Task {
// endLoading(.delayed)를 defer로 두지 않는 이유
// send(.fetchNotifications)가 같은 턴에서 beginLoading(.delayed)를 먼저 올린 뒤
// delayed 로딩을 내려야 같은 isLoading이 끊기지 않기 때문
do {
try await undoDeleteUseCase.execute(notificationId)
} catch {
send(.setNotificationHidden(notificationId, true))
send(.setAlert(isPresented: true))
}

send(.fetchNotifications)
endLoading(.delayed)
}
case .toggleRead(let todoId):
beginLoading(.delayed)
Expand All @@ -190,11 +182,11 @@ private extension PushNotificationListViewModel {
func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .deleteNotification(let item):
if let index = state.notifications.firstIndex(where: { $0.id == item.id }) {
undoDeleteNotificationId = item.id
state.notifications.remove(at: index)
if state.notifications.contains(where: { $0.id == item.id }) {
self.undoNotificationId = item.id
setNotificationHidden(&state, notificationId: item.id, isHidden: true)
setToast(&state, isPresented: true)
return [.delete(item, index)]
return [.delete(item)]
}
return []
case .toggleRead(let item):
Expand All @@ -203,9 +195,10 @@ private extension PushNotificationListViewModel {
return [.toggleRead(item.todoId)]
}
case .undoDelete:
guard let undoDeleteNotificationId else { return [] }
self.undoDeleteNotificationId = nil
return [.undoDelete(undoDeleteNotificationId)]
guard let undoNotificationId else { return [] }
setNotificationHidden(&state, notificationId: undoNotificationId, isHidden: false)
self.undoNotificationId = nil
return [.undoDelete(undoNotificationId)]
case .setAlert(let isPresented):
setAlert(&state, isPresented: isPresented)
case .toggleSortOption:
Expand Down Expand Up @@ -251,7 +244,8 @@ private extension PushNotificationListViewModel {
case .setToast(let isPresented):
setToast(&state, isPresented: isPresented)
if !isPresented {
undoDeleteNotificationId = nil
state.notifications.removeAll { $0.isHidden }
self.undoNotificationId = nil
}
case .setSelectedTodoId(let todoId):
state.selectedTodoId = todoId
Expand All @@ -271,24 +265,20 @@ private extension PushNotificationListViewModel {
state.notifications = []
state.nextCursor = nil
case .appendNotifications(let notifications, let nextCursor):
state.notifications.append(contentsOf: notifications)
state.notifications.append(contentsOf: mergedHiddenNotifications(
currentNotifications: state.notifications,
incomingNotifications: notifications
))
state.nextCursor = nextCursor
case .syncNotifications(let notifications, let nextCursor, let hasMore):
state.notifications = notifications
state.notifications = mergedHiddenNotifications(
currentNotifications: state.notifications,
incomingNotifications: notifications
)
state.nextCursor = nextCursor
state.hasMore = hasMore
case .restoreNotification(let notification, let index):
if state.notifications.contains(where: { $0.id == notification.id }) { break }

if index <= state.notifications.count {
state.notifications.insert(notification, at: index)
} else {
state.notifications.append(notification)
}

if undoDeleteNotificationId == notification.id {
undoDeleteNotificationId = nil
}
case .setNotificationHidden(let notificationId, let isHidden):
setNotificationHidden(&state, notificationId: notificationId, isHidden: isHidden)
default:
break
}
Expand All @@ -314,6 +304,38 @@ private extension PushNotificationListViewModel {
state.showToast = isPresented
}

func setNotificationHidden(
_ state: inout State,
notificationId: String,
isHidden: Bool
) {
if let notificationIndex = state.notifications.firstIndex(where: {
$0.id == notificationId
}) {
state.notifications[notificationIndex].isHidden = isHidden
}
}

func mergedHiddenNotifications(
currentNotifications: [PushNotificationItem],
incomingNotifications: [PushNotificationItem]
) -> [PushNotificationItem] {
let hiddenNotificationIds = Set(currentNotifications
.filter(\.isHidden)
.map(\.id)
)

return incomingNotifications.map { incomingNotification in
guard hiddenNotificationIds.contains(incomingNotification.id) else {
return incomingNotification
}

var hiddenNotification = incomingNotification
hiddenNotification.isHidden = true
return hiddenNotification
}
}

func startObservingNotifications(
query: PushNotificationQuery,
limit: Int
Expand Down
Loading