Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix "Load More" Button on Home/Public Timeline #1283

Merged
merged 12 commits into from
May 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reascon sinceID is not an optional?
Both Timeline-calls support sinceID being nil and 1 as fallback confused me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adopted this from the Android implementation, there sinceID is also optional but when loading the Gaps it is explicitly using 1 which makes me assume the API has a different behavior when not setting the value at all.


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
)
}
Expand All @@ -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
Expand Down
68 changes: 52 additions & 16 deletions Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,32 +146,68 @@ extension HomeTimelineViewModel {
extension HomeTimelineViewModel {

// load timeline gap
func loadMore(item: StatusItem) async {
@MainActor
zeitschlag marked this conversation as resolved.
Show resolved Hide resolved
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 {
kimar marked this conversation as resolved.
Show resolved Hide resolved
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]()

// reconfigure item again
snapshot.reconfigureItems([item])
await updateSnapshotUsingReloadData(snapshot: snapshot)
/// 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)
)
}
kimar marked this conversation as resolved.
Show resolved Hide resolved

let combinedRecords = Array(head + feedItems + tail)
dataController.records = combinedRecords

record.isLoadingMore = false
record.hasMore = false
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading