diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift b/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift index 2801993..158891f 100644 --- a/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift +++ b/PixelfedKit/Sources/PixelfedKit/Entities/Account.swift @@ -85,6 +85,9 @@ public struct Account: Codable { /// NULLABLE String (ISO 8601 Date), or null if no statuses public let lastStatusAt: String? + /// Recent photos send by the user. + public let recentPosts: [Status]? + private enum CodingKeys: String, CodingKey { case id case username @@ -111,6 +114,7 @@ public struct Account: Codable { case suspended case limited case lastStatusAt = "last_status_at" + case recentPosts = "recent_posts" } } diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/File.swift b/PixelfedKit/Sources/PixelfedKit/Entities/File.swift new file mode 100644 index 0000000..3599448 --- /dev/null +++ b/PixelfedKit/Sources/PixelfedKit/Entities/File.swift @@ -0,0 +1,26 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +/// Information about trending hashtag. +public struct TagTrend: Codable { + + /// Id number of tag. + public let id: Int + + /// The value of the hashtag. + public let name: String + + /// The value of the hashtag after the # sign. + public let hashtag: String + + /// A link to the hashtag on the instance. + public let url: URL? + + /// Total uses of hashtag. + public let total: Int +} diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/Tag.swift b/PixelfedKit/Sources/PixelfedKit/Entities/Tag.swift index 2e8088f..14f26f6 100644 --- a/PixelfedKit/Sources/PixelfedKit/Entities/Tag.swift +++ b/PixelfedKit/Sources/PixelfedKit/Entities/Tag.swift @@ -31,6 +31,6 @@ public struct TagHistory: Codable { /// The counted usage of the tag within that day. public let uses: String - /// he total of accounts using the tag within that day (cast from an integer). + /// The total of accounts using the tag within that day (cast from an integer). public let accounts: String } diff --git a/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Trends.swift b/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Trends.swift index e8cf60f..c984cfe 100644 --- a/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Trends.swift +++ b/PixelfedKit/Sources/PixelfedKit/PixelfedClient+Trends.swift @@ -17,4 +17,24 @@ public extension PixelfedClientAuthenticated { return try await downloadJson([Status].self, request: request) } + + func tagsTrends() async throws -> [TagTrend] { + let request = try Self.request( + for: baseURL, + target: Pixelfed.Trends.tags(nil, nil, nil), + withBearerToken: token + ) + + return try await downloadJson([TagTrend].self, request: request) + } + + func accountsTrends() async throws -> [Account] { + let request = try Self.request( + for: baseURL, + target: Pixelfed.Trends.accounts(nil, nil, nil), + withBearerToken: token + ) + + return try await downloadJson([Account].self, request: request) + } } diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 4e7eca6..e746d3a 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -81,6 +81,10 @@ F8864CEF29ACE90B0020C534 /* UIFont+Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8864CEE29ACE90B0020C534 /* UIFont+Font.swift */; }; F8864CF129ACFFB80020C534 /* View+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8864CF029ACFFB80020C534 /* View+Keyboard.swift */; }; F886F257297859E300879356 /* CacheImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F886F256297859E300879356 /* CacheImageService.swift */; }; + F88AB05329B3613900345EDE /* PhotoUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05229B3613900345EDE /* PhotoUrl.swift */; }; + F88AB05529B3626300345EDE /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05429B3626300345EDE /* ImageGrid.swift */; }; + F88AB05829B36B8200345EDE /* TrendingAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05729B36B8200345EDE /* TrendingAccountsView.swift */; }; + F88AB05D29B371B500345EDE /* AccountImagesGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05B29B371B500345EDE /* AccountImagesGridView.swift */; }; F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; }; F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; }; F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; }; @@ -127,6 +131,8 @@ F89D6C4A297196FF001DA3D4 /* ImagesViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C49297196FF001DA3D4 /* ImagesViewer.swift */; }; F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; F8AD061329A565620042F111 /* String+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AD061229A565620042F111 /* String+Random.swift */; }; + F8AFF7C129B259150087D083 /* TrendingTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C029B259150087D083 /* TrendingTagsView.swift */; }; + F8AFF7C429B25EF40087D083 /* TagImagesGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C329B25EF40087D083 /* TagImagesGridView.swift */; }; F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885D29942E31002AB40A /* ThirdPartyView.swift */; }; F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; }; F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; }; @@ -227,6 +233,10 @@ F8864CEE29ACE90B0020C534 /* UIFont+Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Font.swift"; sourceTree = ""; }; F8864CF029ACFFB80020C534 /* View+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Keyboard.swift"; sourceTree = ""; }; F886F256297859E300879356 /* CacheImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheImageService.swift; sourceTree = ""; }; + F88AB05229B3613900345EDE /* PhotoUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoUrl.swift; sourceTree = ""; }; + F88AB05429B3626300345EDE /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = ""; }; + F88AB05729B36B8200345EDE /* TrendingAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingAccountsView.swift; sourceTree = ""; }; + F88AB05B29B371B500345EDE /* AccountImagesGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountImagesGridView.swift; sourceTree = ""; }; F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; F88ABD9529687D4D004EF61E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -275,6 +285,8 @@ F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = ""; }; F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; F8AD061229A565620042F111 /* String+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Random.swift"; sourceTree = ""; }; + F8AFF7C029B259150087D083 /* TrendingTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingTagsView.swift; sourceTree = ""; }; + F8AFF7C329B25EF40087D083 /* TagImagesGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagImagesGridView.swift; sourceTree = ""; }; F8B0885D29942E31002AB40A /* ThirdPartyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyView.swift; sourceTree = ""; }; F8B0885F29943498002AB40A /* OtherSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherSectionView.swift; sourceTree = ""; }; F8B08861299435C9002AB40A /* SupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportView.swift; sourceTree = ""; }; @@ -362,6 +374,8 @@ F8341F93295C63E2009C8EE6 /* Views */ = { isa = PBXGroup; children = ( + F88AB05629B36B7700345EDE /* TrendingAccountsView */, + F8AFF7BF29B258FC0087D083 /* TrendingTagsView */, F89D6C4029717FC0001DA3D4 /* SettingsView */, F89D6C4729718822001DA3D4 /* UserProfileView */, F808641229756583009F035C /* NotificationsView */, @@ -418,6 +432,7 @@ F8FA9918299FA35A007AB130 /* PhotoAttachment.swift */, F89AC00429A1F9B500F4159F /* AppMetadata.swift */, F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */, + F88AB05229B3613900345EDE /* PhotoUrl.swift */, ); path = Models; sourceTree = ""; @@ -461,6 +476,7 @@ F85D497C29640D5900751DF7 /* InteractionRow.swift */, F897978729681B9C00B22335 /* UserAvatar.swift */, F89797892968314A00B22335 /* LoadingIndicator.swift */, + F88AB05429B3626300345EDE /* ImageGrid.swift */, F86B7217296C27C100EE59EC /* ActionButton.swift */, F86B721D296C458700EE59EC /* BlurredImage.swift */, F86B7222296C4BF500EE59EC /* ContentWarning.swift */, @@ -549,6 +565,23 @@ path = TextView; sourceTree = ""; }; + F88AB05629B36B7700345EDE /* TrendingAccountsView */ = { + isa = PBXGroup; + children = ( + F88AB05929B3719300345EDE /* Subviews */, + F88AB05729B36B8200345EDE /* TrendingAccountsView.swift */, + ); + path = TrendingAccountsView; + sourceTree = ""; + }; + F88AB05929B3719300345EDE /* Subviews */ = { + isa = PBXGroup; + children = ( + F88AB05B29B371B500345EDE /* AccountImagesGridView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; F88ABD9029686F00004EF61E /* Cache */ = { isa = PBXGroup; children = ( @@ -674,6 +707,23 @@ path = StatusView; sourceTree = ""; }; + F8AFF7BF29B258FC0087D083 /* TrendingTagsView */ = { + isa = PBXGroup; + children = ( + F8AFF7C229B25ED60087D083 /* Subviews */, + F8AFF7C029B259150087D083 /* TrendingTagsView.swift */, + ); + path = TrendingTagsView; + sourceTree = ""; + }; + F8AFF7C229B25ED60087D083 /* Subviews */ = { + isa = PBXGroup; + children = ( + F8AFF7C329B25EF40087D083 /* TagImagesGridView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; F8B9B354298D4B88009CC69C /* EnvironmentObjects */ = { isa = PBXGroup; children = ( @@ -799,6 +849,7 @@ files = ( F85D497729640A5200751DF7 /* ImageRow.swift in Sources */, F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */, + F88AB05529B3626300345EDE /* ImageGrid.swift in Sources */, F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */, F88E4D44297E82EB0057491A /* Status+MediaAttachmentType.swift in Sources */, F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */, @@ -866,17 +917,20 @@ F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */, F88C246E295C37B80006098B /* MainView.swift in Sources */, F89AC00729A208CC00F4159F /* PlaceSelectorView.swift in Sources */, + F8AFF7C429B25EF40087D083 /* TagImagesGridView.swift in Sources */, F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */, F8B9B349298D4AA2009CC69C /* Client+Timeline.swift in Sources */, F8FA9919299FA35A007AB130 /* PhotoAttachment.swift in Sources */, F8B9B34B298D4ACE009CC69C /* Client+Tags.swift in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, F8AD061329A565620042F111 /* String+Random.swift in Sources */, + F8AFF7C129B259150087D083 /* TrendingTagsView.swift in Sources */, F898DE7229728CB2004B4A6A /* CommentModel.swift in Sources */, F89A46DE296EABA20062125F /* StatusPlaceholderView.swift in Sources */, F88C2482295C3A4F0006098B /* StatusView.swift in Sources */, F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */, F8996DEB2971D29D0043EEC6 /* View+Transition.swift in Sources */, + F88AB05D29B371B500345EDE /* AccountImagesGridView.swift in Sources */, F876418B298AC1B80057D362 /* NoDataView.swift in Sources */, F89D6C4629718193001DA3D4 /* ThemeSectionView.swift in Sources */, F85D497F296416C800751DF7 /* CommentsSectionView.swift in Sources */, @@ -905,8 +959,10 @@ F8CEEDFA29ABAFD200DBED66 /* ImageFileTranseferable.swift in Sources */, F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */, F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */, + F88AB05829B36B8200345EDE /* TrendingAccountsView.swift in Sources */, F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */, F8B9B356298D4C1E009CC69C /* Client+Instance.swift in Sources */, + F88AB05329B3613900345EDE /* PhotoUrl.swift in Sources */, F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */, F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */, F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */, @@ -1059,7 +1115,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 42; + CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1096,7 +1152,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 42; + CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; diff --git a/Vernissage/AppRouteur.swift b/Vernissage/AppRouteur.swift index bce4f5a..373fea7 100644 --- a/Vernissage/AppRouteur.swift +++ b/Vernissage/AppRouteur.swift @@ -38,6 +38,10 @@ extension View { ThirdPartyView() case .photoEditor(let photoAttachment): PhotoEditorView(photoAttachment: photoAttachment) + case .trendingTags: + TrendingTagsView() + case .trendingAccounts: + TrendingAccountsView() } } } diff --git a/Vernissage/EnvironmentObjects/Client+Trends.swift b/Vernissage/EnvironmentObjects/Client+Trends.swift index 184d868..8ce17a3 100644 --- a/Vernissage/EnvironmentObjects/Client+Trends.swift +++ b/Vernissage/EnvironmentObjects/Client+Trends.swift @@ -13,5 +13,13 @@ extension Client { public func statuses(range: Pixelfed.Trends.TrendRange) async throws -> [Status] { return try await pixelfedClient.statusesTrends(range: range) } + + public func tags() async throws -> [TagTrend] { + return try await pixelfedClient.tagsTrends() + } + + public func accounts() async throws -> [Account] { + return try await pixelfedClient.accountsTrends() + } } } diff --git a/Vernissage/Models/PhotoUrl.swift b/Vernissage/Models/PhotoUrl.swift new file mode 100644 index 0000000..84c6a80 --- /dev/null +++ b/Vernissage/Models/PhotoUrl.swift @@ -0,0 +1,17 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation + +public class PhotoUrl: ObservableObject, Identifiable { + public var id: String + @Published public var url: URL? + @Published public var blurhash: String? + + init(id: String) { + self.id = id + } +} diff --git a/Vernissage/Services/RouterPath.swift b/Vernissage/Services/RouterPath.swift index b30af87..3107bd8 100644 --- a/Vernissage/Services/RouterPath.swift +++ b/Vernissage/Services/RouterPath.swift @@ -19,6 +19,8 @@ enum RouteurDestinations: Hashable { case signIn case thirdParty case photoEditor(photoAttachment: PhotoAttachment) + case trendingTags + case trendingAccounts } enum SheetDestinations: Identifiable { diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 1cc4d2f..c75086a 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -27,7 +27,7 @@ struct MainView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults private enum ViewMode { - case home, local, federated, profile, notifications, trending + case home, local, federated, profile, notifications, trendingPhotos, trendingTags, trendingAccounts } var body: some View { @@ -55,9 +55,15 @@ struct MainView: View { case .home: HomeFeedView(accountId: applicationState.account?.id ?? String.empty()) .id(applicationState.account?.id ?? String.empty()) - case .trending: + case .trendingPhotos: TrendStatusesView(accountId: applicationState.account?.id ?? String.empty()) .id(applicationState.account?.id ?? String.empty()) + case .trendingTags: + TrendingTagsView() + .id(applicationState.account?.id ?? String.empty()) + case .trendingAccounts: + TrendingAccountsView() + .id(applicationState.account?.id ?? String.empty()) case .local: StatusesView(listType: .local) .id(applicationState.account?.id ?? String.empty()) @@ -115,12 +121,39 @@ struct MainView: View { Divider() - Button { - HapticService.shared.fireHaptic(of: .tabSelection) - viewMode = .trending + Menu { + Button { + HapticService.shared.fireHaptic(of: .tabSelection) + viewMode = .trendingPhotos + } label: { + HStack { + Text(self.getViewTitle(viewMode: .trendingPhotos)) + Image(systemName: "photo.stack") + } + } + + Button { + HapticService.shared.fireHaptic(of: .tabSelection) + viewMode = .trendingTags + } label: { + HStack { + Text(self.getViewTitle(viewMode: .trendingTags)) + Image(systemName: "tag") + } + } + + Button { + HapticService.shared.fireHaptic(of: .tabSelection) + viewMode = .trendingAccounts + } label: { + HStack { + Text(self.getViewTitle(viewMode: .trendingAccounts)) + Image(systemName: "person.3") + } + } } label: { HStack { - Text(self.getViewTitle(viewMode: .trending)) + Text("Trending") Image(systemName: "chart.line.uptrend.xyaxis") } } @@ -209,7 +242,7 @@ struct MainView: View { @ToolbarContentBuilder private func getTrailingToolbar() -> some ToolbarContent { - if viewMode == .local || viewMode == .home || viewMode == .federated || viewMode == .trending { + if viewMode == .local || viewMode == .home || viewMode == .federated || viewMode == .trendingPhotos { ToolbarItem(placement: .navigationBarTrailing) { Button { HapticService.shared.fireHaptic(of: .buttonPress) @@ -228,8 +261,12 @@ struct MainView: View { switch viewMode { case .home: return "Home" - case .trending: - return "Trending" + case .trendingPhotos: + return "Photos" + case .trendingTags: + return "Tags" + case .trendingAccounts: + return "Accounts" case .local: return "Local" case .federated: diff --git a/Vernissage/Views/TrendingAccountsView/Subviews/AccountImagesGridView.swift b/Vernissage/Views/TrendingAccountsView/Subviews/AccountImagesGridView.swift new file mode 100644 index 0000000..df11866 --- /dev/null +++ b/Vernissage/Views/TrendingAccountsView/Subviews/AccountImagesGridView.swift @@ -0,0 +1,68 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import PixelfedKit +import NukeUI + +struct AccountImagesGridView: View { + @EnvironmentObject var client: Client + @EnvironmentObject var routerPath: RouterPath + + private let account: Account + private var photoUrls: [PhotoUrl] + + init(account: Account) { + self.account = account + self.photoUrls = [ + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString) + ] + } + + var body: some View { + LazyVGrid(columns: [GridItem(.adaptive(minimum:140))]) { + ForEach(self.photoUrls) { photoUrl in + ImageGrid(photoUrl: photoUrl) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(width: 140, height: 140) + .id(photoUrl.id) + } + + Button { + self.routerPath.navigate(to: .userProfile(accountId: account.id, + accountDisplayName: account.displayNameWithoutEmojis, + accountUserName: account.acct)) + } label: { + Text("more...") + } + } + .onFirstAppear { + self.loadData() + } + } + + private func loadData() { + if let statuses = self.account.recentPosts { + let statusesWithImages = statuses.getStatusesWithImagesOnly() + + var index = 0 + for status in statusesWithImages { + if let mediaAttachment = status.getAllImageMediaAttachments().first { + self.photoUrls[index].url = mediaAttachment.url + self.photoUrls[index].blurhash = mediaAttachment.blurhash + + index = index + 1 + } + + if index == 3 { + break; + } + } + } + } +} diff --git a/Vernissage/Views/TrendingAccountsView/TrendingAccountsView.swift b/Vernissage/Views/TrendingAccountsView/TrendingAccountsView.swift new file mode 100644 index 0000000..a4ec8d8 --- /dev/null +++ b/Vernissage/Views/TrendingAccountsView/TrendingAccountsView.swift @@ -0,0 +1,89 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import PixelfedKit +import Foundation + +struct TrendingAccountsView: View { + @EnvironmentObject var applicationState: ApplicationState + @EnvironmentObject var client: Client + @EnvironmentObject var routerPath: RouterPath + + @State private var accounts: [Account] = [] + @State private var state: ViewState = .loading + + var body: some View { + self.mainBody() + .navigationTitle("Tags") + } + + @ViewBuilder + private func mainBody() -> some View { + switch state { + case .loading: + LoadingIndicator() + .task { + await self.loadData() + } + case .loaded: + if self.accounts.isEmpty { + NoDataView(imageSystemName: "person.3.sequence", text: "Unfortunately, there is no one here.") + } else { + List { + ForEach(self.accounts, id: \.id) { account in + Section { + AccountImagesGridView(account: account) + } header: { + HStack { + UsernameRow( + accountId: account.id, + accountAvatar: account.avatar, + accountDisplayName: account.displayNameWithoutEmojis, + accountUsername: account.acct) + Spacer() + } + .padding(.horizontal, 8) + } + } + } + } + case .error(let error): + ErrorView(error: error) { + self.state = .loading + + self.accounts = [] + await self.loadData() + } + .padding() + } + } + + private func loadData() async { + do { + try await self.loadAccounts() + self.state = .loaded + } catch NetworkError.notSuccessResponse(let response) { + // TODO: This code can be removed when other Pixelfed server will support trending accounts. + if response.statusCode() == HTTPStatusCode.notFound { + self.accounts = [] + self.state = .loaded + } + } catch { + if !Task.isCancelled { + ErrorService.shared.handle(error, message: "Accounts not retrieved.", showToastr: true) + self.state = .error(error) + } else { + ErrorService.shared.handle(error, message: "Accounts not retrieved.", showToastr: false) + } + } + } + + private func loadAccounts() async throws { + let accountsFromApi = try await self.client.trends?.accounts() + self.accounts = accountsFromApi ?? [] + } +} diff --git a/Vernissage/Views/TrendingTagsView/Subviews/TagImagesGridView.swift b/Vernissage/Views/TrendingTagsView/Subviews/TagImagesGridView.swift new file mode 100644 index 0000000..4c50c7e --- /dev/null +++ b/Vernissage/Views/TrendingTagsView/Subviews/TagImagesGridView.swift @@ -0,0 +1,79 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import PixelfedKit +import NukeUI + +struct TagImagesGridView: View { + @EnvironmentObject var client: Client + @EnvironmentObject var routerPath: RouterPath + + private let hashtag: String + private let photoUrls: [PhotoUrl] + + init(hashtag: String) { + self.hashtag = hashtag + self.photoUrls = [ + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString), + PhotoUrl(id: UUID().uuidString) + ] + } + + var body: some View { + LazyVGrid(columns: [GridItem(.adaptive(minimum:80))]) { + ForEach(self.photoUrls) { photoUrl in + ImageGrid(photoUrl: photoUrl) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(width: 80, height: 80) + .id(photoUrl.id) + } + + Button { + self.routerPath.navigate(to: .tag(hashTag: hashtag)) + } label: { + Text("more...") + } + } + .onFirstAppear { + Task { + await self.loadData() + } + } + } + + private func loadData() async { + do { + let statusesFromApi = try await self.client.publicTimeline?.getTagStatuses( + tag: self.hashtag, + local: true, + remote: false, + limit: 10) ?? [] + + let statusesWithImages = statusesFromApi.getStatusesWithImagesOnly() + + var index = 0 + for status in statusesWithImages { + if let mediaAttachment = status.getAllImageMediaAttachments().first { + self.photoUrls[index].url = mediaAttachment.url + self.photoUrls[index].blurhash = mediaAttachment.blurhash + + index = index + 1 + } + + if index == 5 { + break; + } + } + + } catch { + ErrorService.shared.handle(error, message: "Loading tags failed.", showToastr: !Task.isCancelled) + } + } +} diff --git a/Vernissage/Views/TrendingTagsView/TrendingTagsView.swift b/Vernissage/Views/TrendingTagsView/TrendingTagsView.swift new file mode 100644 index 0000000..c08f7e2 --- /dev/null +++ b/Vernissage/Views/TrendingTagsView/TrendingTagsView.swift @@ -0,0 +1,74 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import PixelfedKit +import Foundation + +struct TrendingTagsView: View { + @EnvironmentObject var applicationState: ApplicationState + @EnvironmentObject var client: Client + @EnvironmentObject var routerPath: RouterPath + + @State private var tags: [TagTrend] = [] + @State private var state: ViewState = .loading + + var body: some View { + self.mainBody() + .navigationTitle("Tags") + } + + @ViewBuilder + private func mainBody() -> some View { + switch state { + case .loading: + LoadingIndicator() + .task { + await self.loadData() + } + case .loaded: + if self.tags.isEmpty { + NoDataView(imageSystemName: "person.3.sequence", text: "Unfortunately, there is no one here.") + } else { + List { + ForEach(self.tags, id: \.id) { tag in + Section(header: Text(tag.name).font(.headline)) { + TagImagesGridView(hashtag: tag.hashtag) + .id(UUID().uuidString) + } + } + } + } + case .error(let error): + ErrorView(error: error) { + self.state = .loading + + self.tags = [] + await self.loadData() + } + .padding() + } + } + + private func loadData() async { + do { + try await self.loadTags() + self.state = .loaded + } catch { + if !Task.isCancelled { + ErrorService.shared.handle(error, message: "Tags not retrieved.", showToastr: true) + self.state = .error(error) + } else { + ErrorService.shared.handle(error, message: "Tags not retrieved.", showToastr: false) + } + } + } + + private func loadTags() async throws { + let tagsFromApi = try await self.client.trends?.tags() + self.tags = tagsFromApi ?? [] + } +} diff --git a/Vernissage/Widgets/ImageGrid.swift b/Vernissage/Widgets/ImageGrid.swift new file mode 100644 index 0000000..9122651 --- /dev/null +++ b/Vernissage/Widgets/ImageGrid.swift @@ -0,0 +1,42 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import NukeUI + +struct ImageGrid: View { + @StateObject var photoUrl: PhotoUrl + + var body: some View { + if let url = photoUrl.url { + LazyImage(url: url) { state in + if let image = state.image { + image + .aspectRatio(contentMode: .fit) + } else if state.isLoading { + placeholder() + } else { + placeholder() + } + } + .priority(.high) + } else { + self.placeholder() + } + } + + @ViewBuilder + private func placeholder() -> some View { + if let imageBlurhash = photoUrl.blurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) { + Image(uiImage: uiImage) + .resizable() + } else { + Rectangle() + .fill(Color.placeholderText) + .redacted(reason: .placeholder) + } + } +}