From eb7fe22863b7d19e1e7db3724c079f03916bda4c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 11 Nov 2022 23:29:15 -0500 Subject: [PATCH] Add mute action to profiles Closes #201 --- .../Sources/Pachyderm/Model/Account.swift | 8 +- Tusker.xcodeproj/project.pbxproj | 12 ++ Tusker/Screens/Mute/MuteAccountView.swift | 144 ++++++++++++++++++ Tusker/Screens/Utilities/Previewing.swift | 30 ++++ 4 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 Tusker/Screens/Mute/MuteAccountView.swift diff --git a/Pachyderm/Sources/Pachyderm/Model/Account.swift b/Pachyderm/Sources/Pachyderm/Model/Account.swift index f22f36cc11..49dd7f1209 100644 --- a/Pachyderm/Sources/Pachyderm/Model/Account.swift +++ b/Pachyderm/Sources/Pachyderm/Model/Account.swift @@ -120,14 +120,14 @@ public final class Account: AccountProtocol, Decodable { return Request(method: .post, path: "/api/v1/accounts/\(accountID)/unblock") } - public static func mute(_ account: Account, notifications: Bool? = nil) -> Request { - return Request(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: ParametersBody([ + public static func mute(_ accountID: String, notifications: Bool? = nil) -> Request { + return Request(method: .post, path: "/api/v1/accounts/\(accountID)/mute", body: ParametersBody([ "notifications" => notifications ])) } - public static func unmute(_ account: Account) -> Request { - return Request(method: .post, path: "/api/v1/accounts/\(account.id)/unmute") + public static func unmute(_ accountID: String) -> Request { + return Request(method: .post, path: "/api/v1/accounts/\(accountID)/unmute") } public static func getLists(_ account: Account) -> Request<[List]> { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 64c0b40237..06a99e6cb2 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -315,6 +315,7 @@ D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; }; D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; }; D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A553291F0D9600F496A8 /* DeleteListService.swift */; }; + D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; }; /* End PBXBuildFile section */ @@ -685,6 +686,7 @@ D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = ""; }; D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = ""; }; D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = ""; }; + D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAccountView.swift; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -901,6 +903,7 @@ D641C788213DD86D004B4513 /* Large Image */, D627944B23A9A02400D38C68 /* Lists */, D641C782213DD7F0004B4513 /* Main */, + D6F6A555291F4F0C00F496A8 /* Mute */, D641C786213DD852004B4513 /* Notifications */, D641C783213DD7FE004B4513 /* Onboarding */, D641C789213DD87E004B4513 /* Preferences */, @@ -1486,6 +1489,14 @@ path = "Crash Reporter"; sourceTree = ""; }; + D6F6A555291F4F0C00F496A8 /* Mute */ = { + isa = PBXGroup; + children = ( + D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */, + ); + path = Mute; + sourceTree = ""; + }; D6F953F121251A2F00CF0F2B /* API */ = { isa = PBXGroup; children = ( @@ -1892,6 +1903,7 @@ D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */, D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, + D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */, D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */, diff --git a/Tusker/Screens/Mute/MuteAccountView.swift b/Tusker/Screens/Mute/MuteAccountView.swift new file mode 100644 index 0000000000..aeb4593c82 --- /dev/null +++ b/Tusker/Screens/Mute/MuteAccountView.swift @@ -0,0 +1,144 @@ +// +// MuteAccountView.swift +// Tusker +// +// Created by Shadowfacts on 11/11/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI +import Pachyderm + +struct MuteAccountView: View { + private static let durationOptions: [MenuPicker.Option] = { + let f = DateComponentsFormatter() + f.maximumUnitCount = 1 + f.unitsStyle = .full + f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] + + let durations: [TimeInterval] = [ + 30 * 60, + 60 * 60, + 6 * 60 * 60, + 24 * 60 * 60, + 3 * 24 * 60 * 60, + 7 * 60 * 60 * 60, + ] + return [ + .init(title: "Forever", value: 0) + ] + durations.map { .init(title: f.string(from: $0)!, value: $0) } + }() + + let account: AccountMO + let mastodonController: MastodonController + + @Environment(\.dismiss) private var dismiss: DismissAction + @ObservedObject private var preferences = Preferences.shared + @State private var muteNotifications = true + @State private var duration: TimeInterval = 0 + @State private var isMuting = false + @State private var error: Error? + + var body: some View { + NavigationView { + navigationViewContent + } + } + + private var navigationViewContent: some View { + Form { + Section { + HStack { + ComposeAvatarImageView(url: account.avatar) + .frame(width: 50, height: 50) + .cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50) + + VStack(alignment: .leading) { + AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17) + Text("@\(account.acct)") + .fontWeight(.light) + .foregroundColor(.secondary) + } + } + .frame(height: 50) + .listRowBackground(EmptyView()) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } + + Section { + Toggle(isOn: $muteNotifications) { + Text("Hide notifications from this person") + } + } footer: { + if muteNotifications { + Text("This user's posts and notifications will be hidden.") + } else { + Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.") + } + } + + Section { + Picker(selection: $duration) { + ForEach(MuteAccountView.durationOptions, id: \.value) { option in + Text(option.title).tag(option.value) + } + } label: { + Text("Duration") + } + } footer: { + if duration != 0 { + Text("The mute will automatically be removed after the selected time.") + } + } + + Button(action: self.mute) { + if isMuting { + HStack { + Text("Muting User") + Spacer() + ProgressView() + .progressViewStyle(.circular) + } + } else { + Text("Mute User") + } + } + .disabled(isMuting) + } + .alertWithData("Erorr Muting", data: $error, actions: { error in + Button("Ok") {} + }, message: { error in + Text(error.localizedDescription) + }) + .navigationTitle("Mute \(account.displayOrUserName)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.dismiss() + } + } + } + } + + private func mute() { + isMuting = true + let req = Account.mute(account.id, notifications: muteNotifications) + Task { + do { + let (relationship, _) = try await mastodonController.run(req) + mastodonController.persistentContainer.addOrUpdate(relationship: relationship) + self.dismiss() + } catch { + self.error = error + isMuting = false + } + } + } +} + +//struct MuteAccountView_Previews: PreviewProvider { +// static var previews: some View { +// MuteAccountView() +// } +//} diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index cb41de65a4..1ee1bcc8ce 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -10,6 +10,7 @@ import UIKit import SafariServices import Pachyderm import WebURLFoundationExtras +import SwiftUI protocol MenuActionProvider: AnyObject { var navigationDelegate: TuskerNavigationDelegate? { get } @@ -68,6 +69,7 @@ extension MenuActionProvider { if accountID != loggedInAccountID { actionsSection.append(relationshipAction(accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.followAction(for: $0, mastodonController: $1) })) suppressSection.append(relationshipAction(accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.blockAction(for: $0, mastodonController: $1) })) + suppressSection.append(relationshipAction(accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.muteAction(for: $0, mastodonController: $1) })) } addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID)) @@ -441,6 +443,34 @@ extension MenuActionProvider { } } + @MainActor + private func muteAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement { + if relationship.muting || relationship.mutingNotifications { + return UIAction(title: "Unmute", image: UIImage(systemName: "speaker")) { [unowned self] _ in + let req = Account.unmute(relationship.accountID) + mastodonController.run(req) { response in + switch response { + case .failure(let error): + if let toastable = self.toastableViewController { + let config = ToastConfiguration(from: error, with: "Error Unmuting", in: toastable, retryAction: nil) + DispatchQueue.main.async { + toastable.showToast(configuration: config, animated: true) + } + } + case .success(let relationship, _): + mastodonController.persistentContainer.addOrUpdate(relationship: relationship) + } + } + } + } else { + return UIAction(title: "Mute", image: UIImage(systemName: "speaker.slash")) { [unowned self] _ in + let view = MuteAccountView(account: relationship.account!, mastodonController: mastodonController) + let host = UIHostingController(rootView: view) + self.navigationDelegate?.present(host, animated: true) + } + } + } + } private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {