diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 74033851..1754be85 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -46,7 +46,8 @@ public struct AccountDetailView: View { Picker("", selection: $viewModel.selectedTab) { ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs, id: \.self) { tab in - Text(tab.title).tag(tab) + Image(systemName: tab.iconName) + .tag(tab) } } .pickerStyle(.segmented) diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index b78c0988..32c30eb4 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -15,24 +15,25 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } enum Tab: Int { - case statuses, favourites, followedTags, postsAndReplies, media, lists + case statuses, favourites, bookmarks, followedTags, postsAndReplies, media, lists static var currentAccountTabs: [Tab] { - [.statuses, .favourites, .followedTags, .lists] + [.statuses, .favourites, .bookmarks, .followedTags, .lists] } static var accountTabs: [Tab] { [.statuses, .postsAndReplies, .media] } - var title: String { + var iconName: String { switch self { - case .statuses: return "Posts" - case .favourites: return "Favorites" - case .followedTags: return "Tags" - case .postsAndReplies: return "Posts & Replies" - case .media: return "Media" - case .lists: return "Lists" + case .statuses: return "bubble.right" + case .favourites: return "star" + case .bookmarks: return "bookmark" + case .followedTags: return "tag" + case .postsAndReplies: return "bubble.left.and.bubble.right" + case .media: return "photo.on.rectangle.angled" + case .lists: return "list.bullet" } } } @@ -61,7 +62,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { @Published var relationship: Relationshionship? @Published var pinned: [Status] = [] @Published var favourites: [Status] = [] + @Published var bookmarks: [Status] = [] private var favouritesNextPage: LinkHandler? + private var bookmarksNextPage: LinkHandler? @Published var featuredTags: [FeaturedTag] = [] @Published var fields: [Account.Field] = [] @Published var familliarFollowers: [Account] = [] @@ -145,6 +148,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } if isCurrentUser { (favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil)) + (bookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nil)) } reloadTabState() } catch { @@ -175,6 +179,12 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { (newFavourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nextPageId)) favourites.append(contentsOf: newFavourites) tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage)) + case .bookmarks: + guard let nextPageId = bookmarksNextPage?.maxId else { return } + let newBookmarks: [Status] + (newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId)) + bookmarks.append(contentsOf: newBookmarks) + tabState = .statuses(statusesState: .display(statuses: bookmarks, nextPageState: .hasNextPage)) case .followedTags, .lists: break } @@ -208,6 +218,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { case .favourites: tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: favouritesNextPage != nil ? .hasNextPage : .none)) + case .bookmarks: + tabState = .statuses(statusesState: .display(statuses: bookmarks, + nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none)) case .followedTags: tabState = .followedTags case .lists: diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 514ce828..8a4fd3ff 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -40,6 +40,7 @@ public protocol AnyStatus { var favourited: Bool? { get } var reblogged: Bool? { get } var pinned: Bool? { get } + var bookmarked: Bool? { get } var emojis: [Emoji] { get } var url: URL? { get } var application: Application? { get } @@ -71,6 +72,7 @@ public struct Status: AnyStatus, Codable, Identifiable { public let favourited: Bool? public let reblogged: Bool? public let pinned: Bool? + public let bookmarked: Bool? public let emojis: [Emoji] public let url: URL? public let application: Application? @@ -96,6 +98,7 @@ public struct Status: AnyStatus, Codable, Identifiable { favourited: false, reblogged: false, pinned: false, + bookmarked: false, emojis: [], url: nil, application: nil, @@ -130,6 +133,7 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable { public let favourited: Bool? public let reblogged: Bool? public let pinned: Bool? + public let bookmarked: Bool? public let emojis: [Emoji] public let url: URL? public var application: Application? diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index 3830b63e..7a120fc0 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -3,6 +3,7 @@ import Foundation public enum Accounts: Endpoint { case accounts(id: String) case favourites(sinceId: String?) + case bookmarks(sinceId: String?) case followedTags case featuredTags(id: String) case verifyCredentials @@ -27,6 +28,8 @@ public enum Accounts: Endpoint { return "accounts/\(id)" case .favourites: return "favourites" + case .bookmarks: + return "bookmarks" case .followedTags: return "followed_tags" case .featuredTags(let id): @@ -87,6 +90,9 @@ public enum Accounts: Endpoint { case let .favourites(sinceId): guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] + case let .bookmarks(sinceId): + guard let sinceId else { return nil } + return [.init(name: "max_id", value: sinceId)] default: return nil } diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift index 653ba7f6..fe6092e2 100644 --- a/Packages/Network/Sources/Network/Endpoint/Statuses.swift +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -22,6 +22,8 @@ public enum Statuses: Endpoint { case favouritedBy(id: String, maxId: String?) case pin(id: String) case unpin(id: String) + case bookmark(id: String) + case unbookmark(id: String) public func path() -> String { switch self { @@ -49,6 +51,10 @@ public enum Statuses: Endpoint { return "statuses/\(id)/pin" case let .unpin(id): return "statuses/\(id)/unpin" + case let .bookmark(id): + return "statuses/\(id)/bookmark" + case let .unbookmark(id): + return "statuses/\(id)/unbookmark" } } diff --git a/Packages/Status/Sources/Status/Row/StatusActionsView.swift b/Packages/Status/Sources/Status/Row/StatusActionsView.swift index 887a8e5a..a539b01e 100644 --- a/Packages/Status/Sources/Status/Row/StatusActionsView.swift +++ b/Packages/Status/Sources/Status/Row/StatusActionsView.swift @@ -14,7 +14,7 @@ struct StatusActionsView: View { @MainActor enum Actions: CaseIterable { - case respond, boost, favourite, share + case respond, boost, favourite, bookmark, share func iconName(viewModel: StatusRowViewModel) -> String { switch self { @@ -24,6 +24,8 @@ struct StatusActionsView: View { return viewModel.isReblogged ? "arrow.left.arrow.right.circle.fill" : "arrow.left.arrow.right.circle" case .favourite: return viewModel.isFavourited ? "star.fill" : "star" + case .bookmark: + return viewModel.isBookmarked ? "bookmark.fill" : "bookmark" case .share: return "square.and.arrow.up" } @@ -40,7 +42,7 @@ struct StatusActionsView: View { return viewModel.favouritesCount case .boost: return viewModel.reblogsCount - case .share: + case .share, .bookmark: return nil } } @@ -51,6 +53,8 @@ struct StatusActionsView: View { return nil case .favourite: return viewModel.isFavourited ? .yellow : nil + case .bookmark: + return viewModel.isBookmarked ? .pink : nil case .boost: return viewModel.isReblogged ? theme.tintColor : nil } @@ -150,6 +154,12 @@ struct StatusActionsView: View { } else { await viewModel.favourite() } + case .bookmark: + if viewModel.isBookmarked { + await viewModel.unbookmark() + } else { + await viewModel.bookmark() + } case .boost: if viewModel.isReblogged { await viewModel.unReblog() diff --git a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift index 0b1f4713..9c342966 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift @@ -30,7 +30,16 @@ struct StatusRowContextMenu: View { } } label: { Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle") } - + Button { Task { + if viewModel.isBookmarked { + await viewModel.unbookmark() + } else { + await viewModel.bookmark() + } + } } label: { + Label(viewModel.isReblogged ? "Unbookmark" : "Bookmark", + systemImage: "bookmark") + } } if viewModel.status.visibility == .pub, !viewModel.isRemote { diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 52386b49..8cb94d72 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -14,6 +14,7 @@ public class StatusRowViewModel: ObservableObject { @Published var isFavourited: Bool @Published var isReblogged: Bool @Published var isPinned: Bool + @Published var isBookmarked: Bool @Published var reblogsCount: Int @Published var repliesCount: Int @Published var embededStatus: Status? @@ -39,10 +40,12 @@ public class StatusRowViewModel: ObservableObject { self.isFavourited = reblog.favourited == true self.isReblogged = reblog.reblogged == true self.isPinned = reblog.pinned == true + self.isBookmarked = reblog.bookmarked == true } else { self.isFavourited = status.favourited == true self.isReblogged = status.reblogged == true self.isPinned = status.pinned == true + self.isBookmarked = status.bookmarked == true } self.favouritesCount = status.reblog?.favouritesCount ?? status.favouritesCount self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount @@ -166,6 +169,28 @@ public class StatusRowViewModel: ObservableObject { } } + func bookmark() async { + guard let client, client.isAuth else { return } + isBookmarked = true + do { + let status: Status = try await client.post(endpoint: Statuses.bookmark(id: status.reblog?.id ?? status.id)) + updateFromStatus(status: status) + } catch { + isBookmarked = false + } + } + + func unbookmark() async { + guard let client, client.isAuth else { return } + isBookmarked = false + do { + let status: Status = try await client.post(endpoint: Statuses.unbookmark(id: status.reblog?.id ?? status.id)) + updateFromStatus(status: status) + } catch { + isBookmarked = true + } + } + func delete() async { guard let client else { return } do { @@ -178,10 +203,12 @@ public class StatusRowViewModel: ObservableObject { isFavourited = reblog.favourited == true isReblogged = reblog.reblogged == true isPinned = reblog.pinned == true + isBookmarked = reblog.bookmarked == true } else { isFavourited = status.favourited == true isReblogged = status.reblogged == true isPinned = status.pinned == true + isBookmarked = status.bookmarked == true } favouritesCount = status.reblog?.favouritesCount ?? status.favouritesCount reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount