mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
use env object for home feed view
This commit is contained in:
parent
bb63806f08
commit
d1f0e90e62
5 changed files with 53 additions and 112 deletions
|
|
@ -1,9 +1,8 @@
|
|||
import SwiftUI
|
||||
import Views
|
||||
|
||||
// TODO: maybe this can be removed??
|
||||
enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
|
||||
case feed(viewModel: HomeFeedViewModel)
|
||||
case feed
|
||||
case profile
|
||||
|
||||
static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool {
|
||||
|
|
@ -38,8 +37,8 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
|
|||
|
||||
@ViewBuilder var destinationView: some View {
|
||||
switch self {
|
||||
case let .feed(viewModel: viewModel):
|
||||
HomeFeedView(viewModel: viewModel)
|
||||
case .feed:
|
||||
HomeFeedView()
|
||||
case .profile:
|
||||
ProfileContainerView()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ public struct RootView: View {
|
|||
|
||||
@ViewBuilder private var innerBody: some View {
|
||||
if authenticator.isLoggedIn {
|
||||
PrimaryContentView(services: viewModel.services)
|
||||
PrimaryContentView()
|
||||
.onAppear {
|
||||
viewModel.triggerPushNotificationRequestIfNeeded()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,33 +4,22 @@ import SwiftUI
|
|||
import Views
|
||||
|
||||
public struct PrimaryContentView: View {
|
||||
let homeFeedViewModel: HomeFeedViewModel
|
||||
|
||||
public init(services: Services) {
|
||||
self.homeFeedViewModel = HomeFeedViewModel.make(services: services)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
#if os(iOS)
|
||||
if UIDevice.isIPad {
|
||||
regularView
|
||||
} else {
|
||||
compactView
|
||||
HomeFeedView()
|
||||
}
|
||||
#elseif os(macOS)
|
||||
regularView
|
||||
#endif
|
||||
}
|
||||
|
||||
// iphone view container
|
||||
private var compactView: some View {
|
||||
HomeFeedView(viewModel: homeFeedViewModel)
|
||||
}
|
||||
|
||||
// ipad and mac view container
|
||||
private var regularView: some View {
|
||||
let categories = [
|
||||
PrimaryContentCategory.feed(viewModel: homeFeedViewModel),
|
||||
PrimaryContentCategory.feed,
|
||||
PrimaryContentCategory.profile
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ struct ProfileContainerView: View {
|
|||
#if os(iOS)
|
||||
Button(
|
||||
action: {
|
||||
viewModel.performActionSubject.send(.showIntercomMessenger)
|
||||
DataService.showIntercomMessenger?()
|
||||
},
|
||||
label: { Text("Feedback") }
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue