From 0ae992db2010aeedb7d90d8610dda980f7ff03a8 Mon Sep 17 00:00:00 2001 From: Lumaa Date: Wed, 10 Jan 2024 17:46:44 +0100 Subject: [PATCH] Post details additions --- Threaded/Data/Status.swift | 11 ++ Threaded/Views/PostDetailsView.swift | 157 +++++++++++++++++---------- 2 files changed, 108 insertions(+), 60 deletions(-) diff --git a/Threaded/Data/Status.swift b/Threaded/Data/Status.swift index 1a2d306..5b7bb12 100644 --- a/Threaded/Data/Status.swift +++ b/Threaded/Data/Status.swift @@ -240,6 +240,17 @@ public protocol AnyStatus { var language: String? { get } } +public struct StatusContext: Decodable { + public let ancestors: [Status] + public let descendants: [Status] + + public static func empty() -> StatusContext { + .init(ancestors: [], descendants: []) + } +} + +extension StatusContext: Sendable {} + public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { public struct MetaContainer: Codable, Equatable { public struct Meta: Codable, Equatable { diff --git a/Threaded/Views/PostDetailsView.swift b/Threaded/Views/PostDetailsView.swift index 69be9f9..b836d6d 100644 --- a/Threaded/Views/PostDetailsView.swift +++ b/Threaded/Views/PostDetailsView.swift @@ -6,23 +6,57 @@ struct PostDetailsView: View { @Environment(Navigator.self) private var navigator: Navigator @Environment(AccountManager.self) private var accountManager: AccountManager - var status: Status + var detailedStatus: Status + @State private var statuses: [Status] = [] + @State private var scrollId: String? = nil @State private var initialLike: Bool = false @State private var isLiked: Bool = false @State private var isReposted: Bool = false @State private var hasQuote: Bool = false @State private var quoteStatus: Status? = nil + init(status: Status) { + self.detailedStatus = status + } + var body: some View { - VStack { - statusPost(status, isMain: true) + ScrollView(.vertical) { + ScrollViewReader { proxy in + VStack(alignment: .leading) { + if statuses.isEmpty { + statusPost(detailedStatus) + + Spacer() + } else { + ForEach(statuses) { status in + if status.id == detailedStatus.id { + statusPost(detailedStatus) + .padding(.horizontal, 15) + .padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical) + .onAppear { + proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom) + } + } else { + CompactPostView(status: status, navigator: navigator) + } + } + } + } + .task { + await fetchStatusDetail() + } + } } + .background(Color.appBackground) + .toolbarBackground(Color.appBackground, for: .navigationBar) + .safeAreaPadding() + .navigationBarTitleDisplayMode(.inline) } @ViewBuilder - func statusPost(_ status: AnyStatus, isMain: Bool = false) -> some View { - VStack { + func statusPost(_ status: AnyStatus) -> some View { + VStack(alignment: .leading) { HStack { profilePicture .onTapGesture { @@ -46,6 +80,7 @@ struct PostDetailsView: View { .frame(width: 300, alignment: .topLeading) .fixedSize(horizontal: false, vertical: true) .font(.callout) + .id("\(detailedStatus.id)@\(detailedStatus.account.id)") } if status.card != nil && status.mediaAttachments.isEmpty { @@ -58,22 +93,22 @@ struct PostDetailsView: View { } } -// if hasQuote { -// if quoteStatus != nil { -// QuotePostView(status: quoteStatus!) -// } else { -// ProgressView() -// .progressViewStyle(.circular) -// } -// } + if hasQuote { + if quoteStatus != nil { + QuotePostView(status: quoteStatus!) + } else { + ProgressView() + .progressViewStyle(.circular) + } + } } //MARK: Action buttons HStack(spacing: 13) { asyncActionButton(isLiked ? "heart.fill" : "heart") { do { - try await likePost() HapticManager.playHaptics(haptics: Haptic.tap) + try await likePost() } catch { HapticManager.playHaptics(haptics: Haptic.error) print("Error: \(error.localizedDescription)") @@ -85,8 +120,8 @@ struct PostDetailsView: View { } asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") { do { - try await repostPost() HapticManager.playHaptics(haptics: Haptic.tap) + try await repostPost() } catch { HapticManager.playHaptics(haptics: Haptic.error) print("Error: \(error.localizedDescription)") @@ -106,10 +141,38 @@ struct PostDetailsView: View { } } + private func fetchStatusDetail() async { + guard let client = accountManager.getClient() else { return } + do { + let data = try await fetchContextData(client: client, statusId: detailedStatus.id) + + var statusesContext = data.context.ancestors + statusesContext.append(data.status) + statusesContext.append(contentsOf: data.context.descendants) + + statuses = statusesContext + } catch { + if let error = error as? ServerError, error.httpCode == 404 { + _ = navigator.path.popLast() + } + } + } + + private func fetchContextData(client: Client, statusId: String) async throws -> ContextData { + async let status: Status = client.get(endpoint: Statuses.status(id: statusId)) + async let context: StatusContext = client.get(endpoint: Statuses.context(id: statusId)) + return try await .init(status: status, context: context) + } + + struct ContextData { + let status: Status + let context: StatusContext + } + func likePost() async throws { if let client = accountManager.getClient() { guard client.isAuth else { fatalError("Client is not authenticated") } - let statusId: String = status.reblog != nil ? status.reblog!.id : status.id + let statusId: String = detailedStatus.reblog != nil ? detailedStatus.reblog!.id : detailedStatus.id let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId) isLiked = !isLiked @@ -123,7 +186,7 @@ struct PostDetailsView: View { func repostPost() async throws { if let client = accountManager.getClient() { guard client.isAuth else { fatalError("Client is not authenticated") } - let statusId: String = status.reblog != nil ? status.reblog!.id : status.id + let statusId: String = detailedStatus.reblog != nil ? detailedStatus.reblog!.id : detailedStatus.id let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId) isReposted = !isReposted @@ -134,63 +197,37 @@ struct PostDetailsView: View { } } - var pinnedNotice: some View { - HStack (alignment:.center, spacing: 5) { - Image(systemName: "pin.fill") - - Text("status.pinned") - } - .padding(.leading, 20) - .multilineTextAlignment(.leading) - .lineLimit(1) - .font(.caption) - .foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3)) - } - - var repostNotice: some View { - HStack (alignment:.center, spacing: 5) { - Image(systemName: "bolt.horizontal") - - Text("status.reposted-by.\(status.account.username)") - } - .padding(.leading, 20) - .multilineTextAlignment(.leading) - .lineLimit(1) - .font(.caption) - .foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3)) - } - var profilePicture: some View { - if status.reblog != nil { - OnlineImage(url: status.reblog!.account.avatar, size: 50, useNuke: true) + if detailedStatus.reblog != nil { + OnlineImage(url: detailedStatus.reblog!.account.avatar, size: 50, useNuke: true) .frame(width: 40, height: 40) - .padding(.horizontal) + .padding(.trailing) .clipShape(.circle) } else { - OnlineImage(url: status.account.avatar, size: 50, useNuke: true) + OnlineImage(url: detailedStatus.account.avatar, size: 50, useNuke: true) .frame(width: 40, height: 40) - .padding(.horizontal) + .padding(.trailing) .clipShape(.circle) } } var stats: some View { //MARK: I acknowledge the existance of a count bug here - if status.reblog == nil { + if detailedStatus.reblog == nil { HStack { - if status.repliesCount > 0 { - Text("status.replies-\(status.repliesCount)") + if detailedStatus.repliesCount > 0 { + Text("status.replies-\(detailedStatus.repliesCount)") .monospacedDigit() .foregroundStyle(.gray) } - if status.repliesCount > 0 && (status.favouritesCount > 0 || isLiked) { + if detailedStatus.repliesCount > 0 && (detailedStatus.favouritesCount > 0 || isLiked) { Text("•") .foregroundStyle(.gray) } - if status.favouritesCount > 0 || isLiked { - let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0) + if detailedStatus.favouritesCount > 0 || isLiked { + let likeCount: Int = detailedStatus.favouritesCount - (initialLike ? 1 : 0) let incrLike: Int = isLiked ? 1 : 0 Text("status.favourites-\(likeCount + incrLike)") .monospacedDigit() @@ -203,19 +240,19 @@ struct PostDetailsView: View { } } else { HStack { - if status.reblog!.repliesCount > 0 { - Text("status.replies-\(status.reblog!.repliesCount)") + if detailedStatus.reblog!.repliesCount > 0 { + Text("status.replies-\(detailedStatus.reblog!.repliesCount)") .monospacedDigit() .foregroundStyle(.gray) } - if status.reblog!.repliesCount > 0 && (status.reblog!.favouritesCount > 0 || isLiked) { + if detailedStatus.reblog!.repliesCount > 0 && (detailedStatus.reblog!.favouritesCount > 0 || isLiked) { Text("•") .foregroundStyle(.gray) } - if status.reblog!.favouritesCount > 0 || isLiked { - let likeCount: Int = status.reblog!.favouritesCount - (initialLike ? 1 : 0) + if detailedStatus.reblog!.favouritesCount > 0 || isLiked { + let likeCount: Int = detailedStatus.reblog!.favouritesCount - (initialLike ? 1 : 0) let incrLike: Int = isLiked ? 1 : 0 Text("status.favourites-\(likeCount + incrLike)") .monospacedDigit() @@ -230,7 +267,7 @@ struct PostDetailsView: View { } private func embededStatusURL() -> URL? { - let content = status.content + let content = detailedStatus.content if let client = accountManager.getClient() { if !content.statusesURLs.isEmpty, let url = content.statusesURLs.first, client.hasConnection(with: url) { return url