This commit is contained in:
Thomas Ricouard 2024-01-08 18:22:44 +01:00
parent e725b6be4d
commit 5d24c4d2e8
4 changed files with 84 additions and 132 deletions

View file

@ -63,9 +63,15 @@ public struct AccountDetailView: View {
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs, ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
id: \.self) id: \.self)
{ tab in { tab in
Image(systemName: tab.iconName) if tab == .boosts {
.tag(tab) Image("Rocket")
.accessibilityLabel(tab.accessibilityLabel) .tag(tab)
.accessibilityLabel(tab.accessibilityLabel)
} else {
Image(systemName: tab.iconName)
.tag(tab)
.accessibilityLabel(tab.accessibilityLabel)
}
} }
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
@ -73,19 +79,12 @@ public struct AccountDetailView: View {
.applyAccountDetailsRowStyle(theme: theme) .applyAccountDetailsRowStyle(theme: theme)
.id("status") .id("status")
switch viewModel.tabState { if viewModel.selectedTab == .statuses {
case .statuses: pinnedPostsView
if viewModel.selectedTab == .statuses {
pinnedPostsView
}
StatusesListView(fetcher: viewModel,
client: client,
routerPath: routerPath)
case .followedTags:
tagsListView
case .lists:
listsListView
} }
StatusesListView(fetcher: viewModel,
client: client,
routerPath: routerPath)
} }
.environment(\.defaultMinListRowHeight, 1) .environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain) .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 @ViewBuilder
private var pinnedPostsView: some View { private var pinnedPostsView: some View {
if !viewModel.pinned.isEmpty { if !viewModel.pinned.isEmpty {

View file

@ -16,14 +16,14 @@ import SwiftUI
} }
enum Tab: Int { enum Tab: Int {
case statuses, favorites, bookmarks, followedTags, postsAndReplies, media, lists case statuses, favorites, bookmarks, replies, boosts, media
static var currentAccountTabs: [Tab] { static var currentAccountTabs: [Tab] {
[.statuses, .favorites, .bookmarks, .followedTags, .lists] [.statuses, .replies, .boosts, .favorites, .bookmarks]
} }
static var accountTabs: [Tab] { static var accountTabs: [Tab] {
[.statuses, .postsAndReplies, .media] [.statuses, .replies, .boosts, .media]
} }
var iconName: String { var iconName: String {
@ -31,10 +31,9 @@ import SwiftUI
case .statuses: "bubble.right" case .statuses: "bubble.right"
case .favorites: "star" case .favorites: "star"
case .bookmarks: "bookmark" case .bookmarks: "bookmark"
case .followedTags: "tag" case .replies: "bubble.left.and.bubble.right"
case .postsAndReplies: "bubble.left.and.bubble.right" case .boosts: ""
case .media: "photo.on.rectangle.angled" case .media: "photo.on.rectangle.angled"
case .lists: "list.bullet"
} }
} }
@ -43,34 +42,14 @@ import SwiftUI
case .statuses: "accessibility.tabs.profile.picker.statuses" case .statuses: "accessibility.tabs.profile.picker.statuses"
case .favorites: "accessibility.tabs.profile.picker.favorites" case .favorites: "accessibility.tabs.profile.picker.favorites"
case .bookmarks: "accessibility.tabs.profile.picker.bookmarks" case .bookmarks: "accessibility.tabs.profile.picker.bookmarks"
case .followedTags: "accessibility.tabs.profile.picker.followed-tags" case .replies: "accessibility.tabs.profile.picker.posts-and-replies"
case .postsAndReplies: "accessibility.tabs.profile.picker.posts-and-replies" case .boosts: "accessibility.tabs.profile.picker.boosts"
case .media: "accessibility.tabs.profile.picker.media" 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 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 statusesState: StatusesState = .loading
var relationship: Relationship? var relationship: Relationship?
@ -85,7 +64,7 @@ import SwiftUI
var selectedTab = Tab.statuses { var selectedTab = Tab.statuses {
didSet { didSet {
switch selectedTab { switch selectedTab {
case .statuses, .postsAndReplies, .media: case .statuses, .replies, .boosts, .media:
tabTask?.cancel() tabTask?.cancel()
tabTask = Task { tabTask = Task {
await fetchNewestStatuses(pullToRefresh: false) await fetchNewestStatuses(pullToRefresh: false)
@ -105,6 +84,8 @@ import SwiftUI
private var tabTask: Task<Void, Never>? private var tabTask: Task<Void, Never>?
private(set) var statuses: [Status] = [] private(set) var statuses: [Status] = []
var boosts: [Status] = []
/// When coming from a URL like a mention tap in a status. /// When coming from a URL like a mention tap in a status.
init(accountId: String) { init(accountId: String) {
@ -173,22 +154,28 @@ import SwiftUI
func fetchNewestStatuses(pullToRefresh: Bool) async { func fetchNewestStatuses(pullToRefresh: Bool) async {
guard let client else { return } guard let client else { return }
do { do {
tabState = .statuses(statusesState: .loading) statusesState = .loading
boosts = []
statuses = statuses =
try await client.get(endpoint: Accounts.statuses(id: accountId, try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: nil, sinceId: nil,
tag: nil, tag: nil,
onlyMedia: selectedTab == .media ? true : nil, onlyMedia: selectedTab == .media,
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, excludeReplies: selectedTab != .replies,
excludeReblogs: selectedTab != .boosts,
pinned: nil)) pinned: nil))
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if selectedTab == .boosts {
boosts = statuses.filter{ $0.reblog != nil }
}
if selectedTab == .statuses { if selectedTab == .statuses {
pinned = pinned =
try await client.get(endpoint: Accounts.statuses(id: accountId, try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: nil, sinceId: nil,
tag: nil, tag: nil,
onlyMedia: nil, onlyMedia: false,
excludeReplies: nil, excludeReplies: false,
excludeReblogs: false,
pinned: true)) pinned: true))
StatusDataControllerProvider.shared.updateDataControllers(for: pinned, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: pinned, client: client)
} }
@ -200,7 +187,7 @@ import SwiftUI
} }
reloadTabState() reloadTabState()
} catch { } catch {
tabState = .statuses(statusesState: .error(error: error)) statusesState = .error(error: error)
} }
} }
@ -208,56 +195,66 @@ import SwiftUI
guard let client else { return } guard let client else { return }
do { do {
switch selectedTab { switch selectedTab {
case .statuses, .postsAndReplies, .media: case .statuses, .replies, .boosts, .media:
guard let lastId = statuses.last?.id else { return } 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] = let newStatuses: [Status] =
try await client.get(endpoint: Accounts.statuses(id: accountId, try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: lastId, sinceId: lastId,
tag: nil, tag: nil,
onlyMedia: selectedTab == .media ? true : nil, onlyMedia: selectedTab == .media,
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, excludeReplies: selectedTab != .replies,
excludeReblogs: selectedTab != .boosts,
pinned: nil)) pinned: nil))
statuses.append(contentsOf: newStatuses) 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) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
tabState = .statuses(statusesState: .display(statuses: statuses, if selectedTab == .boosts {
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)) statusesState = .display(statuses: boosts,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} else {
statusesState = .display(statuses: statuses,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
}
case .favorites: case .favorites:
guard let nextPageId = favoritesNextPage?.maxId else { return } guard let nextPageId = favoritesNextPage?.maxId else { return }
let newFavorites: [Status] let newFavorites: [Status]
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId)) (newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
favorites.append(contentsOf: newFavorites) favorites.append(contentsOf: newFavorites)
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
tabState = .statuses(statusesState: .display(statuses: favorites, nextPageState: .hasNextPage)) statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
case .bookmarks: case .bookmarks:
guard let nextPageId = bookmarksNextPage?.maxId else { return } guard let nextPageId = bookmarksNextPage?.maxId else { return }
let newBookmarks: [Status] let newBookmarks: [Status]
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId)) (newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
bookmarks.append(contentsOf: newBookmarks) bookmarks.append(contentsOf: newBookmarks)
tabState = .statuses(statusesState: .display(statuses: bookmarks, nextPageState: .hasNextPage)) statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
case .followedTags, .lists:
break
} }
} catch { } catch {
tabState = .statuses(statusesState: .error(error: error)) statusesState = .error(error: error)
} }
} }
private func reloadTabState() { private func reloadTabState() {
switch selectedTab { switch selectedTab {
case .statuses, .postsAndReplies, .media: case .statuses, .replies, .media:
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)) statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
case .boosts:
statusesState = .display(statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
case .favorites: case .favorites:
tabState = .statuses(statusesState: .display(statuses: favorites, statusesState = .display(statuses: favorites,
nextPageState: favoritesNextPage != nil ? .hasNextPage : .none)) nextPageState: favoritesNextPage != nil ? .hasNextPage : .none)
case .bookmarks: case .bookmarks:
tabState = .statuses(statusesState: .display(statuses: bookmarks, statusesState = .display(statuses: bookmarks,
nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none)) nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none)
case .followedTags:
tabState = .followedTags
case .lists:
tabState = .lists
} }
} }

View file

@ -14,8 +14,9 @@ public enum Accounts: Endpoint {
case statuses(id: String, case statuses(id: String,
sinceId: String?, sinceId: String?,
tag: String?, tag: String?,
onlyMedia: Bool?, onlyMedia: Bool,
excludeReplies: Bool?, excludeReplies: Bool,
excludeReblogs: Bool,
pinned: Bool?) pinned: Bool?)
case relationships(ids: [String]) case relationships(ids: [String])
case follow(id: String, notify: Bool, reblogs: Bool) case follow(id: String, notify: Bool, reblogs: Bool)
@ -50,7 +51,7 @@ public enum Accounts: Endpoint {
"accounts/verify_credentials" "accounts/verify_credentials"
case .updateCredentials, .updateCredentialsMedia: case .updateCredentials, .updateCredentialsMedia:
"accounts/update_credentials" "accounts/update_credentials"
case let .statuses(id, _, _, _, _, _): case let .statuses(id, _, _, _, _, _, _):
"accounts/\(id)/statuses" "accounts/\(id)/statuses"
case .relationships: case .relationships:
"accounts/relationships" "accounts/relationships"
@ -89,7 +90,7 @@ public enum Accounts: Endpoint {
return [ return [
.init(name: "acct", value: name), .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] = [] var params: [URLQueryItem] = []
if let tag { if let tag {
params.append(.init(name: "tagged", value: tag)) params.append(.init(name: "tagged", value: tag))
@ -97,12 +98,11 @@ public enum Accounts: Endpoint {
if let sinceId { if let sinceId {
params.append(.init(name: "max_id", value: sinceId)) params.append(.init(name: "max_id", value: sinceId))
} }
if let onlyMedia {
params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false")) params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false"))
} params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false"))
if let excludeReplies { params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false"))
params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false"))
}
if let pinned { if let pinned {
params.append(.init(name: "pinned", value: pinned ? "true" : "false")) params.append(.init(name: "pinned", value: pinned ? "true" : "false"))
} }

View file

@ -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 .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
case let .hashtag(tag, accountId): case let .hashtag(tag, accountId):
if let 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 { } else {
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId, minId: minId) return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId, minId: minId)
} }