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.
This commit is contained in:
Nathan Reed 2023-07-03 01:40:49 -04:00 committed by GitHub
parent 8c97c9e1be
commit 194e3aea74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 1 deletions

View file

@ -9,6 +9,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ObservedObject private var fetcher: Fetcher @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 isRemote: Bool
private let routerPath: RouterPath private let routerPath: RouterPath
private let client: Client private let client: Client

View file

@ -99,6 +99,11 @@ public struct StatusRowView: View {
} }
.contextMenu { .contextMenu {
contextMenu contextMenu
.onAppear {
Task {
await viewModel.loadAuthorRelationship()
}
}
} }
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
// The actions associated with the swipes are exposed as custom accessibility actions and there is no way to remove them. // The actions associated with the swipes are exposed as custom accessibility actions and there is no way to remove them.

View file

@ -10,6 +10,7 @@ import SwiftUI
public class StatusRowViewModel: ObservableObject { public class StatusRowViewModel: ObservableObject {
let status: Status let status: Status
let isFocused: Bool let isFocused: Bool
// Whether this status is on a remote local timeline (many actions are unavailable if so)
let isRemote: Bool let isRemote: Bool
let showActions: Bool let showActions: Bool
let textDisabled: Bool let textDisabled: Bool
@ -32,6 +33,17 @@ public class StatusRowViewModel: ObservableObject {
@Published var isLoadingRemoteContent: Bool = false @Published var isLoadingRemoteContent: Bool = false
@Published var localStatusId: String? @Published var localStatusId: String?
@Published var localStatus: Status? @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 // used by the button to expand a collapsed post
@Published var isCollapsed: Bool = true { @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? { private func embededStatusURL() -> URL? {
let content = finalStatus.content let content = finalStatus.content
if !content.statusesURLs.isEmpty, if !content.statusesURLs.isEmpty,

View file

@ -7,6 +7,7 @@ import SwiftUI
struct StatusRowContextMenu: View { struct StatusRowContextMenu: View {
@Environment(\.displayScale) var displayScale @Environment(\.displayScale) var displayScale
@EnvironmentObject private var client: Client
@EnvironmentObject private var sceneDelegate: SceneDelegate @EnvironmentObject private var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var account: CurrentAccount @EnvironmentObject private var account: CurrentAccount
@ -186,6 +187,68 @@ struct StatusRowContextMenu: View {
} label: { } label: {
Label("status.action.message", systemImage: "tray.full") 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 { Section {

View file

@ -113,9 +113,14 @@ struct StatusRowHeaderView: View {
private var contextMenuButton: some View { private var contextMenuButton: some View {
Menu { Menu {
StatusRowContextMenu(viewModel: viewModel) StatusRowContextMenu(viewModel: viewModel)
.onAppear {
Task {
await viewModel.loadAuthorRelationship()
}
}
} label: { } label: {
Image(systemName: "ellipsis") Image(systemName: "ellipsis")
.frame(width: 20, height: 20) .frame(width: 40, height: 40)
} }
.menuStyle(.borderlessButton) .menuStyle(.borderlessButton)
.foregroundColor(.gray) .foregroundColor(.gray)