Comments improvements

* Show text when there is no comments or comments are disabled
* Show progress indicator for loading comments/replies
* Improve layout of icons and text spacing
This commit is contained in:
Arkadiusz Fal 2021-12-05 18:14:49 +01:00
parent 37b99c59e1
commit 1f495562fc
7 changed files with 142 additions and 52 deletions

View File

@ -72,10 +72,12 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
} }
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
let comments = content.json.dictionaryValue["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? [] let details = content.json.dictionaryValue
let nextPage = content.json.dictionaryValue["nextpage"]?.stringValue let comments = details["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? []
let nextPage = details["nextpage"]?.stringValue
let disabled = details["disabled"]?.boolValue ?? false
return CommentsPage(comments: comments, nextPage: nextPage) return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
} }
if account.token.isNil { if account.token.isNil {

View File

@ -4,16 +4,27 @@ import SwiftyJSON
final class CommentsModel: ObservableObject { final class CommentsModel: ObservableObject {
@Published var all = [Comment]() @Published var all = [Comment]()
@Published var replies = [Comment]()
@Published var nextPage: String? @Published var nextPage: String?
@Published var firstPage = true @Published var firstPage = true
@Published var loaded = false @Published var loaded = true
@Published var disabled = false
@Published var replies = [Comment]()
@Published var repliesLoaded = false
var accounts: AccountsModel! var accounts: AccountsModel!
var player: PlayerModel! var player: PlayerModel!
var instance: Instance? {
InstancesModel.find(Defaults[.commentsInstanceID])
}
var api: VideosAPI? {
instance.isNil ? nil : PipedAPI(account: instance!.anonymousAccount)
}
static var enabled: Bool { static var enabled: Bool {
!Defaults[.commentsInstanceID].isNil && !Defaults[.commentsInstanceID]!.isEmpty !Defaults[.commentsInstanceID].isNil && !Defaults[.commentsInstanceID]!.isEmpty
} }
@ -27,23 +38,23 @@ final class CommentsModel: ObservableObject {
return return
} }
loaded = false reset()
clear()
guard let instance = InstancesModel.find(Defaults[.commentsInstanceID]), guard !instance.isNil,
!player.currentVideo.isNil !(player?.currentVideo.isNil ?? true)
else { else {
return return
} }
firstPage = page.isNil || page!.isEmpty firstPage = page.isNil || page!.isEmpty
PipedAPI(account: instance.anonymousAccount).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
} }
} }
.onCompletion { [weak self] _ in .onCompletion { [weak self] _ in
@ -61,19 +72,27 @@ final class CommentsModel: ObservableObject {
} }
replies = [] replies = []
repliesLoaded = false
accounts.api.comments(player.currentVideo!.videoID, page: page)?.load().onSuccess { response in api?.comments(player.currentVideo!.videoID, page: page)?
if let page: CommentsPage = response.typedContent() { .load()
self.replies = page.comments .onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() {
self?.replies = page.comments
}
}
.onCompletion { [weak self] _ in
self?.repliesLoaded = true
} }
}
} }
func clear() { func reset() {
all = [] all = []
replies = [] disabled = false
firstPage = true firstPage = true
nextPage = nil nextPage = nil
loaded = false loaded = false
replies = []
repliesLoaded = false
} }
} }

View File

@ -3,4 +3,5 @@ import Foundation
struct CommentsPage { struct CommentsPage {
var comments = [Comment]() var comments = [Comment]()
var nextPage: String? var nextPage: String?
var disabled = false
} }

View File

@ -37,7 +37,7 @@ extension PlayerModel {
} }
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) { func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
comments.clear() comments.reset()
currentItem = item currentItem = item
if !time.isNil { if !time.isNil {

View File

@ -40,7 +40,7 @@ struct CommentView: View {
Spacer() Spacer()
VStack(spacing: 5) { VStack(alignment: .trailing, spacing: 8) {
likes likes
statusIcons statusIcons
} }
@ -65,7 +65,14 @@ struct CommentView: View {
commentText commentText
if comment.hasReplies { if comment.hasReplies {
repliesButton HStack(spacing: repliesButtonStackSpacing) {
repliesButton
ProgressView()
.scaleEffect(progressViewScale, anchor: .center)
.opacity(repliesID == comment.id && !comments.repliesLoaded ? 1 : 0)
.frame(maxHeight: 0)
}
if comment.id == repliesID { if comment.id == repliesID {
repliesList repliesList
@ -148,16 +155,15 @@ struct CommentView: View {
comments.loadReplies(page: comment.repliesPage!) comments.loadReplies(page: comment.repliesPage!)
} label: { } label: {
HStack(spacing: 5) { HStack(spacing: 5) {
Image(systemName: self.repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down") Image(systemName: repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
Text("Replies") Text("Replies")
} }
#if os(tvOS) #if os(tvOS)
.padding(10) .padding(10)
#endif #endif
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.top, 2) .padding(.vertical, 2)
#if os(tvOS) #if os(tvOS)
.padding(.leading, 5) .padding(.leading, 5)
#else #else
@ -165,6 +171,24 @@ struct CommentView: View {
#endif #endif
} }
private var repliesButtonStackSpacing: Double {
#if os(tvOS)
24
#elseif os(iOS)
4
#else
2
#endif
}
private var progressViewScale: Double {
#if os(macOS)
0.4
#else
0.8
#endif
}
private var repliesList: some View { private var repliesList: some View {
Group { Group {
let last = comments.replies.last let last = comments.replies.last
@ -179,8 +203,8 @@ struct CommentView: View {
.padding(.vertical, 5) .padding(.vertical, 5)
} }
} }
.padding(.leading, 22)
} }
.padding(.leading, 22)
} }
private var commentText: some View { private var commentText: some View {
@ -219,3 +243,13 @@ struct CommentView: View {
} }
} }
} }
struct CommentView_Previews: PreviewProvider {
static var fixture: Comment {
Comment.fixture
}
static var previews: some View {
CommentView(comment: fixture, repliesID: .constant(fixture.id))
}
}

View File

@ -7,41 +7,73 @@ struct CommentsView: View {
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
var body: some View { var body: some View {
ScrollView(.vertical, showsIndicators: false) { Group {
VStack(alignment: .leading) { if comments.disabled {
let last = comments.all.last Text("Comments are disabled for this video")
ForEach(comments.all) { comment in .foregroundColor(.secondary)
CommentView(comment: comment, repliesID: $repliesID) } else if comments.loaded && comments.all.isEmpty {
Text("No comments")
.foregroundColor(.secondary)
} else if !comments.loaded {
progressView
} else {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
let last = comments.all.last
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
if comment != last { if comment != last {
Divider() Divider()
.padding(.vertical, 5) .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")
}
}
}
.buttonStyle(.plain)
.padding(.vertical, 8)
.foregroundColor(.secondary)
} }
} }
HStack {
if comments.nextPageAvailable {
Button {
comments.loadNextPage()
} label: {
Label("Show more", systemImage: "arrow.turn.down.right")
}
}
if !comments.firstPage {
Button {
comments.load(page: nil)
} label: {
Label("Show first", systemImage: "arrow.turn.down.left")
}
}
}
.buttonStyle(.plain)
.padding(.vertical, 5)
.foregroundColor(.secondary)
} }
} }
.padding(.horizontal) .padding(.horizontal)
.onAppear {
if !comments.loaded {
comments.load()
}
}
}
private var progressView: some View {
VStack {
Spacer()
HStack {
Spacer()
ProgressView()
Spacer()
}
Spacer()
}
} }
} }

View File

@ -141,6 +141,8 @@ struct VideoDetails: View {
player.closeCurrentItem() player.closeCurrentItem()
if !sidebarQueue { if !sidebarQueue {
currentPage = .queue currentPage = .queue
} else {
currentPage = .info
} }
} label: { } label: {
Label("Close Video", systemImage: "xmark.circle") Label("Close Video", systemImage: "xmark.circle")