Skip to content

Commit

Permalink
Merge pull request #793 from planetary-social/paginate-home-feed
Browse files Browse the repository at this point in the history
Paginate home feed
  • Loading branch information
mplorentz authored Jan 11, 2024
2 parents 25bf5d0 + ca05d44 commit be0a02f
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 119 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.2 (144)] - 2024-01-11Z
- Added pagination to the home feed.

## [0.1.2 (153)] - 2024-01-11Z

- Fixed a crash that sometimes occured when opening the profile view.
- Fixed a crash that sometimes occured when viewing a note.
Expand Down
36 changes: 26 additions & 10 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@
C9680AD12ACC5251006C8C93 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9680AD02ACC5251006C8C93 /* Either.swift */; };
C9680AD22ACC5251006C8C93 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9680AD02ACC5251006C8C93 /* Either.swift */; };
C9680AD42ACDF57D006C8C93 /* UNSWizardNeedsPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9680AD32ACDF57D006C8C93 /* UNSWizardNeedsPaymentView.swift */; };
C96877B92B4EDD110051ED2F /* AuthorStoryCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96877B82B4EDD110051ED2F /* AuthorStoryCarousel.swift */; };
C96877BB2B4EDE510051ED2F /* StoryAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96877BA2B4EDE510051ED2F /* StoryAvatarView.swift */; };
C96CB98C2A6040C500498C4E /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = C96CB98B2A6040C500498C4E /* DequeModule */; };
C97141EE2AE95F07001A5DD0 /* USBCAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97141ED2AE95F07001A5DD0 /* USBCAddress.swift */; };
C97141EF2AE95F07001A5DD0 /* USBCAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97141ED2AE95F07001A5DD0 /* USBCAddress.swift */; };
Expand Down Expand Up @@ -566,6 +568,8 @@
C9671D72298DB94C00EE7E12 /* Data+Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Encoding.swift"; sourceTree = "<group>"; };
C9680AD02ACC5251006C8C93 /* Either.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = "<group>"; };
C9680AD32ACDF57D006C8C93 /* UNSWizardNeedsPaymentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNSWizardNeedsPaymentView.swift; sourceTree = "<group>"; };
C96877B82B4EDD110051ED2F /* AuthorStoryCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorStoryCarousel.swift; sourceTree = "<group>"; };
C96877BA2B4EDE510051ED2F /* StoryAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAvatarView.swift; sourceTree = "<group>"; };
C97141ED2AE95F07001A5DD0 /* USBCAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USBCAddress.swift; sourceTree = "<group>"; };
C973364E2A7968220012D8B8 /* SetUpUNSBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpUNSBanner.swift; sourceTree = "<group>"; };
C973AB552A323167002AED16 /* Follow+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Follow+CoreDataProperties.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -872,6 +876,19 @@
path = Form;
sourceTree = "<group>";
};
C96877B32B4EDCCF0051ED2F /* Home */ = {
isa = PBXGroup;
children = (
C9DEBFD8298941000078B43A /* HomeFeedView.swift */,
5BE281C92AE2CCEB00880466 /* HomeTab.swift */,
5BE281BD2AE2CCAE00880466 /* StoriesView.swift */,
5BE281C02AE2CCB400880466 /* StoryNoteView.swift */,
C96877B82B4EDD110051ED2F /* AuthorStoryCarousel.swift */,
C96877BA2B4EDE510051ED2F /* StoryAvatarView.swift */,
);
path = Home;
sourceTree = "<group>";
};
C97797B7298AA1600046BD25 /* Service */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1212,10 +1229,9 @@
C987F81929BA4D0E00B44E7A /* ActionButton.swift */,
C9A0DAE929C6A34200466635 /* ActivityView.swift */,
C9F84C20298DC36800C6714D /* AppView.swift */,
2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */,
5B8C96B129DB313300B73AEC /* AuthorCard.swift */,
C97465302A3B89140031226F /* AuthorLabel.swift */,
5B8C96AB29D52AD200B73AEC /* AuthorListView.swift */,
5B8C96B129DB313300B73AEC /* AuthorCard.swift */,
5BE281C32AE2CCC300880466 /* AuthorStoryView.swift */,
3FFB1D88299FF37C002A755D /* AvatarView.swift */,
C92F01542AC4D6CF00972489 /* BeveledSeparator.swift */,
Expand All @@ -1231,17 +1247,16 @@
5B8C96B529DDD3B200B73AEC /* EditableText.swift */,
CD76864B29B12F7E00085358 /* ExpandingTextFieldAndSubmitButton.swift */,
A303AF8229A9153A005DC8FC /* FollowButton.swift */,
C9E8C1142B081EBE002D46B0 /* NIP05View.swift */,
A32B6C7729A6C99200653FF5 /* FollowCard.swift */,
A32B6C7229A6BE9B00653FF5 /* FollowsView.swift */,
C9B678E629F01A8500303F33 /* FullscreenProgressView.swift */,
C9CDBBA329A8FA2900C555C7 /* GoldenPostView.swift */,
C9A0DAE329C69F0C00466635 /* HighlightedText.swift */,
C9DEBFD8298941000078B43A /* HomeFeedView.swift */,
5BE281C92AE2CCEB00880466 /* HomeTab.swift */,
2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */,
C960C57029F3236200929990 /* LikeButton.swift */,
C905B0762A619E99009B8A78 /* LinkPreview.swift */,
5BFF66B52A58A8A000AA79DD /* MutesView.swift */,
C9E8C1142B081EBE002D46B0 /* NIP05View.swift */,
C9A0DADC29C689C900466635 /* NosNavigationBar.swift */,
CD2CF38F299E68BE00332116 /* NoteButton.swift */,
C9DFA964299BEB96006929C1 /* NoteCard.swift */,
Expand All @@ -1250,6 +1265,7 @@
C9B708BA2A13BE41006C613A /* NoteTextEditor.swift */,
C98B8B3F29FBF83B009789C8 /* NotificationCard.swift */,
C94437E529B0DB83004D8C86 /* NotificationsView.swift */,
C986510F2B0BD49200597B68 /* PagedNoteListView.swift */,
C95D68A4299E6E1E00429F86 /* PlaceholderModifier.swift */,
C9E37E112A1E7EC5003D4B0A /* PreviewContainer.swift */,
C94BC09A2A0AC74A0098F6F1 /* PreviewData.swift */,
Expand All @@ -1259,7 +1275,6 @@
5B834F682A83FC7F000C1432 /* ProfileSocialStatsView.swift */,
C987F81C29BA6D9A00B44E7A /* ProfileTab.swift */,
C95D68AC299E721700429F86 /* ProfileView.swift */,
C986510F2B0BD49200597B68 /* PagedNoteListView.swift */,
5BFF66B32A58853D00AA79DD /* PublishedEventsView.swift */,
C97A1C8729E45B3C009D9E8D /* RawEventView.swift */,
C9A25B3C29F174D200B39534 /* ReadabilityPadding.swift */,
Expand All @@ -1283,21 +1298,20 @@
C92DF80729C25FA900400561 /* SquareImage.swift */,
3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */,
C931517C29B915AF00934506 /* StaggeredGrid.swift */,
5BE281BD2AE2CCAE00880466 /* StoriesView.swift */,
5BE281C02AE2CCB400880466 /* StoryNoteView.swift */,
2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */,
3F60F42829B27D3E000D62C4 /* ThreadView.swift */,
C9E8C1122B081E9C002D46B0 /* UNSNameView.swift */,
C9A2FCA12AE6FF360020A5C6 /* USBCBalanceBarButtonItem.swift */,
C913DA0D2AEB3265003BDD6D /* WarningView.swift */,
C9CE5B132A0172CF008E198C /* WebView.swift */,
C92F01502AC4D67B00972489 /* Form */,
C96877B32B4EDCCF0051ED2F /* Home */,
C9CFF6D02AB241EB00D4B368 /* Modifiers */,
C9EE3E652A053CF1008A7491 /* New Note */,
3FB5E64F299D288E00386527 /* Onboarding */,
C9DEBFDD298941020078B43A /* Preview Content */,
C905EA362A33620A006F1E99 /* UNS */,
C9F2046E2ADEDDCA0029A858 /* USBC */,
C913DA0D2AEB3265003BDD6D /* WarningView.swift */,
C9E8C1122B081E9C002D46B0 /* UNSNameView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -1722,6 +1736,7 @@
C92F015B2AC4D74E00972489 /* NosTextEditor.swift in Sources */,
C913DA0C2AEB2EBF003BDD6D /* FetchRequestPublisher.swift in Sources */,
C973AB5D2A323167002AED16 /* Event+CoreDataProperties.swift in Sources */,
C96877BB2B4EDE510051ED2F /* StoryAvatarView.swift in Sources */,
C95D68A3299E6D9000429F86 /* SelectableText.swift in Sources */,
C9F84C27298DC98800C6714D /* KeyPair.swift in Sources */,
5B8C96B629DDD3B200B73AEC /* EditableText.swift in Sources */,
Expand Down Expand Up @@ -1839,6 +1854,7 @@
C98CA9072B14FBBF00929141 /* PagedNoteDataSource.swift in Sources */,
C9A0DAEA29C6A34200466635 /* ActivityView.swift in Sources */,
CD2CF390299E68BE00332116 /* NoteButton.swift in Sources */,
C96877B92B4EDD110051ED2F /* AuthorStoryCarousel.swift in Sources */,
C93F48932AC5C9CE00900CEC /* UNSWizardIntroView.swift in Sources */,
5BE281C42AE2CCC300880466 /* AuthorStoryView.swift in Sources */,
C9DEC04D29894BED0078B43A /* Author+CoreDataClass.swift in Sources */,
Expand Down
19 changes: 17 additions & 2 deletions Nos/Controller/PagedNoteDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,14 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
private var movedIndexes = [(IndexPath, IndexPath)]()

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates {
collectionView.performBatchUpdates({
Log.debug("Started batch updates")
insertedIndexes = [IndexPath]()
deletedIndexes = [IndexPath]()
movedIndexes = [(IndexPath, IndexPath)]()
}
}, completion: { (success) in
Log.debug("Completed batch updates with \(success))")
})
}

func controller(
Expand All @@ -188,14 +191,18 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?
) {
Log.debug("handling update type: \(type) indexPath: \(String(describing: indexPath))")

// Note: I tried using UICollectionViewDiffableDatasource but it didn't seem to work well with SwiftUI views
// as it kept reloading cells with animations when nothing was visually changing.
switch type {
case .insert:
Log.debug("queuing index path for insertion: \(String(describing: indexPath))")
if let newIndexPath = newIndexPath {
insertedIndexes.append(newIndexPath)
}
case .delete:
Log.debug("queuing index path for deletion: \(String(describing: indexPath))")
if let indexPath = indexPath {
deletedIndexes.append(indexPath)
}
Expand All @@ -205,6 +212,7 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
return
case .move:
if let oldIndexPath = indexPath, let newIndexPath {
Log.debug("queuing index path \(oldIndexPath) for move to \(newIndexPath)")
movedIndexes.append((oldIndexPath, newIndexPath))
}
@unknown default:
Expand All @@ -213,15 +221,22 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
Log.debug("controllerDidChangeContent started.")
if !deletedIndexes.isEmpty { // it doesn't seem like this check should be necessary but it crashes otherwise
Log.debug("deleting indexPaths: \(deletedIndexes)")
collectionView.deleteItems(at: deletedIndexes)
}
if !insertedIndexes.isEmpty {
Log.debug("inserting indexPaths: \(insertedIndexes)")
collectionView.insertItems(at: insertedIndexes)
}

Log.debug("moving indexes: \(movedIndexes)")
movedIndexes.forEach { indexPair in
let (oldIndex, newIndex) = indexPair
collectionView.moveItem(at: oldIndex, to: newIndex)
}

Log.debug("controllerDidChangeContent finished.")
}
}
2 changes: 1 addition & 1 deletion Nos/Models/Event+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ public class Event: NosManagedObject {
@nonobjc public class func homeFeedPredicate(for user: Author, before: Date) -> NSPredicate {
NSPredicate(
// swiftlint:disable line_length
format: "((kind = 1 AND SUBQUERY(eventReferences, $reference, $reference.marker = 'root' OR $reference.marker = 'reply' OR $reference.marker = nil).@count = 0) OR kind = 6 OR kind = 30023) AND (ANY author.followers.source = %@ OR author = %@) AND author.muted = 0 AND (receivedAt == nil OR receivedAt <= %@ AND deletedOn.@count = 0)",
format: "((kind = 1 AND SUBQUERY(eventReferences, $reference, $reference.marker = 'root' OR $reference.marker = 'reply' OR $reference.marker = nil).@count = 0) OR kind = 6 OR kind = 30023) AND (ANY author.followers.source = %@ OR author = %@) AND author.muted = 0 AND createdAt <= %@ AND deletedOn.@count = 0",
// swiftlint:enable line_length
user,
user,
Expand Down
6 changes: 6 additions & 0 deletions Nos/Service/Relay/PagedRelaySubscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ class PagedRelaySubscription {
for subscriptionID in pagedSubscriptionIDs {
if let subscription = await subscriptionManager.subscription(from: subscriptionID),
let newDate = subscription.oldestEventCreationDate {

guard newDate != subscription.filter.until else {
// Optimization. Don't close and reopen an identical filter.
continue
}

newUntilDates[subscription.relayAddress] = newDate
await subscriptionManager.decrementSubscriptionCount(for: subscriptionID)
}
Expand Down
44 changes: 44 additions & 0 deletions Nos/Views/Home/AuthorStoryCarousel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// AuthorStoryCarousel.swift
// Nos
//
// Created by Matthew Lorentz on 1/10/24.
//

import SwiftUI

/// Shows a scrollable horizontal feed of authors who have unread stories.
struct AuthorStoryCarousel: View {

@Binding var authors: [Author]
@Binding var selectedStoryAuthor: Author?

@EnvironmentObject private var router: Router

var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
ForEach(authors) { author in
Button {
withAnimation {
selectedStoryAuthor = author
}
} label: {
StoryAvatarView(author: author)
.contextMenu {
Button {
router.push(author)
} label: {
Text(.localizable.seeProfile)
}
}
}
}
}
.padding(.horizontal, 15)
.padding(.top, 15)
.padding(.bottom, 0)
}
.readabilityPadding()
}
}
Loading

0 comments on commit be0a02f

Please sign in to comment.