diff --git a/Threaded/Components/CompactPostView.swift b/Threaded/Components/CompactPostView.swift index bf08aef..b8536b7 100644 --- a/Threaded/Components/CompactPostView.swift +++ b/Threaded/Components/CompactPostView.swift @@ -11,6 +11,8 @@ struct CompactPostView: View { @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 var body: some View { VStack { @@ -37,6 +39,13 @@ struct CompactPostView: View { isLiked = status.reblog != nil ? status.reblog!.favourited ?? false : status.favourited ?? false initialLike = isLiked isReposted = status.reblog != nil ? status.reblog!.reblogged ?? false : status.reblogged ?? false + + let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0) + let incrLike: Int = isLiked ? 1 : 0 + print("original: \(status.favouritesCount)\nmin1: \(likeCount)\nincr1: \(likeCount + incrLike)") + } + .task { + await loadEmbeddedStatus() } } @@ -124,6 +133,16 @@ struct CompactPostView: View { if status.card != nil { PostCardView(card: status.card!) } + +// if hasQuote { +// if quoteStatus != nil { +// //TODO: Fix profile picture and stats +// QuotePostView(status: quoteStatus!) +// } else { +// ProgressView() +// .progressViewStyle(.circular) +// } +// } } //MARK: Action buttons @@ -220,12 +239,12 @@ struct CompactPostView: View { } if status.favouritesCount > 0 || isLiked { - let i: Int = status.favouritesCount - let favsCount: Int = i - (initialLike ? 1 : 0) + (isLiked ? 1 : 0) - Text("status.favourites-\(favsCount)") + let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0) + let incrLike: Int = isLiked ? 1 : 0 + Text("status.favourites-\(likeCount + incrLike)") .monospacedDigit() .foregroundStyle(.gray) - .contentTransition(.numericText(value: Double(favsCount))) + .contentTransition(.numericText(value: Double(likeCount + incrLike))) .transaction { t in t.animation = .default } @@ -245,11 +264,12 @@ struct CompactPostView: View { } if status.reblog!.favouritesCount > 0 || isLiked { - let favsCount: Int = (status.favouritesCount - (initialLike ? 1 : 0)) + (isLiked ? 1 : 0) - Text("status.favourites-\(favsCount)") + let likeCount: Int = status.reblog!.favouritesCount - (initialLike ? 1 : 0) + let incrLike: Int = isLiked ? 1 : 0 + Text("status.favourites-\(likeCount + incrLike)") .monospacedDigit() .foregroundStyle(.gray) - .contentTransition(.numericText(value: Double(favsCount))) + .contentTransition(.numericText(value: Double(likeCount + incrLike))) .transaction { t in t.animation = .default } @@ -258,6 +278,33 @@ struct CompactPostView: View { } } + private func embededStatusURL() -> URL? { + let content = status.content + if let client = accountManager.getClient() { + if !content.statusesURLs.isEmpty, let url = content.statusesURLs.first, client.hasConnection(with: url) { + return url + } + } + return nil + } + + func loadEmbeddedStatus() async { + guard let url = embededStatusURL(), let client = accountManager.getClient() else { hasQuote = false; return } + + do { + hasQuote = true + if url.absoluteString.contains(client.server), let id = Int(url.lastPathComponent) { + quoteStatus = try await client.get(endpoint: Statuses.status(id: String(id))) + } else { + let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString, type: "statuses", offset: 0, following: nil), forceVersion: .v2) + quoteStatus = results.statuses.first + } + } catch { + hasQuote = false + quoteStatus = nil + } + } + @ViewBuilder func actionButton(_ image: String, action: @escaping () -> Void) -> some View { Button { diff --git a/Threaded/Components/QuotePostView.swift b/Threaded/Components/QuotePostView.swift index a29400e..81afac0 100644 --- a/Threaded/Components/QuotePostView.swift +++ b/Threaded/Components/QuotePostView.swift @@ -3,11 +3,123 @@ import SwiftUI struct QuotePostView: View { + @Environment(Navigator.self) private var navigator: Navigator + var status: Status + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + statusPost(status) + .frame(width: 250) + .padding(.horizontal, 10) + .clipShape(.rect(cornerRadius: 15)) + .fixedSize(horizontal: false, vertical: true) + .overlay( + RoundedRectangle(cornerRadius: 15) + .stroke(.gray.opacity(0.3), lineWidth: 1) + ) + .onTapGesture { + if UIApplication.shared.canOpenURL(URL(string: status.url ?? .fallbackUrl)!) { + UIApplication.shared.open(URL(string: status.url ?? .fallbackUrl)!) + } + } + } + + @ViewBuilder + func statusPost(_ status: AnyStatus) -> some View { + HStack(alignment: .top, spacing: 0) { + // MARK: Profile picture + if status.repliesCount > 0 { + VStack { + profilePicture + .onTapGesture { + navigator.navigate(to: .account(acc: status.account)) + } + + Spacer() + + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 2.5) + .clipShape(.capsule) + .padding([.vertical], 5) + + Spacer() + + Image(systemName: "person.crop.circle") + .resizable() + .frame(width: 15, height: 15) + .symbolRenderingMode(.monochrome) + .foregroundStyle(Color.gray.opacity(0.3)) + .padding(.bottom, 2.5) + } + } else { + profilePicture + .onTapGesture { + navigator.navigate(to: .account(acc: status.account)) + } + } + + VStack(alignment: .leading) { + // MARK: Status main content + VStack(alignment: .leading, spacing: 10) { + Text(status.account.username) + .multilineTextAlignment(.leading) + .bold() + .onTapGesture { + navigator.navigate(to: .account(acc: status.account)) + } + + if !status.content.asRawText.isEmpty { + TextEmoji(status.content, emojis: status.emojis, language: status.language) + .multilineTextAlignment(.leading) + .frame(width: 250, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .font(.callout) + } + + if status.card != nil { + PostCardView(card: status.card!, inQuote: true) + } + } + .padding(.top) + + // MARK: Status stats + stats + .padding(.top, 5) + .padding(.bottom, status.repliesCount > 0 || status.favouritesCount > 0 ? 10 : 0) + } + } + } + + var profilePicture: some View { + OnlineImage(url: status.account.avatar, size: 40, useNuke: true) + .frame(width: 25, height: 25) + .padding(.horizontal) + .clipShape(.circle) + } + + var stats: some View { + //TODO: Put this in its own view (maybe?) + HStack { + if status.repliesCount > 0 { + Text("status.replies-\(status.repliesCount)") + .monospacedDigit() + .foregroundStyle(.gray) + } + + if status.repliesCount > 0 && status.favouritesCount > 0 { + Text("•") + .foregroundStyle(.gray) + } + + if status.favouritesCount > 0 { + Text("status.favourites-\(status.favouritesCount)") + .monospacedDigit() + .foregroundStyle(.gray) + } + } } } -#Preview { - QuotePostView() +private extension String { + static let fallbackUrl = "https://joinmastodon.org/" } diff --git a/Threaded/Data/SearchResults.swift b/Threaded/Data/SearchResults.swift index 02c3e31..b11bce7 100644 --- a/Threaded/Data/SearchResults.swift +++ b/Threaded/Data/SearchResults.swift @@ -1,3 +1,53 @@ //Made by Lumaa import Foundation + +public struct SearchResults: Decodable { + enum CodingKeys: String, CodingKey { + case accounts, statuses, hashtags + } + + public let accounts: [Account] + public var relationships: [Relationship] = [] + public let statuses: [Status] + public let hashtags: [Tag] + + public var isEmpty: Bool { + accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty + } +} + +extension SearchResults: Sendable {} + +public enum Search: Endpoint { + case search(query: String, type: String?, offset: Int?, following: Bool?) + case accountsSearch(query: String, type: String?, offset: Int?, following: Bool?) + + public func path() -> String { + switch self { + case .search: + "search" + case .accountsSearch: + "accounts/search" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .search(query, type, offset, following), + let .accountsSearch(query, type, offset, following): + var params: [URLQueryItem] = [.init(name: "q", value: query)] + if let type { + params.append(.init(name: "type", value: type)) + } + if let offset { + params.append(.init(name: "offset", value: String(offset))) + } + if let following { + params.append(.init(name: "following", value: following ? "true" : "false")) + } + params.append(.init(name: "resolve", value: "true")) + return params + } + } +}