Post details additions
This commit is contained in:
parent
9c97f532ff
commit
0ae992db20
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue