mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-20 12:58:07 +00:00
Favourite / Unfavourite status
This commit is contained in:
parent
60a963441c
commit
024d325291
8 changed files with 130 additions and 28 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
22
Packages/Network/Sources/Network/Endpoint/Statuses.swift
Normal file
22
Packages/Network/Sources/Network/Endpoint/Statuses.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
52
Packages/Status/Sources/Status/Row/StatusRowViewModel.swift
Normal file
52
Packages/Status/Sources/Status/Row/StatusRowViewModel.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue