From 194e3aea74e878e1095e802952a535a8f61216aa Mon Sep 17 00:00:00 2001 From: Nathan Reed Date: Mon, 3 Jul 2023 01:40:49 -0400 Subject: [PATCH] Add feature to block or mute user directly from post (#1460) * Make status context menu button frame tap target larger This makes it much easier to hit on the first try, and doesn't appear to negatively impact the layout. * Add feature to block or mute user directly from post To avoid calling the /accounts/relationships endpoint for every single status displayed, the data is only loaded when the menu is activated. When the API call comes back, the items are added to the menu (updating the view model appears to cause the menu to update, even while it is displayed) Borrowed blocking & muting logic/menu items from AccountDetailContextMenu. --- .../Status/List/StatusesListView.swift | 1 + .../Sources/Status/Row/StatusRowView.swift | 5 ++ .../Status/Row/StatusRowViewModel.swift | 17 +++++ .../Row/Subviews/StatusRowContextMenu.swift | 63 +++++++++++++++++++ .../Row/Subviews/StatusRowHeaderView.swift | 7 ++- 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/Packages/Status/Sources/Status/List/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift index efae9440..9eebdb4a 100644 --- a/Packages/Status/Sources/Status/List/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -9,6 +9,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { @EnvironmentObject private var theme: Theme @ObservedObject private var fetcher: Fetcher + // Whether this status is on a remote local timeline (many actions are unavailable if so) private let isRemote: Bool private let routerPath: RouterPath private let client: Client diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 73d20413..1bcd3f31 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -99,6 +99,11 @@ public struct StatusRowView: View { } .contextMenu { contextMenu + .onAppear { + Task { + await viewModel.loadAuthorRelationship() + } + } } .swipeActions(edge: .trailing) { // The actions associated with the swipes are exposed as custom accessibility actions and there is no way to remove them. diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 1d9dc8fa..72c24bff 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI public class StatusRowViewModel: ObservableObject { let status: Status let isFocused: Bool + // Whether this status is on a remote local timeline (many actions are unavailable if so) let isRemote: Bool let showActions: Bool let textDisabled: Bool @@ -32,6 +33,17 @@ public class StatusRowViewModel: ObservableObject { @Published var isLoadingRemoteContent: Bool = false @Published var localStatusId: String? @Published var localStatus: Status? + + // The relationship our user has to the author of this post, if available + @Published var authorRelationship: Relationship? { + didSet { + // if we are newly blocking or muting the author, force collapse post so it goes away + if let relationship = authorRelationship, + relationship.blocking || relationship.muting { + lineLimit = 0 + } + } + } // used by the button to expand a collapsed post @Published var isCollapsed: Bool = true { @@ -182,6 +194,11 @@ public class StatusRowViewModel: ObservableObject { } } + func loadAuthorRelationship() async { + let relationships: [Relationship]? = try? await client.get(endpoint: Accounts.relationships(ids: [status.reblog?.account.id ?? status.account.id])) + authorRelationship = relationships?.first + } + private func embededStatusURL() -> URL? { let content = finalStatus.content if !content.statusesURLs.isEmpty, diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift index 7bf1dbe4..04f6f7f8 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift @@ -7,6 +7,7 @@ import SwiftUI struct StatusRowContextMenu: View { @Environment(\.displayScale) var displayScale + @EnvironmentObject private var client: Client @EnvironmentObject private var sceneDelegate: SceneDelegate @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var account: CurrentAccount @@ -186,6 +187,68 @@ struct StatusRowContextMenu: View { } label: { Label("status.action.message", systemImage: "tray.full") } + + + if viewModel.authorRelationship?.blocking == true { + Button { + Task { + do { + let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post(endpoint: Accounts.unblock(id: operationAccount.id)) + } catch { + print("Error while unblocking: \(error.localizedDescription)") + } + } + } label: { + Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark") + } + } else { + Button { + Task { + do { + let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post(endpoint: Accounts.block(id: operationAccount.id)) + } catch { + print("Error while blocking: \(error.localizedDescription)") + } + } + } label: { + Label("account.action.block", systemImage: "person.crop.circle.badge.xmark") + } + } + + if viewModel.authorRelationship?.muting == true { + Button { + Task { + do { + let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post(endpoint: Accounts.unmute(id: operationAccount.id)) + } catch { + print("Error while unmuting: \(error.localizedDescription)") + } + } + } label: { + Label("account.action.unmute", systemImage: "speaker") + } + } else { + Menu { + ForEach(Duration.mutingDurations(), id: \.rawValue) { duration in + Button(duration.description) { + Task { + do { + let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post(endpoint: Accounts.mute(id: operationAccount.id, json: MuteData(duration: duration.rawValue))) + } catch { + print("Error while muting: \(error.localizedDescription)") + } + } + } + } + } label: { + Label("account.action.mute", systemImage: "speaker.slash") + } + } + } } Section { diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift index e365c83c..bf84c364 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift @@ -113,9 +113,14 @@ struct StatusRowHeaderView: View { private var contextMenuButton: some View { Menu { StatusRowContextMenu(viewModel: viewModel) + .onAppear { + Task { + await viewModel.loadAuthorRelationship() + } + } } label: { Image(systemName: "ellipsis") - .frame(width: 20, height: 20) + .frame(width: 40, height: 40) } .menuStyle(.borderlessButton) .foregroundColor(.gray)