From 9dcbd5336b89637dbb7732c515abcb309a3cddf3 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Mon, 23 Jan 2023 11:42:28 +0100 Subject: [PATCH] Add list with trends. --- .../Sources/MastodonKit/Entities/Types.swift | 1 + .../MastodonKit/MastodonClient+Trends.swift | 20 ++++ .../MastodonKit/Targets/PixelfedTrends.swift | 72 ++++++++++++ .../Sources/MastodonKit/Targets/Trends.swift | 76 ++++++++++++ Vernissage.xcodeproj/project.pbxproj | 20 +++- .../Status+MediaAttachmentType.swift | 18 +++ Vernissage/Services/HomeTimelineService.swift | 3 +- ...vice.swift => PublicTimelineService.swift} | 0 Vernissage/Services/TrendsService.swift | 23 ++++ Vernissage/Views/MainView.swift | 16 ++- Vernissage/Views/NotificationsView.swift | 4 +- Vernissage/Views/SettingsView.swift | 4 +- Vernissage/Views/StatusesView.swift | 6 +- Vernissage/Views/TrendStatusesView.swift | 108 ++++++++++++++++++ Vernissage/Widgets/InteractionRow.swift | 4 +- 15 files changed, 361 insertions(+), 14 deletions(-) create mode 100644 MastodonKit/Sources/MastodonKit/MastodonClient+Trends.swift create mode 100644 MastodonKit/Sources/MastodonKit/Targets/PixelfedTrends.swift create mode 100644 MastodonKit/Sources/MastodonKit/Targets/Trends.swift create mode 100644 Vernissage/Extensions/Status+MediaAttachmentType.swift rename Vernissage/Services/{LocalFeedService.swift => PublicTimelineService.swift} (100%) create mode 100644 Vernissage/Services/TrendsService.swift create mode 100644 Vernissage/Views/TrendStatusesView.swift diff --git a/MastodonKit/Sources/MastodonKit/Entities/Types.swift b/MastodonKit/Sources/MastodonKit/Entities/Types.swift index 9336ed5..7893321 100644 --- a/MastodonKit/Sources/MastodonKit/Entities/Types.swift +++ b/MastodonKit/Sources/MastodonKit/Entities/Types.swift @@ -20,6 +20,7 @@ public typealias MaxId = EntityId public typealias MinId = EntityId public typealias Limit = Int public typealias Page = Int +public typealias Offset = Int public typealias Scope = String public typealias Scopes = [Scope] diff --git a/MastodonKit/Sources/MastodonKit/MastodonClient+Trends.swift b/MastodonKit/Sources/MastodonKit/MastodonClient+Trends.swift new file mode 100644 index 0000000..b38cb37 --- /dev/null +++ b/MastodonKit/Sources/MastodonKit/MastodonClient+Trends.swift @@ -0,0 +1,20 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +public extension MastodonClientAuthenticated { + + func statusesTrends(range: Mastodon.PixelfedTrends.TrendRange) async throws -> [Status] { + let request = try Self.request( + for: baseURL, + target: Mastodon.PixelfedTrends.statuses(range), + withBearerToken: token + ) + + return try await downloadJson([Status].self, request: request) + } +} diff --git a/MastodonKit/Sources/MastodonKit/Targets/PixelfedTrends.swift b/MastodonKit/Sources/MastodonKit/Targets/PixelfedTrends.swift new file mode 100644 index 0000000..cc9ffb9 --- /dev/null +++ b/MastodonKit/Sources/MastodonKit/Targets/PixelfedTrends.swift @@ -0,0 +1,72 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +extension Mastodon { + public enum PixelfedTrends { + case statuses(TrendRange?) + } +} + +extension Mastodon.PixelfedTrends: TargetType { + public enum TrendRange: String { + case daily = "daily" + case monthly = "monthly" + case yearly = "yearly" + } + + fileprivate var apiPath: String { return "/api/pixelfed/v2/discover/posts/trending" } + + /// The path to be appended to `baseURL` to form the full `URL`. + public var path: String { + switch self { + case .statuses(_): + return "\(apiPath)" + } + } + + /// The HTTP method used in the request. + public var method: Method { + switch self { + case .statuses: + return .get + } + } + + /// The parameters to be incoded in the request. + public var queryItems: [(String, String)]? { + var params: [(String, String)] = [] + + var trendRange: TrendRange? = nil + + switch self { + case .statuses(let _trendRange): + trendRange = _trendRange + } + + switch trendRange { + case .daily: + params.append(("range", "daily")) + case .monthly: + params.append(("range", "monthly")) + case .yearly: + params.append(("range", "yearly")) + case .none: + params.append(("range", "daily")) + } + + return params + } + + public var headers: [String: String]? { + [:].contentTypeApplicationJson + } + + public var httpBody: Data? { + nil + } +} diff --git a/MastodonKit/Sources/MastodonKit/Targets/Trends.swift b/MastodonKit/Sources/MastodonKit/Targets/Trends.swift new file mode 100644 index 0000000..38b85b7 --- /dev/null +++ b/MastodonKit/Sources/MastodonKit/Targets/Trends.swift @@ -0,0 +1,76 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +extension Mastodon { + public enum Trends { + case tags(Offset?, Limit?) + case statuses(Offset?, Limit?) + case links(Offset?, Limit?) + } +} + +extension Mastodon.Trends: TargetType { + fileprivate var apiPath: String { return "/api/v1/trends" } + + /// The path to be appended to `baseURL` to form the full `URL`. + public var path: String { + switch self { + case .tags(_, _): + return "\(apiPath)/tags" + case .statuses(_, _): + return "\(apiPath)/statuses" + case .links(_, _): + return "\(apiPath)/links" + } + } + + /// The HTTP method used in the request. + public var method: Method { + switch self { + case .tags, .statuses, .links: + return .get + } + } + + /// The parameters to be incoded in the request. + public var queryItems: [(String, String)]? { + var params: [(String, String)] = [] + + var offset: Offset? = nil + var limit: Limit? = nil + + switch self { + case .tags(let _offset, let _limit): + offset = _offset + limit = _limit + case .statuses(let _offset, let _limit): + offset = _offset + limit = _limit + case .links(let _offset, let _limit): + offset = _offset + limit = _limit + } + + if let offset { + params.append(("offset", "\(offset)")) + } + if let limit { + params.append(("limit", "\(limit)")) + } + + return params + } + + public var headers: [String: String]? { + [:].contentTypeApplicationJson + } + + public var httpBody: Data? { + nil + } +} diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 9cf5467..4f6dc04 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F802884E297AEED5000BDD51 /* DatabaseError.swift */; }; F80864112975537F009F035C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80864102975537F009F035C /* NotificationService.swift */; }; F808641429756666009F035C /* NotificationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808641329756666009F035C /* NotificationRow.swift */; }; - F8163776297C3E3D00E6E04A /* LocalFeedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8163775297C3E3D00E6E04A /* LocalFeedService.swift */; }; + F8163776297C3E3D00E6E04A /* PublicTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8163775297C3E3D00E6E04A /* PublicTimelineService.swift */; }; F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DCE2966B600001D9973 /* ImageRowAsync.swift */; }; F8210DD52966BB7E001D9973 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD42966BB7E001D9973 /* Nuke */; }; F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD62966BB7E001D9973 /* NukeExtensions */; }; @@ -70,6 +70,9 @@ 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 */; }; + F88E4D44297E82EB0057491A /* Status+MediaAttachmentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */; }; + F88E4D46297E89DF0057491A /* TrendsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D45297E89DF0057491A /* TrendsService.swift */; }; + F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D47297E90CD0057491A /* TrendStatusesView.swift */; }; F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.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 */; }; @@ -114,7 +117,7 @@ F802884E297AEED5000BDD51 /* DatabaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = ""; }; F80864102975537F009F035C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; F808641329756666009F035C /* NotificationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRow.swift; sourceTree = ""; }; - F8163775297C3E3D00E6E04A /* LocalFeedService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedService.swift; sourceTree = ""; }; + F8163775297C3E3D00E6E04A /* PublicTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineService.swift; sourceTree = ""; }; F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = ""; }; F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = ""; }; F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = ""; }; @@ -168,6 +171,9 @@ 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 = ""; }; + F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+MediaAttachmentType.swift"; sourceTree = ""; }; + F88E4D45297E89DF0057491A /* TrendsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsService.swift; sourceTree = ""; }; + F88E4D47297E90CD0057491A /* TrendStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendStatusesView.swift; sourceTree = ""; }; F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.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 = ""; }; @@ -257,6 +263,7 @@ F88ABD9329687CA4004EF61E /* ComposeView.swift */, F89A46DB296EAACE0062125F /* SettingsView.swift */, F88E4D41297E69FD0057491A /* StatusesView.swift */, + F88E4D47297E90CD0057491A /* TrendStatusesView.swift */, ); path = Views; sourceTree = ""; @@ -273,6 +280,7 @@ F8C14393296AF21B001FE31D /* Double+Round.swift */, F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */, F8996DEA2971D29D0043EEC6 /* View+Transition.swift */, + F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */, ); path = Extensions; sourceTree = ""; @@ -427,7 +435,8 @@ F85E131F297409CD006A051D /* ErrorsService.swift */, F80864102975537F009F035C /* NotificationService.swift */, F886F256297859E300879356 /* CacheImageService.swift */, - F8163775297C3E3D00E6E04A /* LocalFeedService.swift */, + F8163775297C3E3D00E6E04A /* PublicTimelineService.swift */, + F88E4D45297E89DF0057491A /* TrendsService.swift */, ); path = Services; sourceTree = ""; @@ -571,11 +580,12 @@ files = ( F85D497729640A5200751DF7 /* ImageRow.swift in Sources */, F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */, + F88E4D44297E82EB0057491A /* Status+MediaAttachmentType.swift in Sources */, F89D6C4229717FDC001DA3D4 /* AccountsSection.swift in Sources */, F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */, - F8163776297C3E3D00E6E04A /* LocalFeedService.swift in Sources */, + F8163776297C3E3D00E6E04A /* PublicTimelineService.swift in Sources */, F886F257297859E300879356 /* CacheImageService.swift in Sources */, F8984E4D296B648000A2610F /* UIImage+Blurhash.swift in Sources */, F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */, @@ -608,6 +618,7 @@ F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */, F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */, F8B1E6512973FB7E00EE0D10 /* ToastrService.swift in Sources */, + F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */, F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */, F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */, @@ -634,6 +645,7 @@ F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */, F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */, F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */, + F88E4D46297E89DF0057491A /* TrendsService.swift in Sources */, F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */, F866F6A729604629002E8F88 /* SignInView.swift in Sources */, F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */, diff --git a/Vernissage/Extensions/Status+MediaAttachmentType.swift b/Vernissage/Extensions/Status+MediaAttachmentType.swift new file mode 100644 index 0000000..92c3568 --- /dev/null +++ b/Vernissage/Extensions/Status+MediaAttachmentType.swift @@ -0,0 +1,18 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonKit + +extension [Status] { + func getStatusesWithImagesOnly() -> [Status] { + return self.filter { status in + status.mediaAttachments.contains { mediaAttachment in + mediaAttachment.type == .image + } + } + } +} diff --git a/Vernissage/Services/HomeTimelineService.swift b/Vernissage/Services/HomeTimelineService.swift index a29a396..dd45e71 100644 --- a/Vernissage/Services/HomeTimelineService.swift +++ b/Vernissage/Services/HomeTimelineService.swift @@ -189,7 +189,8 @@ public class HomeTimelineService { public func fetchAllImages(statuses: [Status]) async -> Dictionary { var attachmentUrls: Dictionary = [:] - statuses.forEach { status in + let statusesWithImages = statuses.getStatusesWithImagesOnly() + statusesWithImages.forEach { status in status.mediaAttachments.forEach { attachment in attachmentUrls[attachment.id] = attachment.url } diff --git a/Vernissage/Services/LocalFeedService.swift b/Vernissage/Services/PublicTimelineService.swift similarity index 100% rename from Vernissage/Services/LocalFeedService.swift rename to Vernissage/Services/PublicTimelineService.swift diff --git a/Vernissage/Services/TrendsService.swift b/Vernissage/Services/TrendsService.swift new file mode 100644 index 0000000..6c4fb52 --- /dev/null +++ b/Vernissage/Services/TrendsService.swift @@ -0,0 +1,23 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonKit + +public class TrendsService { + public static let shared = TrendsService() + private init() { } + + public func statuses(accountData: AccountData?, + range: Mastodon.PixelfedTrends.TrendRange) 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.statusesTrends(range: range) + } +} diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 4b89bc1..07bd54b 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -34,7 +34,7 @@ struct MainView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults private enum ViewMode { - case home, local, federated, profile, notifications + case home, local, federated, profile, notifications, trending } var body: some View { @@ -66,6 +66,9 @@ struct MainView: View { case .home: HomeFeedView(accountId: applicationState.accountData?.id ?? String.empty()) .id(applicationState.accountData?.id ?? String.empty()) + case .trending: + TrendStatusesView(accountId: applicationState.accountData?.id ?? String.empty()) + .id(applicationState.accountData?.id ?? String.empty()) case .local: StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .local) .id(applicationState.accountData?.id ?? String.empty()) @@ -99,6 +102,15 @@ struct MainView: View { Image(systemName: "house") } } + + Button { + viewMode = .trending + } label: { + HStack { + Text(self.getViewTitle(viewMode: .trending)) + Image(systemName: "chart.line.uptrend.xyaxis") + } + } Button { viewMode = .local @@ -208,6 +220,8 @@ struct MainView: View { switch viewMode { case .home: return "Home" + case .trending: + return "Trending" case .local: return "Local" case .federated: diff --git a/Vernissage/Views/NotificationsView.swift b/Vernissage/Views/NotificationsView.swift index 837cd75..0823764 100644 --- a/Vernissage/Views/NotificationsView.swift +++ b/Vernissage/Views/NotificationsView.swift @@ -69,10 +69,10 @@ struct NotificationsView: View { } else { if self.notifications.isEmpty { VStack { - Image(systemName: "person.3.sequence") + Image(systemName: "bell") .font(.largeTitle) .padding(.bottom, 4) - Text("Unfortunately, there is no one here.") + Text("Unfortunately, there is nothing here.") .font(.title3) }.foregroundColor(.lightGrayColor) } diff --git a/Vernissage/Views/SettingsView.swift b/Vernissage/Views/SettingsView.swift index 9e826fa..8bef9ce 100644 --- a/Vernissage/Views/SettingsView.swift +++ b/Vernissage/Views/SettingsView.swift @@ -13,6 +13,7 @@ struct SettingsView: View { @State private var theme: ColorScheme? @State private var appVersion: String? + @State private var appBundleVersion: String? var onTintChange: ((TintColor) -> Void)? var onThemeChange: ((Theme) -> Void)? @@ -45,7 +46,7 @@ struct SettingsView: View { HStack { Text("Version") Spacer() - Text(appVersion ?? String.empty()) + Text("\(appVersion ?? String.empty()) (\(appBundleVersion ?? String.empty()))") .foregroundColor(.accentColor) } } @@ -60,6 +61,7 @@ struct SettingsView: View { } .task { self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + self.appBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in self.theme = applicationState.theme.colorScheme() ?? self.getSystemColorScheme() diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index e1650a6..3d5dae0 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -100,7 +100,7 @@ struct StatusesView: View { let statuses = try await self.loadFromApi() var inPlaceStatuses: [StatusViewModel] = [] - for item in statuses { + for item in statuses.getStatusesWithImagesOnly() { inPlaceStatuses.append(StatusViewModel(status: item)) } @@ -121,7 +121,7 @@ struct StatusesView: View { } var inPlaceStatuses: [StatusViewModel] = [] - for item in previousStatuses { + for item in previousStatuses.getStatusesWithImagesOnly() { inPlaceStatuses.append(StatusViewModel(status: item)) } @@ -134,7 +134,7 @@ struct StatusesView: View { let newestStatuses = try await self.loadFromApi(sinceId: firstStatusId) var inPlaceStatuses: [StatusViewModel] = [] - for item in newestStatuses { + for item in newestStatuses.getStatusesWithImagesOnly() { inPlaceStatuses.append(StatusViewModel(status: item)) } diff --git a/Vernissage/Views/TrendStatusesView.swift b/Vernissage/Views/TrendStatusesView.swift new file mode 100644 index 0000000..e42c17f --- /dev/null +++ b/Vernissage/Views/TrendStatusesView.swift @@ -0,0 +1,108 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonKit + +struct TrendStatusesView: View { + + @EnvironmentObject private var applicationState: ApplicationState + @State public var accountId: String + + @State private var firstLoadFinished = false + @State private var tabSelectedValue: Mastodon.PixelfedTrends.TrendRange = .daily + + @State private var statusViewModels: [StatusViewModel] = [] + + var body: some View { + ScrollView { + + Picker(selection: $tabSelectedValue, label: Text("")) { + Text("Daily").tag(Mastodon.PixelfedTrends.TrendRange.daily) + Text("Monthly").tag(Mastodon.PixelfedTrends.TrendRange.monthly) + Text("Yearly").tag(Mastodon.PixelfedTrends.TrendRange.yearly) + + } + .padding() + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: tabSelectedValue) { _ in + Task { + do { + self.firstLoadFinished = false; + self.statusViewModels = [] + try await self.loadStatuses() + } catch { + ErrorService.shared.handle(error, message: "Loading statuses failed.", showToastr: !Task.isCancelled) + } + } + } + + VStack(alignment: .center) { + if firstLoadFinished == true { + ForEach(self.statusViewModels, id: \.uniqueId) { item in + NavigationLink(destination: StatusView(statusId: item.id, + imageBlurhash: item.mediaAttachments.first?.blurhash, + imageWidth: item.getImageWidth(), + imageHeight: item.getImageHeight()) + .environmentObject(applicationState)) { + ImageRowAsync(statusViewModel: item) + } + .buttonStyle(EmptyButtonStyle()) + + } + } + } + } + .navigationBarTitle("Trends") + .overlay(alignment: .center) { + if firstLoadFinished == false { + LoadingIndicator() + } else { + if self.statusViewModels.isEmpty { + VStack { + Image(systemName: "photo.on.rectangle.angled") + .font(.largeTitle) + .padding(.bottom, 4) + Text("Unfortunately, there are no photos here.") + .font(.title3) + }.foregroundColor(.lightGrayColor) + } + } + } + .task { + do { + try await self.loadStatuses() + } catch { + ErrorService.shared.handle(error, message: "Loading statuses failed.", showToastr: !Task.isCancelled) + } + }.refreshable { + do { + try await self.loadStatuses() + } catch { + ErrorService.shared.handle(error, message: "Loading statuses failed.", showToastr: !Task.isCancelled) + } + } + } + + private func loadStatuses() async throws { + guard firstLoadFinished == false else { + return + } + + let statuses = try await TrendsService.shared.statuses( + accountData: self.applicationState.accountData, + range: tabSelectedValue) + + var inPlaceStatuses: [StatusViewModel] = [] + + for item in statuses.getStatusesWithImagesOnly() { + inPlaceStatuses.append(StatusViewModel(status: item)) + } + + self.statusViewModels = inPlaceStatuses + self.firstLoadFinished = true + } +} diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index 701b2b6..1f8b7f9 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -122,14 +122,14 @@ struct InteractionRow: View { ) { Label("Favourited by", systemImage: "hand.thumbsup") } - + if let url = statusViewModel.url { Divider() Link(destination: url) { Label("Open in browser", systemImage: "safari") } - + ShareLink(item: url) { Label("Share post", systemImage: "square.and.arrow.up") }