Post details additions

This commit is contained in:
Lumaa 2024-01-10 17:46:44 +01:00
parent 9c97f532ff
commit 0ae992db20
2 changed files with 108 additions and 60 deletions

View File

@ -240,6 +240,17 @@ public protocol AnyStatus {
var language: String? { get } 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 MediaAttachment: Codable, Identifiable, Hashable, Equatable {
public struct MetaContainer: Codable, Equatable { public struct MetaContainer: Codable, Equatable {
public struct Meta: Codable, Equatable { public struct Meta: Codable, Equatable {

View File

@ -6,23 +6,57 @@ struct PostDetailsView: View {
@Environment(Navigator.self) private var navigator: Navigator @Environment(Navigator.self) private var navigator: Navigator
@Environment(AccountManager.self) private var accountManager: AccountManager @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 initialLike: Bool = false
@State private var isLiked: Bool = false @State private var isLiked: Bool = false
@State private var isReposted: Bool = false @State private var isReposted: Bool = false
@State private var hasQuote: Bool = false @State private var hasQuote: Bool = false
@State private var quoteStatus: Status? = nil @State private var quoteStatus: Status? = nil
var body: some View { init(status: Status) {
VStack { self.detailedStatus = status
statusPost(status, isMain: true)
} }
var body: some View {
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 @ViewBuilder
func statusPost(_ status: AnyStatus, isMain: Bool = false) -> some View { func statusPost(_ status: AnyStatus) -> some View {
VStack { VStack(alignment: .leading) {
HStack { HStack {
profilePicture profilePicture
.onTapGesture { .onTapGesture {
@ -46,6 +80,7 @@ struct PostDetailsView: View {
.frame(width: 300, alignment: .topLeading) .frame(width: 300, alignment: .topLeading)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.font(.callout) .font(.callout)
.id("\(detailedStatus.id)@\(detailedStatus.account.id)")
} }
if status.card != nil && status.mediaAttachments.isEmpty { if status.card != nil && status.mediaAttachments.isEmpty {
@ -58,22 +93,22 @@ struct PostDetailsView: View {
} }
} }
// if hasQuote { if hasQuote {
// if quoteStatus != nil { if quoteStatus != nil {
// QuotePostView(status: quoteStatus!) QuotePostView(status: quoteStatus!)
// } else { } else {
// ProgressView() ProgressView()
// .progressViewStyle(.circular) .progressViewStyle(.circular)
// } }
// } }
} }
//MARK: Action buttons //MARK: Action buttons
HStack(spacing: 13) { HStack(spacing: 13) {
asyncActionButton(isLiked ? "heart.fill" : "heart") { asyncActionButton(isLiked ? "heart.fill" : "heart") {
do { do {
try await likePost()
HapticManager.playHaptics(haptics: Haptic.tap) HapticManager.playHaptics(haptics: Haptic.tap)
try await likePost()
} catch { } catch {
HapticManager.playHaptics(haptics: Haptic.error) HapticManager.playHaptics(haptics: Haptic.error)
print("Error: \(error.localizedDescription)") print("Error: \(error.localizedDescription)")
@ -85,8 +120,8 @@ struct PostDetailsView: View {
} }
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") { asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
do { do {
try await repostPost()
HapticManager.playHaptics(haptics: Haptic.tap) HapticManager.playHaptics(haptics: Haptic.tap)
try await repostPost()
} catch { } catch {
HapticManager.playHaptics(haptics: Haptic.error) HapticManager.playHaptics(haptics: Haptic.error)
print("Error: \(error.localizedDescription)") 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 { func likePost() async throws {
if let client = accountManager.getClient() { if let client = accountManager.getClient() {
guard client.isAuth else { fatalError("Client is not authenticated") } 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) let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId)
isLiked = !isLiked isLiked = !isLiked
@ -123,7 +186,7 @@ struct PostDetailsView: View {
func repostPost() async throws { func repostPost() async throws {
if let client = accountManager.getClient() { if let client = accountManager.getClient() {
guard client.isAuth else { fatalError("Client is not authenticated") } 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) let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId)
isReposted = !isReposted 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 { var profilePicture: some View {
if status.reblog != nil { if detailedStatus.reblog != nil {
OnlineImage(url: status.reblog!.account.avatar, size: 50, useNuke: true) OnlineImage(url: detailedStatus.reblog!.account.avatar, size: 50, useNuke: true)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.padding(.horizontal) .padding(.trailing)
.clipShape(.circle) .clipShape(.circle)
} else { } else {
OnlineImage(url: status.account.avatar, size: 50, useNuke: true) OnlineImage(url: detailedStatus.account.avatar, size: 50, useNuke: true)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.padding(.horizontal) .padding(.trailing)
.clipShape(.circle) .clipShape(.circle)
} }
} }
var stats: some View { var stats: some View {
//MARK: I acknowledge the existance of a count bug here //MARK: I acknowledge the existance of a count bug here
if status.reblog == nil { if detailedStatus.reblog == nil {
HStack { HStack {
if status.repliesCount > 0 { if detailedStatus.repliesCount > 0 {
Text("status.replies-\(status.repliesCount)") Text("status.replies-\(detailedStatus.repliesCount)")
.monospacedDigit() .monospacedDigit()
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
if status.repliesCount > 0 && (status.favouritesCount > 0 || isLiked) { if detailedStatus.repliesCount > 0 && (detailedStatus.favouritesCount > 0 || isLiked) {
Text("") Text("")
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
if status.favouritesCount > 0 || isLiked { if detailedStatus.favouritesCount > 0 || isLiked {
let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0) let likeCount: Int = detailedStatus.favouritesCount - (initialLike ? 1 : 0)
let incrLike: Int = isLiked ? 1 : 0 let incrLike: Int = isLiked ? 1 : 0
Text("status.favourites-\(likeCount + incrLike)") Text("status.favourites-\(likeCount + incrLike)")
.monospacedDigit() .monospacedDigit()
@ -203,19 +240,19 @@ struct PostDetailsView: View {
} }
} else { } else {
HStack { HStack {
if status.reblog!.repliesCount > 0 { if detailedStatus.reblog!.repliesCount > 0 {
Text("status.replies-\(status.reblog!.repliesCount)") Text("status.replies-\(detailedStatus.reblog!.repliesCount)")
.monospacedDigit() .monospacedDigit()
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
if status.reblog!.repliesCount > 0 && (status.reblog!.favouritesCount > 0 || isLiked) { if detailedStatus.reblog!.repliesCount > 0 && (detailedStatus.reblog!.favouritesCount > 0 || isLiked) {
Text("") Text("")
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
if status.reblog!.favouritesCount > 0 || isLiked { if detailedStatus.reblog!.favouritesCount > 0 || isLiked {
let likeCount: Int = status.reblog!.favouritesCount - (initialLike ? 1 : 0) let likeCount: Int = detailedStatus.reblog!.favouritesCount - (initialLike ? 1 : 0)
let incrLike: Int = isLiked ? 1 : 0 let incrLike: Int = isLiked ? 1 : 0
Text("status.favourites-\(likeCount + incrLike)") Text("status.favourites-\(likeCount + incrLike)")
.monospacedDigit() .monospacedDigit()
@ -230,7 +267,7 @@ struct PostDetailsView: View {
} }
private func embededStatusURL() -> URL? { private func embededStatusURL() -> URL? {
let content = status.content let content = detailedStatus.content
if let client = accountManager.getClient() { if let client = accountManager.getClient() {
if !content.statusesURLs.isEmpty, let url = content.statusesURLs.first, client.hasConnection(with: url) { if !content.statusesURLs.isEmpty, let url = content.statusesURLs.first, client.hasConnection(with: url) {
return url return url