diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 164f92c880..b57b18e380 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -698,7 +698,7 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } Task { - await viewModel?.loadMore(item: item) + await viewModel?.loadMore(item: item, at: indexPath) } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 4e011c55ee..3f49f39afd 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -116,15 +116,20 @@ extension HomeTimelineViewModel.LoadLatestState { do { await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService) let response: Mastodon.Response.Content<[Mastodon.Entity.Status]> - + + /// To find out wether or not we need to show the "Load More" button + /// we have make sure to eventually overlap with the most recent cached item + let sinceID = latestFeedRecords.count > 1 ? latestFeedRecords[1].id : "1" + switch viewModel.timelineContext { case .home: response = try await viewModel.context.apiService.homeTimeline( + sinceID: sinceID, authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) case .public: response = try await viewModel.context.apiService.publicTimeline( - query: .init(local: true), + query: .init(local: true, sinceID: sinceID), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) } @@ -140,10 +145,25 @@ extension HomeTimelineViewModel.LoadLatestState { viewModel.didLoadLatest.send() } else { viewModel.dataController.records = { - var newRecords: [MastodonFeed] = newStatuses.map { - MastodonFeed.fromStatus(.fromEntity($0), kind: .home) - } var oldRecords = viewModel.dataController.records + + var newRecords = [MastodonFeed]() + + /// See HomeTimelineViewModel.swift for the "Load More"-counterpart when fetching new timeline items + for (index, status) in newStatuses.enumerated() { + if index < newStatuses.count - 1 { + newRecords.append( + MastodonFeed.fromStatus(.fromEntity(status), kind: .home, hasMore: false) + ) + continue + } + + let hasMore = status != oldRecords.first?.status?.entity + + newRecords.append( + MastodonFeed.fromStatus(.fromEntity(status), kind: .home, hasMore: hasMore) + ) + } for (i, record) in newRecords.enumerated() { if let index = oldRecords.firstIndex(where: { $0.status?.reblog?.id == record.id || $0.status?.id == record.id }) { oldRecords[index] = record diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index e199138739..5f4a420da3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -146,32 +146,68 @@ extension HomeTimelineViewModel { extension HomeTimelineViewModel { // load timeline gap - func loadMore(item: StatusItem) async { + @MainActor + func loadMore(item: StatusItem, at indexPath: IndexPath) async { guard case let .feedLoader(record) = item else { return } - guard let diffableDataSource = diffableDataSource else { return } - var snapshot = diffableDataSource.snapshot() guard let status = record.status else { return } record.isLoadingMore = true - // reconfigure item - snapshot.reconfigureItems([item]) - await updateSnapshotUsingReloadData(snapshot: snapshot) - await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService) // fetch data - let maxID = status.id - _ = try? await context.apiService.homeTimeline( - maxID: maxID, - authenticationBox: authContext.mastodonAuthenticationBox - ) + let response: Mastodon.Response.Content<[Mastodon.Entity.Status]>? - record.isLoadingMore = false + switch timelineContext { + case .home: + response = try? await context.apiService.homeTimeline( + maxID: status.id, + limit: 20, + authenticationBox: authContext.mastodonAuthenticationBox + ) + case .public: + response = try? await context.apiService.publicTimeline( + query: .init(maxID: status.id, limit: 20), + authenticationBox: authContext.mastodonAuthenticationBox + ) + } + + // insert missing items + guard let items = response?.value else { + record.isLoadingMore = false + return + } + + let firstIndex = indexPath.row + let oldRecords = dataController.records + let count = oldRecords.count + let head = oldRecords[.. we got another gap + hasMore = item != head.first?.status?.entity + } + + feedItems.append( + .fromStatus(item.asMastodonStatus, kind: .home, hasMore: hasMore) + ) + } + + let combinedRecords = Array(head + feedItems + tail) + dataController.records = combinedRecords + + record.isLoadingMore = false + record.hasMore = false } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index ce3fd232db..1f77f7bea6 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift @@ -39,6 +39,10 @@ extension StatusTableViewCell { case .feed(let feed): statusView.configure(feed: feed) self.separatorLine.isHidden = feed.hasMore + feed.$hasMore.sink(receiveValue: { [weak self] hasMore in + self?.separatorLine.isHidden = hasMore + }) + .store(in: &disposeBag) case .status(let status): statusView.configure(status: status) diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 252dd1c840..f11f4cfe11 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -18,7 +18,11 @@ public final class MastodonFeed { } public let id: String + + @Published public var hasMore: Bool = false + + @Published public var isLoadingMore: Bool = false public let status: MastodonStatus? @@ -39,9 +43,9 @@ public final class MastodonFeed { } public extension MastodonFeed { - static func fromStatus(_ status: MastodonStatus, kind: Feed.Kind) -> MastodonFeed { + static func fromStatus(_ status: MastodonStatus, kind: Feed.Kind, hasMore: Bool? = nil) -> MastodonFeed { MastodonFeed( - hasMore: false, + hasMore: hasMore ?? false, isLoadingMore: false, status: status, notification: nil, @@ -79,7 +83,8 @@ extension MastodonFeed: Hashable { lhs.status?.poll == rhs.status?.poll && lhs.status?.reblog?.poll == rhs.status?.reblog?.poll && lhs.status?.poll?.entity == rhs.status?.poll?.entity && - lhs.status?.reblog?.poll?.entity == rhs.status?.reblog?.poll?.entity + lhs.status?.reblog?.poll?.entity == rhs.status?.reblog?.poll?.entity && + lhs.isLoadingMore == rhs.isLoadingMore } public func hash(into hasher: inout Hasher) { @@ -94,6 +99,7 @@ extension MastodonFeed: Hashable { hasher.combine(status?.reblog?.poll) hasher.combine(status?.poll?.entity) hasher.combine(status?.reblog?.poll?.entity) + hasher.combine(isLoadingMore) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift index fc8a4f3656..94cf28ae7e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift @@ -37,8 +37,10 @@ extension TimelineMiddleLoaderTableViewCell { feed: MastodonFeed, delegate: TimelineMiddleLoaderTableViewCellDelegate? ) { - self.viewModel.isFetching = feed.isLoadingMore - + feed.$isLoadingMore + .assign(to: \.isFetching, on: self.viewModel) + .store(in: &disposeBag) + self.delegate = delegate }