From 4358ed6a382b8cb854818a2463226f6d9fdd1a43 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Mon, 23 Jan 2023 08:43:04 +0100 Subject: [PATCH] Add favourites/bookmarks to profile --- .../MastodonKit/MastodonClient+Account.swift | 26 ++++++ .../MastodonKit/Targets/Bookmarks.swift | 74 +++++++++++++++++ .../MastodonKit/Targets/Favourites.swift | 33 +++++++- Vernissage.xcodeproj/project.pbxproj | 12 +-- Vernissage/Services/AccountService.swift | 26 ++++++ Vernissage/Views/FederatedFeedView.swift | 13 --- Vernissage/Views/MainView.swift | 4 +- ...elineFeedView.swift => StatusesView.swift} | 83 ++++++++++++++----- .../UserProfile/UserProfileHeader.swift | 16 ++-- 9 files changed, 231 insertions(+), 56 deletions(-) create mode 100644 MastodonKit/Sources/MastodonKit/Targets/Bookmarks.swift delete mode 100644 Vernissage/Views/FederatedFeedView.swift rename Vernissage/Views/{TimelineFeedView.swift => StatusesView.swift} (71%) diff --git a/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift b/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift index 0c343e7..6703893 100644 --- a/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift +++ b/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift @@ -133,4 +133,30 @@ public extension MastodonClientAuthenticated { return try await downloadJson([Account].self, request: request) } + + func favourites(maxId: EntityId? = nil, + sinceId: EntityId? = nil, + minId: EntityId? = nil, + limit: Int? = nil) async throws -> [Status] { + let request = try Self.request( + for: baseURL, + target: Mastodon.Favourites.favourites(maxId, sinceId, minId, limit), + withBearerToken: token + ) + + return try await downloadJson([Status].self, request: request) + } + + func bookmarks(maxId: EntityId? = nil, + sinceId: EntityId? = nil, + minId: EntityId? = nil, + limit: Int? = nil) async throws -> [Status] { + let request = try Self.request( + for: baseURL, + target: Mastodon.Bookmarks.bookmarks(maxId, sinceId, minId, limit), + withBearerToken: token + ) + + return try await downloadJson([Status].self, request: request) + } } diff --git a/MastodonKit/Sources/MastodonKit/Targets/Bookmarks.swift b/MastodonKit/Sources/MastodonKit/Targets/Bookmarks.swift new file mode 100644 index 0000000..f125291 --- /dev/null +++ b/MastodonKit/Sources/MastodonKit/Targets/Bookmarks.swift @@ -0,0 +1,74 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +extension Mastodon { + public enum Bookmarks { + case bookmarks(MaxId?, SinceId?, MinId?, Limit?) + } +} + +extension Mastodon.Bookmarks: TargetType { + fileprivate var apiPath: String { return "/api/v1/bookmarks" } + + /// The path to be appended to `baseURL` to form the full `URL`. + public var path: String { + switch self { + case .bookmarks(_, _, _, _): + return "\(apiPath)" + } + } + + /// The HTTP method used in the request. + public var method: Method { + switch self { + case .bookmarks: + return .get + } + } + + /// The parameters to be incoded in the request. + public var queryItems: [(String, String)]? { + var params: [(String, String)] = [] + + var maxId: MaxId? = nil + var sinceId: SinceId? = nil + var minId: MinId? = nil + var limit: Limit? = nil + + switch self { + case .bookmarks(let _maxId, let _sinceId, let _minId, let _limit): + maxId = _maxId + sinceId = _sinceId + minId = _minId + limit = _limit + } + + if let maxId { + params.append(("max_id", maxId)) + } + if let sinceId { + params.append(("since_id", sinceId)) + } + if let minId { + params.append(("min_id", minId)) + } + if let limit { + params.append(("limit", "\(limit)")) + } + + return params + } + + public var headers: [String: String]? { + [:].contentTypeApplicationJson + } + + public var httpBody: Data? { + nil + } +} diff --git a/MastodonKit/Sources/MastodonKit/Targets/Favourites.swift b/MastodonKit/Sources/MastodonKit/Targets/Favourites.swift index bbec584..b948095 100644 --- a/MastodonKit/Sources/MastodonKit/Targets/Favourites.swift +++ b/MastodonKit/Sources/MastodonKit/Targets/Favourites.swift @@ -8,7 +8,7 @@ import Foundation extension Mastodon { public enum Favourites { - case favourites + case favourites(MaxId?, SinceId?, MinId?, Limit?) } } @@ -18,7 +18,7 @@ extension Mastodon.Favourites: TargetType { /// The path to be appended to `baseURL` to form the full `URL`. public var path: String { switch self { - case .favourites: + case .favourites(_, _, _, _): return "\(apiPath)" } } @@ -33,10 +33,35 @@ extension Mastodon.Favourites: TargetType { /// The parameters to be incoded in the request. public var queryItems: [(String, String)]? { + var params: [(String, String)] = [] + + var maxId: MaxId? = nil + var sinceId: SinceId? = nil + var minId: MinId? = nil + var limit: Limit? = nil + switch self { - case .favourites: - return nil + case .favourites(let _maxId, let _sinceId, let _minId, let _limit): + maxId = _maxId + sinceId = _sinceId + minId = _minId + limit = _limit } + + if let maxId { + params.append(("max_id", maxId)) + } + if let sinceId { + params.append(("since_id", sinceId)) + } + if let minId { + params.append(("min_id", minId)) + } + if let limit { + params.append(("limit", "\(limit)")) + } + + return params } public var headers: [String: String]? { diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index ba2361f..9cf5467 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -69,9 +69,8 @@ F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; }; F88C2482295C3A4F0006098B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* StatusView.swift */; }; F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; }; + F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D41297E69FD0057491A /* StatusesView.swift */; }; F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; }; - F88FAD23295F3FC4009B20C9 /* TimelineFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD22295F3FC4009B20C9 /* TimelineFeedView.swift */; }; - F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */; }; F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD26295F400E009B20C9 /* NotificationsView.swift */; }; F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; }; F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; }; @@ -168,9 +167,8 @@ F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = ""; }; F88C2481295C3A4F0006098B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = ""; }; + F88E4D41297E69FD0057491A /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = ""; }; F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = ""; }; - F88FAD22295F3FC4009B20C9 /* TimelineFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFeedView.swift; sourceTree = ""; }; - F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederatedFeedView.swift; sourceTree = ""; }; F88FAD26295F400E009B20C9 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = ""; }; F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = ""; }; @@ -251,8 +249,6 @@ F88C2481295C3A4F0006098B /* StatusView.swift */, F88C246D295C37B80006098B /* MainView.swift */, F88FAD20295F3944009B20C9 /* HomeFeedView.swift */, - F88FAD22295F3FC4009B20C9 /* TimelineFeedView.swift */, - F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */, F88FAD26295F400E009B20C9 /* NotificationsView.swift */, F866F6A629604629002E8F88 /* SignInView.swift */, F8A93D7D2965FD89001D8331 /* UserProfileView.swift */, @@ -260,6 +256,7 @@ F897978E29684BCB00B22335 /* LoadingView.swift */, F88ABD9329687CA4004EF61E /* ComposeView.swift */, F89A46DB296EAACE0062125F /* SettingsView.swift */, + F88E4D41297E69FD0057491A /* StatusesView.swift */, ); path = Views; sourceTree = ""; @@ -584,7 +581,6 @@ F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */, F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */, F85DBF93296760790069BF89 /* CacheAvatarService.swift in Sources */, - F88FAD23295F3FC4009B20C9 /* TimelineFeedView.swift in Sources */, F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */, F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */, F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */, @@ -601,6 +597,7 @@ F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */, F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */, F89D6C4429718092001DA3D4 /* AccentsSection.swift in Sources */, + F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */, F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */, F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */, F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */, @@ -644,7 +641,6 @@ F88C246C295C37B80006098B /* VernissageApp.swift in Sources */, F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */, F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */, - F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */, F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */, F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */, F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */, diff --git a/Vernissage/Services/AccountService.swift b/Vernissage/Services/AccountService.swift index f5a6b86..c2a0395 100644 --- a/Vernissage/Services/AccountService.swift +++ b/Vernissage/Services/AccountService.swift @@ -122,4 +122,30 @@ public class AccountService { let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) return try await client.following(for: accountId, page: page) } + + public func favourites(accountData: AccountData?, + maxId: String? = nil, + sinceId: String? = nil, + minId: String? = nil, + limit: Int = 40) async throws -> [Status] { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return [] + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.favourites(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit) + } + + public func bookmarks(accountData: AccountData?, + maxId: String? = nil, + sinceId: String? = nil, + minId: String? = nil, + limit: Int = 40) async throws -> [Status] { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return [] + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.bookmarks(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit) + } } diff --git a/Vernissage/Views/FederatedFeedView.swift b/Vernissage/Views/FederatedFeedView.swift deleted file mode 100644 index 02303d2..0000000 --- a/Vernissage/Views/FederatedFeedView.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// https://mczachurski.dev -// Copyright © 2022 Marcin Czachurski and the repository contributors. -// Licensed under the MIT License. -// - -import SwiftUI - -struct FederatedFeedView: View { - var body: some View { - Text("Federated feed") - } -} diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 5db87aa..c8b9655 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -67,10 +67,10 @@ struct MainView: View { HomeFeedView(accountId: applicationState.accountData?.id ?? String.empty()) .id(applicationState.accountData?.id ?? String.empty()) case .local: - TimelineFeedView(accountId: applicationState.accountData?.id ?? String.empty(), isLocalOnly: true) + StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .local) .id(applicationState.accountData?.id ?? String.empty()) case .federated: - TimelineFeedView(accountId: applicationState.accountData?.id ?? String.empty(), isLocalOnly: false) + StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .federated) .id(applicationState.accountData?.id ?? String.empty()) case .profile: if let accountData = self.applicationState.accountData { diff --git a/Vernissage/Views/TimelineFeedView.swift b/Vernissage/Views/StatusesView.swift similarity index 71% rename from Vernissage/Views/TimelineFeedView.swift rename to Vernissage/Views/StatusesView.swift index b921f13..e1650a6 100644 --- a/Vernissage/Views/TimelineFeedView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -5,11 +5,19 @@ // import SwiftUI +import MastodonKit -struct TimelineFeedView: View { +struct StatusesView: View { + public enum ListType { + case local + case federated + case favourites + case bookmarks + } + @EnvironmentObject private var applicationState: ApplicationState @State public var accountId: String - @State public var isLocalOnly: Bool + @State public var listType: ListType @State private var allItemsLoaded = false @State private var firstLoadFinished = false @@ -53,6 +61,7 @@ struct TimelineFeedView: View { } } } + .navigationBarTitle(self.getTitle()) .overlay(alignment: .center) { if firstLoadFinished == false { LoadingIndicator() @@ -88,11 +97,7 @@ struct TimelineFeedView: View { return } - let statuses = try await PublicTimelineService.shared.getStatuses( - accountData: self.applicationState.accountData, - local: isLocalOnly, - remote: !isLocalOnly, - limit: self.defaultLimit) + let statuses = try await self.loadFromApi() var inPlaceStatuses: [StatusViewModel] = [] for item in statuses { @@ -109,12 +114,7 @@ struct TimelineFeedView: View { private func loadMoreStatuses() async throws { if let lastStatusId = self.statusViewModels.last?.id { - let previousStatuses = try await PublicTimelineService.shared.getStatuses( - accountData: self.applicationState.accountData, - local: isLocalOnly, - remote: !isLocalOnly, - maxId: lastStatusId, - limit: self.defaultLimit) + let previousStatuses = try await self.loadFromApi(maxId: lastStatusId) if previousStatuses.count < self.defaultLimit { self.allItemsLoaded = true @@ -131,12 +131,7 @@ struct TimelineFeedView: View { private func loadTopStatuses() async throws { if let firstStatusId = self.statusViewModels.first?.id { - let newestStatuses = try await PublicTimelineService.shared.getStatuses( - accountData: self.applicationState.accountData, - local: isLocalOnly, - remote: !isLocalOnly, - sinceId: firstStatusId, - limit: self.defaultLimit) + let newestStatuses = try await self.loadFromApi(sinceId: firstStatusId) var inPlaceStatuses: [StatusViewModel] = [] for item in newestStatuses { @@ -146,4 +141,54 @@ struct TimelineFeedView: View { self.statusViewModels.insert(contentsOf: inPlaceStatuses, at: 0) } } + + private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] { + switch self.listType { + case .local: + return try await PublicTimelineService.shared.getStatuses( + accountData: self.applicationState.accountData, + local: true, + remote: false, + maxId: maxId, + sinceId: sinceId, + minId: minId, + limit: self.defaultLimit) + case .federated: + return try await PublicTimelineService.shared.getStatuses( + accountData: self.applicationState.accountData, + local: false, + remote: true, + maxId: maxId, + sinceId: sinceId, + minId: minId, + limit: self.defaultLimit) + case .favourites: + return try await AccountService.shared.favourites( + accountData: self.applicationState.accountData, + maxId: maxId, + sinceId: sinceId, + minId: minId, + limit: self.defaultLimit) + case .bookmarks: + return try await AccountService.shared.bookmarks( + accountData: self.applicationState.accountData, + maxId: maxId, + sinceId: sinceId, + minId: minId, + limit: self.defaultLimit) + } + } + + private func getTitle() -> String { + switch self.listType { + case .local: + return "Local" + case .federated: + return "Federeted" + case .favourites: + return "Favourites" + case .bookmarks: + return "Bookmarks" + } + } } diff --git a/Vernissage/Widgets/UserProfile/UserProfileHeader.swift b/Vernissage/Widgets/UserProfile/UserProfileHeader.swift index 9d1bf1f..3b7e428 100644 --- a/Vernissage/Widgets/UserProfile/UserProfileHeader.swift +++ b/Vernissage/Widgets/UserProfile/UserProfileHeader.swift @@ -162,19 +162,15 @@ struct UserProfileHeader: View { Divider() } - Button { - Task { - // await onMuteAccount() - } - } label: { + NavigationLink(destination: StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .favourites) + .environmentObject(applicationState) + ) { Label("Favourites", systemImage: "hand.thumbsup") } - Button { - Task { - // await onMuteAccount() - } - } label: { + NavigationLink(destination: StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .bookmarks) + .environmentObject(applicationState) + ) { Label("Bookmarks", systemImage: "bookmark") } }, label: {