From 024d325291f7b8686a0fc2fc7d40e4d25b0de588 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 20 Dec 2022 20:33:45 +0100 Subject: [PATCH] Favourite / Unfavourite status --- .../Sources/Account/AccountDetailView.swift | 2 + Packages/Models/Sources/Models/Status.swift | 10 +++- .../Sources/Network/Endpoint/Statuses.swift | 22 ++++++++ .../Notifications/NotificationRowView.swift | 2 +- .../Status/List/StatusesListView.swift | 4 +- .../Status/Row/StatusActionsView.swift | 42 ++++++++++----- .../Sources/Status/Row/StatusRowView.swift | 24 ++++----- .../Status/Row/StatusRowViewModel.swift | 52 +++++++++++++++++++ 8 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 Packages/Network/Sources/Network/Endpoint/Statuses.swift create mode 100644 Packages/Status/Sources/Status/Row/StatusRowViewModel.swift diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index f0da322c..5ee96e12 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -6,6 +6,7 @@ import Shimmer import DesignSystem public struct AccountDetailView: View { + @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var client: Client @StateObject private var viewModel: AccountDetailViewModel @State private var scrollOffset: CGFloat = 0 @@ -35,6 +36,7 @@ public struct AccountDetailView: View { } } .task { + guard reasons != .placeholder else { return } viewModel.client = client await viewModel.fetchAccount() if viewModel.statuses.isEmpty { diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index b11c2a28..cb0afaa2 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -11,6 +11,8 @@ public protocol AnyStatus { var reblogsCount: Int { get } var favouritesCount: Int { get } var card: Card? { get } + var favourited: Bool { get } + var reblogged: Bool { get } } public struct Status: AnyStatus, Codable, Identifiable { @@ -25,6 +27,8 @@ public struct Status: AnyStatus, Codable, Identifiable { public let reblogsCount: Int public let favouritesCount: Int public let card: Card? + public let favourited: Bool + public let reblogged: Bool public static func placeholder() -> Status { .init(id: UUID().uuidString, @@ -37,7 +41,9 @@ public struct Status: AnyStatus, Codable, Identifiable { repliesCount: 0, reblogsCount: 0, favouritesCount: 0, - card: nil) + card: nil, + favourited: false, + reblogged: false) } public static func placeholders() -> [Status] { @@ -56,4 +62,6 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable { public let reblogsCount: Int public let favouritesCount: Int public let card: Card? + public let favourited: Bool + public let reblogged: Bool } diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift new file mode 100644 index 00000000..66d70bc0 --- /dev/null +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -0,0 +1,22 @@ +import Foundation + +public enum Statuses: Endpoint { + case favourite(id: String) + case unfavourite(id: String) + + public func path() -> String { + switch self { + case .favourite(let id): + return "statuses/\(id)/favourite" + case .unfavourite(let id): + return "statuses/\(id)/unfavourite" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + default: + return nil + } + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index e2534097..ebd53f75 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -36,7 +36,7 @@ struct NotificationRowView: View { } } if let status = notification.status { - StatusRowView(status: status, isEmbed: true) + StatusRowView(viewModel: .init(status: status, isEmbed: true)) } else { Text(notification.account.acct) .font(.callout) diff --git a/Packages/Status/Sources/Status/List/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift index 40676dfc..dbb1b909 100644 --- a/Packages/Status/Sources/Status/List/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -15,7 +15,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { switch fetcher.statusesState { case .loading: ForEach(Status.placeholders()) { status in - StatusRowView(status: status) + StatusRowView(viewModel: .init(status: status, isEmbed: false)) .redacted(reason: .placeholder) .shimmering() Divider() @@ -25,7 +25,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { Text(error.localizedDescription) case let .display(statuses, nextPageState): ForEach(statuses) { status in - StatusRowView(status: status) + StatusRowView(viewModel: .init(status: status, isEmbed: false)) Divider() .padding(.vertical, DS.Constants.dividerPadding) } diff --git a/Packages/Status/Sources/Status/Row/StatusActionsView.swift b/Packages/Status/Sources/Status/Row/StatusActionsView.swift index 3123252e..897d8516 100644 --- a/Packages/Status/Sources/Status/Row/StatusActionsView.swift +++ b/Packages/Status/Sources/Status/Row/StatusActionsView.swift @@ -1,34 +1,36 @@ import SwiftUI import Models import Routeur +import Network struct StatusActionsView: View { - let status: Status - + @ObservedObject var viewModel: StatusRowViewModel + + @MainActor enum Actions: CaseIterable { case respond, boost, favourite, share - var iconName: String { + func iconName(viewModel: StatusRowViewModel) -> String { switch self { case .respond: return "bubble.right" case .boost: return "arrow.left.arrow.right.circle" case .favourite: - return "star" + return viewModel.isFavourited ? "star.fill" : "star" case .share: return "square.and.arrow.up" } } - func count(status: Status) -> Int? { + func count(viewModel: StatusRowViewModel) -> Int? { switch self { case .respond: - return status.repliesCount + return viewModel.status.repliesCount case .favourite: - return status.favouritesCount + return viewModel.favouritesCount case .boost: - return status.reblogsCount + return viewModel.status.reblogsCount case .share: return nil } @@ -39,11 +41,11 @@ struct StatusActionsView: View { HStack { ForEach(Actions.allCases, id: \.self) { action in Button { - + handleAction(action: action) } label: { HStack(spacing: 2) { - Image(systemName: action.iconName) - if let count = action.count(status: status) { + Image(systemName: action.iconName(viewModel: viewModel)) + if let count = action.count(viewModel: viewModel) { Text("\(count)") .font(.footnote) } @@ -53,6 +55,22 @@ struct StatusActionsView: View { Spacer() } } - }.tint(.gray) + } + .tint(.gray) + } + + private func handleAction(action: Actions) { + Task { + switch action { + case .favourite: + if viewModel.isFavourited { + await viewModel.unFavourite() + } else { + await viewModel.favourite() + } + default: + break + } + } } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 21622cb3..e72b4671 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -8,30 +8,30 @@ public struct StatusRowView: View { @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var client: Client @EnvironmentObject private var routeurPath: RouterPath + @StateObject var viewModel: StatusRowViewModel - private let status: Status - private let isEmbed: Bool - - public init(status: Status, isEmbed: Bool = false) { - self.status = status - self.isEmbed = isEmbed + public init(viewModel: StatusRowViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) } - + public var body: some View { VStack(alignment: .leading) { reblogView statusView - StatusActionsView(status: status) + StatusActionsView(viewModel: viewModel) .padding(.vertical, 8) } + .onAppear { + viewModel.client = client + } } @ViewBuilder private var reblogView: some View { - if status.reblog != nil { + if viewModel.status.reblog != nil { HStack(spacing: 2) { Image(systemName:"arrow.left.arrow.right.circle") - Text("\(status.account.displayName) reblogged") + Text("\(viewModel.status.account.displayName) reblogged") } .font(.footnote) .foregroundColor(.gray) @@ -41,8 +41,8 @@ public struct StatusRowView: View { private var statusView: some View { VStack(alignment: .leading, spacing: 8) { - if let status: AnyStatus = status.reblog ?? status { - if !isEmbed { + if let status: AnyStatus = viewModel.status.reblog ?? viewModel.status { + if !viewModel.isEmbed { Button { routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) } label: { diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift new file mode 100644 index 00000000..c9db5efc --- /dev/null +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -0,0 +1,52 @@ +import SwiftUI +import Models +import Network + +@MainActor +public class StatusRowViewModel: ObservableObject { + let status: Status + let isEmbed: Bool + + @Published var favouritesCount: Int + @Published var isFavourited: Bool + + var client: Client? + + public init(status: Status, isEmbed: Bool) { + self.status = status + self.isEmbed = isEmbed + self.isFavourited = status.favourited + self.favouritesCount = status.favouritesCount + } + + func favourite() async { + guard let client else { return } + isFavourited = true + favouritesCount += 1 + do { + let status: Status = try await client.post(endpoint: Statuses.favourite(id: status.id)) + updateFromStatus(status: status) + } catch { + isFavourited = false + favouritesCount -= 1 + } + } + + func unFavourite() async { + guard let client else { return } + isFavourited = false + favouritesCount -= 1 + do { + let status: Status = try await client.post(endpoint: Statuses.unfavourite(id: status.id)) + updateFromStatus(status: status) + } catch { + isFavourited = true + favouritesCount += 1 + } + } + + private func updateFromStatus(status: Status) { + isFavourited = status.favourited + favouritesCount = status.favouritesCount + } +}