Add infinite scroll for comments

This commit is contained in:
Arkadiusz Fal 2022-01-05 17:12:32 +01:00
parent ac755d0ee6
commit 1db4a3197d
5 changed files with 84 additions and 47 deletions

View File

@ -8,7 +8,6 @@ final class CommentsModel: ObservableObject {
@Published var nextPage: String? @Published var nextPage: String?
@Published var firstPage = true @Published var firstPage = true
@Published var loading = false
@Published var loaded = false @Published var loaded = false
@Published var disabled = false @Published var disabled = false
@ -41,37 +40,43 @@ final class CommentsModel: ObservableObject {
} }
func load(page: String? = nil) { func load(page: String? = nil) {
guard Self.enabled, !loading else { guard Self.enabled else {
return return
} }
reset()
loading = true
guard !Self.instance.isNil, guard !Self.instance.isNil,
!(player?.currentVideo.isNil ?? true) !(player?.currentVideo.isNil ?? true)
else { else {
return return
} }
if !firstPage && !nextPageAvailable {
return
}
firstPage = page.isNil || page!.isEmpty firstPage = page.isNil || page!.isEmpty
api?.comments(player.currentVideo!.videoID, page: page)? api?.comments(player.currentVideo!.videoID, page: page)?
.load() .load()
.onSuccess { [weak self] response in .onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() { if let page: CommentsPage = response.typedContent() {
self?.all = page.comments self?.all += page.comments
self?.nextPage = page.nextPage self?.nextPage = page.nextPage
self?.disabled = page.disabled self?.disabled = page.disabled
} }
} }
.onCompletion { [weak self] _ in .onCompletion { [weak self] _ in
self?.loading = false
self?.loaded = true self?.loaded = true
} }
} }
func loadNextPageIfNeeded(current comment: Comment) {
let thresholdIndex = all.index(all.endIndex, offsetBy: -5)
if all.firstIndex(where: { $0 == comment }) == thresholdIndex {
loadNextPage()
}
}
func loadNextPage() { func loadNextPage() {
load(page: nextPage) load(page: nextPage)
} }
@ -108,7 +113,6 @@ final class CommentsModel: ObservableObject {
firstPage = true firstPage = true
nextPage = nil nextPage = nil
loaded = false loaded = false
loading = false
replies = [] replies = []
repliesLoaded = false repliesLoaded = false
} }

View File

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct CommentsView: View { struct CommentsView: View {
var embedInScrollView = false
@State private var repliesID: Comment.ID? @State private var repliesID: Comment.ID?
@EnvironmentObject<CommentsModel> private var comments @EnvironmentObject<CommentsModel> private var comments
@ -8,54 +9,38 @@ struct CommentsView: View {
var body: some View { var body: some View {
Group { Group {
if comments.disabled { if comments.disabled {
Text("Comments are disabled for this video") NoCommentsView(text: "Comments are disabled", systemImage: "xmark.circle.fill")
.foregroundColor(.secondary)
} else if comments.loaded && comments.all.isEmpty { } else if comments.loaded && comments.all.isEmpty {
Text("No comments") NoCommentsView(text: "No comments", systemImage: "0.circle.fill")
.foregroundColor(.secondary)
} else if !comments.loaded { } else if !comments.loaded {
PlaceholderProgressView() PlaceholderProgressView()
.onAppear { .onAppear {
comments.load() comments.load()
} }
} else { } else {
ScrollView(.vertical, showsIndicators: false) { let last = comments.all.last
VStack(alignment: .leading) { let commentsStack = LazyVStack {
let last = comments.all.last ForEach(comments.all) { comment in
ForEach(comments.all) { comment in CommentView(comment: comment, repliesID: $repliesID)
CommentView(comment: comment, repliesID: $repliesID) .onAppear {
comments.loadNextPageIfNeeded(current: comment)
if comment != last {
Divider()
.padding(.vertical, 5)
} }
.padding(.bottom, comment == last ? 5 : 0)
if comment != last {
Divider()
.padding(.vertical, 5)
} }
HStack {
if comments.nextPageAvailable {
Button {
repliesID = nil
comments.loadNextPage()
} label: {
Label("Show more", systemImage: "arrow.turn.down.right")
}
}
if !comments.firstPage {
Button {
repliesID = nil
comments.load(page: nil)
} label: {
Label("Show first", systemImage: "arrow.turn.down.left")
}
}
}
.font(.system(size: 13))
.buttonStyle(.plain)
.padding(.vertical, 8)
.foregroundColor(.secondary)
} }
} }
if embedInScrollView {
ScrollView(.vertical, showsIndicators: false) {
commentsStack
}
} else {
commentsStack
}
} }
} }
.padding(.horizontal) .padding(.horizontal)

View File

@ -0,0 +1,28 @@
import SwiftUI
struct NoCommentsView: View {
var text: String
var systemImage: String
var body: some View {
VStack(alignment: .center, spacing: 10) {
Image(systemName: systemImage)
.font(.system(size: 36))
Text(text)
#if !os(tvOS)
.font(.system(size: 12))
#endif
}
.frame(minWidth: 0, maxWidth: .infinity)
#if !os(tvOS)
.foregroundColor(.secondary)
#endif
}
}
struct NoCommentsView_Previews: PreviewProvider {
static var previews: some View {
NoCommentsView(text: "No comments", systemImage: "xmark.circle.fill")
}
}

View File

@ -512,6 +512,9 @@
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; }; 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; };
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
@ -774,6 +777,7 @@
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; }; 37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; };
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; 37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; }; 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; }; 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; }; 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; }; 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
@ -909,6 +913,7 @@
children = ( children = (
371B7E602759706A00D21217 /* CommentsView.swift */, 371B7E602759706A00D21217 /* CommentsView.swift */,
37EF9A75275BEB8E0043B585 /* CommentView.swift */, 37EF9A75275BEB8E0043B585 /* CommentView.swift */,
37DD9DA22785BBC900539416 /* NoCommentsView.swift */,
37B81B0126D2CAE700675966 /* PlaybackBar.swift */, 37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
37BE0BD226A1D4780092E2DB /* Player.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */,
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
@ -1914,6 +1919,7 @@
37FB28412721B22200A57617 /* ContentItem.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */, 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */, 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
@ -1992,6 +1998,7 @@
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */, 374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */,
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */,
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
@ -2316,6 +2323,7 @@
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
373197DA2732060100EF734F /* RelatedView.swift in Sources */, 373197DA2732060100EF734F /* RelatedView.swift in Sources */,
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */,
37D4B19926717E1500C925CA /* Video.swift in Sources */, 37D4B19926717E1500C925CA /* Video.swift in Sources */,
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */, 378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,

View File

@ -130,7 +130,11 @@ struct NowPlayingView: View {
} }
if sections.contains(.comments) { if sections.contains(.comments) {
if !comments.loaded { if comments.disabled {
NoCommentsView(text: "Comments are disabled", systemImage: "xmark.circle.fill")
} else if comments.loaded && comments.all.isEmpty {
NoCommentsView(text: "No comments", systemImage: "0.circle.fill")
} else if !comments.loaded {
VStack(alignment: .center) { VStack(alignment: .center) {
PlaceholderProgressView() PlaceholderProgressView()
.onAppear { .onAppear {
@ -142,6 +146,14 @@ struct NowPlayingView: View {
ForEach(comments.all) { comment in ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID) CommentView(comment: comment, repliesID: $repliesID)
} }
if comments.nextPageAvailable {
Text("Scroll to load more...")
.foregroundColor(.secondary)
.padding(.leading)
.onAppear {
comments.loadNextPage()
}
}
} }
} }
} }