Fix "Load More" Button on Home/Public Timeline (#1283)
* Begin fixing of "Load More" on Home Timeline (IOS-266) * Don't show "Load More" if last status is first existing (IOS-266) * Insert missing items upon "Load More" (IOS-266) * Implement sinceID usage when loading latest posts (IOS-266) * Change updating of items on Load More(IOS-266) * Do not try to modify datasource directly (IOS-266) * Improve load more (IOS-266) * Fix load more using maxID and limit to 20 items (IOS-266) * Implement loading missing status in public timeline (IOS-266) * Implement subsequent "Load More" (IOS-266) * Make loadMore(item:at:) API more Swifty (IOS-266) * Address PR comments (IOS-266)
This commit is contained in:
parent
9b770d1484
commit
677670055e
@ -693,7 +693,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -148,32 +148,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]>?
|
||||
|
||||
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[..<firstIndex]
|
||||
let tail = oldRecords[firstIndex..<count]
|
||||
|
||||
var feedItems = [MastodonFeed]()
|
||||
|
||||
/// See HomeTimelineViewModel+LoadLatestState.swift for the "Load More"-counterpart when fetching new timeline items
|
||||
for (index, item) in items.enumerated() {
|
||||
let hasMore: Bool
|
||||
|
||||
/// there can only be a gap after the last items
|
||||
if index < items.count - 1 {
|
||||
hasMore = false
|
||||
} else {
|
||||
/// if fetched items and first item after gap don't match -> 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
|
||||
|
||||
// reconfigure item again
|
||||
snapshot.reconfigureItems([item])
|
||||
await updateSnapshotUsingReloadData(snapshot: snapshot)
|
||||
record.hasMore = false
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user