Favourite / Unfavourite status

This commit is contained in:
Thomas Ricouard 2022-12-20 20:33:45 +01:00
parent 60a963441c
commit 024d325291
8 changed files with 130 additions and 28 deletions

View file

@ -6,6 +6,7 @@ import Shimmer
import DesignSystem import DesignSystem
public struct AccountDetailView: View { public struct AccountDetailView: View {
@Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@StateObject private var viewModel: AccountDetailViewModel @StateObject private var viewModel: AccountDetailViewModel
@State private var scrollOffset: CGFloat = 0 @State private var scrollOffset: CGFloat = 0
@ -35,6 +36,7 @@ public struct AccountDetailView: View {
} }
} }
.task { .task {
guard reasons != .placeholder else { return }
viewModel.client = client viewModel.client = client
await viewModel.fetchAccount() await viewModel.fetchAccount()
if viewModel.statuses.isEmpty { if viewModel.statuses.isEmpty {

View file

@ -11,6 +11,8 @@ public protocol AnyStatus {
var reblogsCount: Int { get } var reblogsCount: Int { get }
var favouritesCount: Int { get } var favouritesCount: Int { get }
var card: Card? { get } var card: Card? { get }
var favourited: Bool { get }
var reblogged: Bool { get }
} }
public struct Status: AnyStatus, Codable, Identifiable { public struct Status: AnyStatus, Codable, Identifiable {
@ -25,6 +27,8 @@ public struct Status: AnyStatus, Codable, Identifiable {
public let reblogsCount: Int public let reblogsCount: Int
public let favouritesCount: Int public let favouritesCount: Int
public let card: Card? public let card: Card?
public let favourited: Bool
public let reblogged: Bool
public static func placeholder() -> Status { public static func placeholder() -> Status {
.init(id: UUID().uuidString, .init(id: UUID().uuidString,
@ -37,7 +41,9 @@ public struct Status: AnyStatus, Codable, Identifiable {
repliesCount: 0, repliesCount: 0,
reblogsCount: 0, reblogsCount: 0,
favouritesCount: 0, favouritesCount: 0,
card: nil) card: nil,
favourited: false,
reblogged: false)
} }
public static func placeholders() -> [Status] { public static func placeholders() -> [Status] {
@ -56,4 +62,6 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
public let reblogsCount: Int public let reblogsCount: Int
public let favouritesCount: Int public let favouritesCount: Int
public let card: Card? public let card: Card?
public let favourited: Bool
public let reblogged: Bool
} }

View file

@ -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
}
}
}

View file

@ -36,7 +36,7 @@ struct NotificationRowView: View {
} }
} }
if let status = notification.status { if let status = notification.status {
StatusRowView(status: status, isEmbed: true) StatusRowView(viewModel: .init(status: status, isEmbed: true))
} else { } else {
Text(notification.account.acct) Text(notification.account.acct)
.font(.callout) .font(.callout)

View file

@ -15,7 +15,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
switch fetcher.statusesState { switch fetcher.statusesState {
case .loading: case .loading:
ForEach(Status.placeholders()) { status in ForEach(Status.placeholders()) { status in
StatusRowView(status: status) StatusRowView(viewModel: .init(status: status, isEmbed: false))
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.shimmering() .shimmering()
Divider() Divider()
@ -25,7 +25,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
Text(error.localizedDescription) Text(error.localizedDescription)
case let .display(statuses, nextPageState): case let .display(statuses, nextPageState):
ForEach(statuses) { status in ForEach(statuses) { status in
StatusRowView(status: status) StatusRowView(viewModel: .init(status: status, isEmbed: false))
Divider() Divider()
.padding(.vertical, DS.Constants.dividerPadding) .padding(.vertical, DS.Constants.dividerPadding)
} }

View file

@ -1,34 +1,36 @@
import SwiftUI import SwiftUI
import Models import Models
import Routeur import Routeur
import Network
struct StatusActionsView: View { struct StatusActionsView: View {
let status: Status @ObservedObject var viewModel: StatusRowViewModel
@MainActor
enum Actions: CaseIterable { enum Actions: CaseIterable {
case respond, boost, favourite, share case respond, boost, favourite, share
var iconName: String { func iconName(viewModel: StatusRowViewModel) -> String {
switch self { switch self {
case .respond: case .respond:
return "bubble.right" return "bubble.right"
case .boost: case .boost:
return "arrow.left.arrow.right.circle" return "arrow.left.arrow.right.circle"
case .favourite: case .favourite:
return "star" return viewModel.isFavourited ? "star.fill" : "star"
case .share: case .share:
return "square.and.arrow.up" return "square.and.arrow.up"
} }
} }
func count(status: Status) -> Int? { func count(viewModel: StatusRowViewModel) -> Int? {
switch self { switch self {
case .respond: case .respond:
return status.repliesCount return viewModel.status.repliesCount
case .favourite: case .favourite:
return status.favouritesCount return viewModel.favouritesCount
case .boost: case .boost:
return status.reblogsCount return viewModel.status.reblogsCount
case .share: case .share:
return nil return nil
} }
@ -39,11 +41,11 @@ struct StatusActionsView: View {
HStack { HStack {
ForEach(Actions.allCases, id: \.self) { action in ForEach(Actions.allCases, id: \.self) { action in
Button { Button {
handleAction(action: action)
} label: { } label: {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName: action.iconName) Image(systemName: action.iconName(viewModel: viewModel))
if let count = action.count(status: status) { if let count = action.count(viewModel: viewModel) {
Text("\(count)") Text("\(count)")
.font(.footnote) .font(.footnote)
} }
@ -53,6 +55,22 @@ struct StatusActionsView: View {
Spacer() 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
}
}
} }
} }

View file

@ -8,30 +8,30 @@ public struct StatusRowView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
@StateObject var viewModel: StatusRowViewModel
private let status: Status public init(viewModel: StatusRowViewModel) {
private let isEmbed: Bool _viewModel = StateObject(wrappedValue: viewModel)
public init(status: Status, isEmbed: Bool = false) {
self.status = status
self.isEmbed = isEmbed
} }
public var body: some View { public var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
reblogView reblogView
statusView statusView
StatusActionsView(status: status) StatusActionsView(viewModel: viewModel)
.padding(.vertical, 8) .padding(.vertical, 8)
} }
.onAppear {
viewModel.client = client
}
} }
@ViewBuilder @ViewBuilder
private var reblogView: some View { private var reblogView: some View {
if status.reblog != nil { if viewModel.status.reblog != nil {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName:"arrow.left.arrow.right.circle") Image(systemName:"arrow.left.arrow.right.circle")
Text("\(status.account.displayName) reblogged") Text("\(viewModel.status.account.displayName) reblogged")
} }
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
@ -41,8 +41,8 @@ public struct StatusRowView: View {
private var statusView: some View { private var statusView: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
if let status: AnyStatus = status.reblog ?? status { if let status: AnyStatus = viewModel.status.reblog ?? viewModel.status {
if !isEmbed { if !viewModel.isEmbed {
Button { Button {
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
} label: { } label: {

View file

@ -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
}
}