From 5d24c4d2e8447a4ff63625ccb8a84b88db3ee23d Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 8 Jan 2024 18:22:44 +0100 Subject: [PATCH] Fix #1836 --- .../Sources/Account/AccountDetailView.swift | 79 +++---------- .../Account/AccountDetailViewModel.swift | 109 +++++++++--------- .../Sources/Network/Endpoint/Accounts.swift | 20 ++-- .../Sources/Timeline/TimelineFilter.swift | 8 +- 4 files changed, 84 insertions(+), 132 deletions(-) diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 4458e5c6..deca7bac 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -63,9 +63,15 @@ public struct AccountDetailView: View { ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs, id: \.self) { tab in - Image(systemName: tab.iconName) - .tag(tab) - .accessibilityLabel(tab.accessibilityLabel) + if tab == .boosts { + Image("Rocket") + .tag(tab) + .accessibilityLabel(tab.accessibilityLabel) + } else { + Image(systemName: tab.iconName) + .tag(tab) + .accessibilityLabel(tab.accessibilityLabel) + } } } .pickerStyle(.segmented) @@ -73,19 +79,12 @@ public struct AccountDetailView: View { .applyAccountDetailsRowStyle(theme: theme) .id("status") - switch viewModel.tabState { - case .statuses: - if viewModel.selectedTab == .statuses { - pinnedPostsView - } - StatusesListView(fetcher: viewModel, - client: client, - routerPath: routerPath) - case .followedTags: - tagsListView - case .lists: - listsListView + if viewModel.selectedTab == .statuses { + pinnedPostsView } + StatusesListView(fetcher: viewModel, + client: client, + routerPath: routerPath) } .environment(\.defaultMinListRowHeight, 1) .listStyle(.plain) @@ -236,56 +235,6 @@ public struct AccountDetailView: View { } } - private var tagsListView: some View { - Group { - ForEach(currentAccount.sortedTags) { tag in - HStack { - TagRowView(tag: tag) - Spacer() - Image(systemName: "chevron.right") - } - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif - } - }.task { - await currentAccount.fetchFollowedTags() - } - } - - private var listsListView: some View { - Group { - ForEach(currentAccount.sortedLists) { list in - NavigationLink(value: RouterDestination.list(list: list)) { - Text(list.title) - .font(.scaledHeadline) - .foregroundColor(theme.labelColor) - } - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif - .contextMenu { - Button("account.list.delete", role: .destructive) { - Task { - await currentAccount.deleteList(list: list) - } - } - } - } - Button("account.list.create") { - routerPath.presentedSheet = .listCreate - } - .tint(theme.tintColor) - .buttonStyle(.borderless) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif - } - .task { - await currentAccount.fetchLists() - } - } - @ViewBuilder private var pinnedPostsView: some View { if !viewModel.pinned.isEmpty { diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index ac4f4c57..4b147ba0 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -16,14 +16,14 @@ import SwiftUI } enum Tab: Int { - case statuses, favorites, bookmarks, followedTags, postsAndReplies, media, lists + case statuses, favorites, bookmarks, replies, boosts, media static var currentAccountTabs: [Tab] { - [.statuses, .favorites, .bookmarks, .followedTags, .lists] + [.statuses, .replies, .boosts, .favorites, .bookmarks] } static var accountTabs: [Tab] { - [.statuses, .postsAndReplies, .media] + [.statuses, .replies, .boosts, .media] } var iconName: String { @@ -31,10 +31,9 @@ import SwiftUI case .statuses: "bubble.right" case .favorites: "star" case .bookmarks: "bookmark" - case .followedTags: "tag" - case .postsAndReplies: "bubble.left.and.bubble.right" + case .replies: "bubble.left.and.bubble.right" + case .boosts: "" case .media: "photo.on.rectangle.angled" - case .lists: "list.bullet" } } @@ -43,34 +42,14 @@ import SwiftUI case .statuses: "accessibility.tabs.profile.picker.statuses" case .favorites: "accessibility.tabs.profile.picker.favorites" case .bookmarks: "accessibility.tabs.profile.picker.bookmarks" - case .followedTags: "accessibility.tabs.profile.picker.followed-tags" - case .postsAndReplies: "accessibility.tabs.profile.picker.posts-and-replies" + case .replies: "accessibility.tabs.profile.picker.posts-and-replies" + case .boosts: "accessibility.tabs.profile.picker.boosts" case .media: "accessibility.tabs.profile.picker.media" - case .lists: "accessibility.tabs.profile.picker.lists" } } } - enum TabState { - case followedTags - case statuses(statusesState: StatusesState) - case lists - } - var accountState: AccountState = .loading - var tabState: TabState = .statuses(statusesState: .loading) { - didSet { - /// Forward viewModel tabState related to statusesState to statusesState property - /// for `StatusesFetcher` conformance as we wrap StatusesState in TabState - switch tabState { - case let .statuses(statusesState): - self.statusesState = statusesState - default: - break - } - } - } - var statusesState: StatusesState = .loading var relationship: Relationship? @@ -85,7 +64,7 @@ import SwiftUI var selectedTab = Tab.statuses { didSet { switch selectedTab { - case .statuses, .postsAndReplies, .media: + case .statuses, .replies, .boosts, .media: tabTask?.cancel() tabTask = Task { await fetchNewestStatuses(pullToRefresh: false) @@ -105,6 +84,8 @@ import SwiftUI private var tabTask: Task? private(set) var statuses: [Status] = [] + + var boosts: [Status] = [] /// When coming from a URL like a mention tap in a status. init(accountId: String) { @@ -173,22 +154,28 @@ import SwiftUI func fetchNewestStatuses(pullToRefresh: Bool) async { guard let client else { return } do { - tabState = .statuses(statusesState: .loading) + statusesState = .loading + boosts = [] statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil, - onlyMedia: selectedTab == .media ? true : nil, - excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, + onlyMedia: selectedTab == .media, + excludeReplies: selectedTab != .replies, + excludeReblogs: selectedTab != .boosts, pinned: nil)) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) + if selectedTab == .boosts { + boosts = statuses.filter{ $0.reblog != nil } + } if selectedTab == .statuses { pinned = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil, - onlyMedia: nil, - excludeReplies: nil, + onlyMedia: false, + excludeReplies: false, + excludeReblogs: false, pinned: true)) StatusDataControllerProvider.shared.updateDataControllers(for: pinned, client: client) } @@ -200,7 +187,7 @@ import SwiftUI } reloadTabState() } catch { - tabState = .statuses(statusesState: .error(error: error)) + statusesState = .error(error: error) } } @@ -208,56 +195,66 @@ import SwiftUI guard let client else { return } do { switch selectedTab { - case .statuses, .postsAndReplies, .media: + case .statuses, .replies, .boosts, .media: guard let lastId = statuses.last?.id else { return } - tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage)) + if selectedTab == .boosts { + statusesState = .display(statuses: boosts, nextPageState: .loadingNextPage) + } else { + statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) + } let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId, tag: nil, - onlyMedia: selectedTab == .media ? true : nil, - excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, + onlyMedia: selectedTab == .media, + excludeReplies: selectedTab != .replies, + excludeReblogs: selectedTab != .boosts, pinned: nil)) statuses.append(contentsOf: newStatuses) + if selectedTab == .boosts { + let newBoosts = statuses.filter{ $0.reblog != nil } + self.boosts.append(contentsOf: newBoosts) + } StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) - tabState = .statuses(statusesState: .display(statuses: statuses, - nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)) + if selectedTab == .boosts { + statusesState = .display(statuses: boosts, + nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) + } else { + statusesState = .display(statuses: statuses, + nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) + } case .favorites: guard let nextPageId = favoritesNextPage?.maxId else { return } let newFavorites: [Status] (newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId)) favorites.append(contentsOf: newFavorites) StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client) - tabState = .statuses(statusesState: .display(statuses: favorites, nextPageState: .hasNextPage)) + statusesState = .display(statuses: favorites, 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)) StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client) bookmarks.append(contentsOf: newBookmarks) - tabState = .statuses(statusesState: .display(statuses: bookmarks, nextPageState: .hasNextPage)) - case .followedTags, .lists: - break + statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage) } } catch { - tabState = .statuses(statusesState: .error(error: error)) + statusesState = .error(error: error) } } private func reloadTabState() { switch selectedTab { - case .statuses, .postsAndReplies, .media: - tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)) + case .statuses, .replies, .media: + statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) + case .boosts: + statusesState = .display(statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage) case .favorites: - tabState = .statuses(statusesState: .display(statuses: favorites, - nextPageState: favoritesNextPage != nil ? .hasNextPage : .none)) + statusesState = .display(statuses: favorites, + nextPageState: favoritesNextPage != nil ? .hasNextPage : .none) case .bookmarks: - tabState = .statuses(statusesState: .display(statuses: bookmarks, - nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none)) - case .followedTags: - tabState = .followedTags - case .lists: - tabState = .lists + statusesState = .display(statuses: bookmarks, + nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none) } } diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index f964aca9..bcc8f8e3 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -14,8 +14,9 @@ public enum Accounts: Endpoint { case statuses(id: String, sinceId: String?, tag: String?, - onlyMedia: Bool?, - excludeReplies: Bool?, + onlyMedia: Bool, + excludeReplies: Bool, + excludeReblogs: Bool, pinned: Bool?) case relationships(ids: [String]) case follow(id: String, notify: Bool, reblogs: Bool) @@ -50,7 +51,7 @@ public enum Accounts: Endpoint { "accounts/verify_credentials" case .updateCredentials, .updateCredentialsMedia: "accounts/update_credentials" - case let .statuses(id, _, _, _, _, _): + case let .statuses(id, _, _, _, _, _, _): "accounts/\(id)/statuses" case .relationships: "accounts/relationships" @@ -89,7 +90,7 @@ public enum Accounts: Endpoint { return [ .init(name: "acct", value: name), ] - case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, pinned): + case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, excludeReblogs, pinned): var params: [URLQueryItem] = [] if let tag { params.append(.init(name: "tagged", value: tag)) @@ -97,12 +98,11 @@ public enum Accounts: Endpoint { if let sinceId { params.append(.init(name: "max_id", value: sinceId)) } - if let onlyMedia { - params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false")) - } - if let excludeReplies { - params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false")) - } + + params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false")) + params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false")) + params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false")) + if let pinned { params.append(.init(name: "pinned", value: pinned ? "true" : "false")) } diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index b10bbe53..3cb808d5 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -160,7 +160,13 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable { case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) case let .hashtag(tag, accountId): if let accountId { - return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil) + return Accounts.statuses(id: accountId, + sinceId: nil, + tag: tag, + onlyMedia: false, + excludeReplies: false, + excludeReblogs: false, + pinned: nil) } else { return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId, minId: minId) }