|
|
|
|
@ -6,40 +6,39 @@ import UserNotifications
|
|
|
|
|
import Utils
|
|
|
|
|
import Views
|
|
|
|
|
|
|
|
|
|
extension HomeFeedViewModel {
|
|
|
|
|
static func make(services: Services) -> HomeFeedViewModel {
|
|
|
|
|
let viewModel = HomeFeedViewModel()
|
|
|
|
|
viewModel.bind(services: services)
|
|
|
|
|
viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false)
|
|
|
|
|
return viewModel
|
|
|
|
|
}
|
|
|
|
|
final class HomeFeedViewModel: ObservableObject {
|
|
|
|
|
var currentDetailViewModel: LinkItemDetailViewModel?
|
|
|
|
|
|
|
|
|
|
func bind(services: Services) {
|
|
|
|
|
performActionSubject.sink { [weak self] action in
|
|
|
|
|
switch action {
|
|
|
|
|
case let .refreshItems(query: query):
|
|
|
|
|
self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: true)
|
|
|
|
|
case let .loadItems(query):
|
|
|
|
|
self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: false)
|
|
|
|
|
case let .archive(linkId):
|
|
|
|
|
self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: true)
|
|
|
|
|
case let .unarchive(linkId):
|
|
|
|
|
self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: false)
|
|
|
|
|
case let .remove(linkId):
|
|
|
|
|
self?.removeLink(dataService: services.dataService, linkId: linkId)
|
|
|
|
|
case let .snooze(linkId, until, successMessage):
|
|
|
|
|
self?.snoozeUntil(
|
|
|
|
|
dataService: services.dataService,
|
|
|
|
|
linkId: linkId,
|
|
|
|
|
until: until,
|
|
|
|
|
successMessage: successMessage
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
@Published var items = [FeedItem]()
|
|
|
|
|
@Published var isLoading = false
|
|
|
|
|
@Published var showPushNotificationPrimer = false
|
|
|
|
|
var cursor: String?
|
|
|
|
|
|
|
|
|
|
// These are used to make sure we handle search result
|
|
|
|
|
// responses in the right order
|
|
|
|
|
var searchIdx = 0
|
|
|
|
|
var receivedIdx = 0
|
|
|
|
|
|
|
|
|
|
var subscriptions = Set<AnyCancellable>()
|
|
|
|
|
|
|
|
|
|
init() {}
|
|
|
|
|
|
|
|
|
|
func itemAppeared(item: FeedItem, searchQuery: String, dataService: DataService) {
|
|
|
|
|
if isLoading { return }
|
|
|
|
|
let itemIndex = items.firstIndex(where: { $0.id == item.id })
|
|
|
|
|
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
|
|
|
|
|
|
|
|
|
|
// Check if user has scrolled to the last five items in the list
|
|
|
|
|
if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 {
|
|
|
|
|
loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: false)
|
|
|
|
|
}
|
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) {
|
|
|
|
|
func pushFeedItem(item: FeedItem) {
|
|
|
|
|
items.insert(item, at: 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) {
|
|
|
|
|
// Clear offline highlights since we'll be populating new FeedItems with the correct highlights set
|
|
|
|
|
dataService.clearHighlights()
|
|
|
|
|
|
|
|
|
|
@ -90,7 +89,7 @@ extension HomeFeedViewModel {
|
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) {
|
|
|
|
|
func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) {
|
|
|
|
|
isLoading = true
|
|
|
|
|
startNetworkActivityIndicator()
|
|
|
|
|
|
|
|
|
|
@ -120,7 +119,7 @@ extension HomeFeedViewModel {
|
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func removeLink(dataService: DataService, linkId: String) {
|
|
|
|
|
func removeLink(dataService: DataService, linkId: String) {
|
|
|
|
|
isLoading = true
|
|
|
|
|
startNetworkActivityIndicator()
|
|
|
|
|
|
|
|
|
|
@ -146,7 +145,7 @@ extension HomeFeedViewModel {
|
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) {
|
|
|
|
|
func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) {
|
|
|
|
|
isLoading = true
|
|
|
|
|
startNetworkActivityIndicator()
|
|
|
|
|
|
|
|
|
|
@ -178,55 +177,10 @@ extension HomeFeedViewModel {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: remove this view model
|
|
|
|
|
final class HomeFeedViewModel: ObservableObject {
|
|
|
|
|
var currentDetailViewModel: LinkItemDetailViewModel?
|
|
|
|
|
|
|
|
|
|
@Published var items = [FeedItem]()
|
|
|
|
|
@Published var isLoading = false
|
|
|
|
|
@Published var showPushNotificationPrimer = false
|
|
|
|
|
var cursor: String?
|
|
|
|
|
|
|
|
|
|
// These are used to make sure we handle search result
|
|
|
|
|
// responses in the right order
|
|
|
|
|
var searchIdx = 0
|
|
|
|
|
var receivedIdx = 0
|
|
|
|
|
|
|
|
|
|
enum Action {
|
|
|
|
|
case refreshItems(query: String)
|
|
|
|
|
case loadItems(query: String)
|
|
|
|
|
case archive(linkId: String)
|
|
|
|
|
case unarchive(linkId: String)
|
|
|
|
|
case remove(linkId: String)
|
|
|
|
|
case snooze(linkId: String, until: Date, successMessage: String?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var subscriptions = Set<AnyCancellable>()
|
|
|
|
|
let performActionSubject = PassthroughSubject<Action, Never>()
|
|
|
|
|
|
|
|
|
|
init() {}
|
|
|
|
|
|
|
|
|
|
func itemAppeared(item: FeedItem, searchQuery: String) {
|
|
|
|
|
if isLoading { return }
|
|
|
|
|
let itemIndex = items.firstIndex(where: { $0.id == item.id })
|
|
|
|
|
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
|
|
|
|
|
|
|
|
|
|
// Check if user has scrolled to the last five items in the list
|
|
|
|
|
if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 {
|
|
|
|
|
performActionSubject.send(.loadItems(query: searchQuery))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pushFeedItem(item: FeedItem) {
|
|
|
|
|
items.insert(item, at: 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct HomeFeedView: View {
|
|
|
|
|
// @EnvironmentObject var authenticator: Authenticator
|
|
|
|
|
// @EnvironmentObject var dataService: DataService
|
|
|
|
|
@EnvironmentObject var dataService: DataService
|
|
|
|
|
|
|
|
|
|
@ObservedObject private var viewModel: HomeFeedViewModel
|
|
|
|
|
@ObservedObject private var viewModel = HomeFeedViewModel()
|
|
|
|
|
@State private var selectedLinkItem: FeedItem?
|
|
|
|
|
@State private var searchQuery = ""
|
|
|
|
|
@State private var itemToRemove: FeedItem?
|
|
|
|
|
@ -234,10 +188,6 @@ struct HomeFeedView: View {
|
|
|
|
|
@State private var snoozePresented = false
|
|
|
|
|
@State private var itemToSnooze: FeedItem?
|
|
|
|
|
|
|
|
|
|
init(viewModel: HomeFeedViewModel) {
|
|
|
|
|
self.viewModel = viewModel
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ViewBuilder var conditionalInnerBody: some View {
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
if #available(iOS 15.0, *) {
|
|
|
|
|
@ -301,14 +251,14 @@ struct HomeFeedView: View {
|
|
|
|
|
.opacity(0)
|
|
|
|
|
.buttonStyle(PlainButtonStyle())
|
|
|
|
|
.onAppear {
|
|
|
|
|
viewModel.itemAppeared(item: item, searchQuery: searchQuery)
|
|
|
|
|
viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService)
|
|
|
|
|
}
|
|
|
|
|
FeedCard(item: item)
|
|
|
|
|
}.contextMenu {
|
|
|
|
|
if !item.isArchived {
|
|
|
|
|
Button(action: {
|
|
|
|
|
withAnimation(.linear(duration: 0.4)) {
|
|
|
|
|
viewModel.performActionSubject.send(.archive(linkId: item.id))
|
|
|
|
|
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: true)
|
|
|
|
|
if item == selectedLinkItem {
|
|
|
|
|
selectedLinkItem = nil
|
|
|
|
|
}
|
|
|
|
|
@ -317,7 +267,7 @@ struct HomeFeedView: View {
|
|
|
|
|
} else {
|
|
|
|
|
Button(action: {
|
|
|
|
|
withAnimation(.linear(duration: 0.4)) {
|
|
|
|
|
viewModel.performActionSubject.send(.unarchive(linkId: item.id))
|
|
|
|
|
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: false)
|
|
|
|
|
}
|
|
|
|
|
}, label: { Label("Unarchive", systemImage: "tray.and.arrow.down.fill") })
|
|
|
|
|
}
|
|
|
|
|
@ -335,7 +285,7 @@ struct HomeFeedView: View {
|
|
|
|
|
if !item.isArchived {
|
|
|
|
|
Button {
|
|
|
|
|
withAnimation(.linear(duration: 0.4)) {
|
|
|
|
|
viewModel.performActionSubject.send(.archive(linkId: item.id))
|
|
|
|
|
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: true)
|
|
|
|
|
}
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Archive", systemImage: "archivebox")
|
|
|
|
|
@ -343,7 +293,7 @@ struct HomeFeedView: View {
|
|
|
|
|
} else {
|
|
|
|
|
Button {
|
|
|
|
|
withAnimation(.linear(duration: 0.4)) {
|
|
|
|
|
viewModel.performActionSubject.send(.unarchive(linkId: item.id))
|
|
|
|
|
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: false)
|
|
|
|
|
}
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Unarchive", systemImage: "tray.and.arrow.down.fill")
|
|
|
|
|
@ -365,7 +315,7 @@ struct HomeFeedView: View {
|
|
|
|
|
Button("Remove Link", role: .destructive) {
|
|
|
|
|
if let itemToRemove = itemToRemove {
|
|
|
|
|
withAnimation {
|
|
|
|
|
viewModel.performActionSubject.send(.remove(linkId: itemToRemove.id))
|
|
|
|
|
viewModel.removeLink(dataService: dataService, linkId: itemToRemove.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.itemToRemove = nil
|
|
|
|
|
@ -417,8 +367,11 @@ struct HomeFeedView: View {
|
|
|
|
|
}
|
|
|
|
|
.formSheet(isPresented: $snoozePresented) {
|
|
|
|
|
SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) {
|
|
|
|
|
viewModel.performActionSubject.send(
|
|
|
|
|
.snooze(linkId: $0.feedItemId, until: $0.snoozeUntilDate, successMessage: $0.successMessage)
|
|
|
|
|
viewModel.snoozeUntil(
|
|
|
|
|
dataService: dataService,
|
|
|
|
|
linkId: $0.feedItemId,
|
|
|
|
|
until: $0.snoozeUntilDate,
|
|
|
|
|
successMessage: $0.successMessage
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -459,7 +412,7 @@ struct HomeFeedView: View {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func refresh() {
|
|
|
|
|
viewModel.performActionSubject.send(.refreshItems(query: searchQuery))
|
|
|
|
|
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|