This commit is contained in:
Justin Mazzocchi 2021-02-10 15:41:41 -08:00
parent 1580fb0032
commit 5de072aa8f
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
13 changed files with 232 additions and 14 deletions

View file

@ -24,6 +24,12 @@
"account.hide-reblogs" = "Hide boosts"; "account.hide-reblogs" = "Hide boosts";
"account.locked.accessibility-label" = "Locked account"; "account.locked.accessibility-label" = "Locked account";
"account.mute" = "Mute"; "account.mute" = "Mute";
"account.mute.indefinite" = "Indefnite";
"account.mute.confirm-%@" = "Are you sure you want to mute %@?";
"account.mute.confirm.explanation" = "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.";
"account.mute.confirm.hide-notifications" = "Hide notifications from this user?";
"account.mute.confirm.duration" = "Duration";
"account.mute.target-%@" = "Muting %@";
"account.reject-follow-request-button.accessibility-label" = "Reject follow request"; "account.reject-follow-request-button.accessibility-label" = "Reject follow request";
"account.request" = "Request"; "account.request" = "Request";
"account.request.cancel" = "Cancel follow request"; "account.request.cancel" = "Cancel follow request";
@ -38,6 +44,7 @@
"account.unblock.confirm-%@" = "Unblock %@?"; "account.unblock.confirm-%@" = "Unblock %@?";
"account.unfollow" = "Unfollow"; "account.unfollow" = "Unfollow";
"account.unmute" = "Unmute"; "account.unmute" = "Unmute";
"account.unmute.confirm-%@" = "Unmute %@?";
"activity.open-in-default-browser" = "Open in default browser"; "activity.open-in-default-browser" = "Open in default browser";
"add" = "Add"; "add" = "Add";
"apns-default-message" = "New notification"; "apns-default-message" = "New notification";

View file

@ -9,7 +9,7 @@ public enum RelationshipEndpoint {
case accountsUnfollow(id: Account.Id) case accountsUnfollow(id: Account.Id)
case accountsBlock(id: Account.Id) case accountsBlock(id: Account.Id)
case accountsUnblock(id: Account.Id) case accountsUnblock(id: Account.Id)
case accountsMute(id: Account.Id) case accountsMute(id: Account.Id, notifications: Bool = true, duration: Int = 0)
case accountsUnmute(id: Account.Id) case accountsUnmute(id: Account.Id)
case accountsPin(id: Account.Id) case accountsPin(id: Account.Id)
case accountsUnpin(id: Account.Id) case accountsUnpin(id: Account.Id)
@ -40,7 +40,7 @@ extension RelationshipEndpoint: Endpoint {
return [id, "block"] return [id, "block"]
case let .accountsUnblock(id): case let .accountsUnblock(id):
return [id, "unblock"] return [id, "unblock"]
case let .accountsMute(id): case let .accountsMute(id, _, _):
return [id, "mute"] return [id, "mute"]
case let .accountsUnmute(id): case let .accountsUnmute(id):
return [id, "unmute"] return [id, "unmute"]
@ -72,6 +72,8 @@ extension RelationshipEndpoint: Endpoint {
public var jsonBody: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .accountsMute(_, notifications, duration):
return ["notifications": notifications, "duration": duration]
case let .note(note, _): case let .note(note, _):
return ["comment": note] return ["comment": note]
default: default:

View file

@ -92,6 +92,7 @@
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */; }; D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */; };
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; }; D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; };
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; }; D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; };
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07F4D9725D493E300F61133 /* MuteView.swift */; };
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087671525BAA8C0001FDD43 /* ExploreViewController.swift */; }; D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087671525BAA8C0001FDD43 /* ExploreViewController.swift */; };
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; }; D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
@ -292,6 +293,7 @@
D07EC7F125B13E57006DF726 /* EmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiView.swift; sourceTree = "<group>"; }; D07EC7F125B13E57006DF726 /* EmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiView.swift; sourceTree = "<group>"; };
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategoryHeaderView.swift; sourceTree = "<group>"; }; D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategoryHeaderView.swift; sourceTree = "<group>"; };
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemEmoji+Extensions.swift"; sourceTree = "<group>"; }; D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemEmoji+Extensions.swift"; sourceTree = "<group>"; };
D07F4D9725D493E300F61133 /* MuteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteView.swift; sourceTree = "<group>"; };
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; }; D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; }; D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; }; D087671525BAA8C0001FDD43 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
@ -487,6 +489,7 @@
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */, D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
D0BEB20424FA1107001B0F04 /* FiltersView.swift */, D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */, D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
D07F4D9725D493E300F61133 /* MuteView.swift */,
D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */, D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */,
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */, D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
D0C7D42624F76169001EBDBB /* PreferencesView.swift */, D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
@ -1015,6 +1018,7 @@
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */, D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
D0477F2C25C6EBAD005C5368 /* OpenInDefaultBrowserActivity.swift in Sources */, D0477F2C25C6EBAD005C5368 /* OpenInDefaultBrowserActivity.swift in Sources */,
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */, D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */, D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */,
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */, D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */,
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */, D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,

View file

@ -62,8 +62,8 @@ public extension AccountService {
relationshipAction(.accountsUnblock(id: account.id)) relationshipAction(.accountsUnblock(id: account.id))
} }
func mute() -> AnyPublisher<Never, Error> { func mute(notifications: Bool, duration: Int) -> AnyPublisher<Never, Error> {
relationshipAction(.accountsMute(id: account.id)) relationshipAction(.accountsMute(id: account.id, notifications: notifications, duration: duration))
.collect() .collect()
.flatMap { _ in contentDatabase.mute(id: account.id) } .flatMap { _ in contentDatabase.mute(id: account.id) }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -95,13 +95,13 @@ private extension ProfileViewController {
actions.append(UIAction( actions.append(UIAction(
title: NSLocalizedString("account.unmute", comment: ""), title: NSLocalizedString("account.unmute", comment: ""),
image: UIImage(systemName: "speaker")) { _ in image: UIImage(systemName: "speaker")) { _ in
accountViewModel.unmute() accountViewModel.confirmUnmute()
}) })
} else { } else {
actions.append(UIAction( actions.append(UIAction(
title: NSLocalizedString("account.mute", comment: ""), title: NSLocalizedString("account.mute", comment: ""),
image: UIImage(systemName: "speaker.slash")) { _ in image: UIImage(systemName: "speaker.slash")) { _ in
accountViewModel.mute() accountViewModel.confirmMute()
}) })
} }

View file

@ -443,6 +443,10 @@ private extension TableViewController {
compose(inReplyToViewModel: inReplyToViewModel, redraft: redraft) compose(inReplyToViewModel: inReplyToViewModel, redraft: redraft)
case let .confirmDelete(statusViewModel, redraft): case let .confirmDelete(statusViewModel, redraft):
confirmDelete(statusViewModel: statusViewModel, redraft: redraft) confirmDelete(statusViewModel: statusViewModel, redraft: redraft)
case let .confirmMute(accountViewModel):
confirmMute(muteViewModel: accountViewModel.muteViewModel())
case let .confirmUnmute(accountViewModel):
confirmUnmute(accountViewModel: accountViewModel)
case let .confirmBlock(accountViewModel): case let .confirmBlock(accountViewModel):
confirmBlock(accountViewModel: accountViewModel) confirmBlock(accountViewModel: accountViewModel)
case let .confirmUnblock(accountViewModel): case let .confirmUnblock(accountViewModel):
@ -571,6 +575,21 @@ private extension TableViewController {
present(alertController, animated: true) present(alertController, animated: true)
} }
func confirmMute(muteViewModel: MuteViewModel) {
let muteViewController = MuteViewController(viewModel: muteViewModel)
let navigationController = UINavigationController(rootViewController: muteViewController)
present(navigationController, animated: true)
}
func confirmUnmute(accountViewModel: AccountViewModel) {
confirm(message: String.localizedStringWithFormat(
NSLocalizedString("account.unmute.confirm-%@", comment: ""),
accountViewModel.accountName)) {
accountViewModel.unmute()
}
}
func confirmBlock(accountViewModel: AccountViewModel) { func confirmBlock(accountViewModel: AccountViewModel) {
let alertController = UIAlertController( let alertController = UIAlertController(
title: nil, title: nil,

View file

@ -85,6 +85,15 @@ public extension ReportViewModel {
identityContext: .preview) identityContext: .preview)
} }
public extension MuteViewModel {
static let preview = MuteViewModel(
accountService: AccountService(
account: .preview,
mastodonAPIClient: .preview,
contentDatabase: .preview),
identityContext: .preview)
}
public extension DomainBlocksViewModel { public extension DomainBlocksViewModel {
static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview)) static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
} }

View file

@ -11,6 +11,8 @@ public enum CollectionItemEvent {
case attachment(AttachmentViewModel, StatusViewModel) case attachment(AttachmentViewModel, StatusViewModel)
case compose(inReplyTo: StatusViewModel?, redraft: Status?) case compose(inReplyTo: StatusViewModel?, redraft: Status?)
case confirmDelete(StatusViewModel, redraft: Bool) case confirmDelete(StatusViewModel, redraft: Bool)
case confirmMute(AccountViewModel)
case confirmUnmute(AccountViewModel)
case confirmBlock(AccountViewModel) case confirmBlock(AccountViewModel)
case confirmUnblock(AccountViewModel) case confirmUnblock(AccountViewModel)
case confirmDomainBlock(AccountViewModel) case confirmDomainBlock(AccountViewModel)

View file

@ -95,6 +95,10 @@ public extension AccountViewModel {
ReportViewModel(accountService: accountService, identityContext: identityContext) ReportViewModel(accountService: accountService, identityContext: identityContext)
} }
func muteViewModel() -> MuteViewModel {
MuteViewModel(accountService: accountService, identityContext: identityContext)
}
func follow() { func follow() {
ignorableOutputEvent(accountService.follow()) ignorableOutputEvent(accountService.follow())
} }
@ -127,8 +131,12 @@ public extension AccountViewModel {
ignorableOutputEvent(accountService.unblock()) ignorableOutputEvent(accountService.unblock())
} }
func mute() { func confirmMute() {
ignorableOutputEvent(accountService.mute()) eventsSubject.send(Just(.confirmMute(self)).setFailureType(to: Error.self).eraseToAnyPublisher())
}
func confirmUnmute() {
eventsSubject.send(Just(.confirmUnmute(self)).setFailureType(to: Error.self).eraseToAnyPublisher())
} }
func unmute() { func unmute() {

View file

@ -0,0 +1,64 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
import ServiceLayer
public final class MuteViewModel: ObservableObject {
@Published public var notifications = true
@Published public var duration = Duration.indefinite
@Published public private(set) var loading = false
@Published public var alertItem: AlertItem?
public let events: AnyPublisher<Event, Never>
public let identityContext: IdentityContext
private let accountService: AccountService
private let eventsSubject = PassthroughSubject<Event, Never>()
private var cancellables = Set<AnyCancellable>()
public init(accountService: AccountService, identityContext: IdentityContext) {
self.accountService = accountService
self.identityContext = identityContext
events = eventsSubject.eraseToAnyPublisher()
}
}
public extension MuteViewModel {
enum Event {
case muted
}
enum Duration: Int, CaseIterable {
case indefinite = 0
case fiveMinutes = 300
case thirtyMinutes = 1800
case oneHour = 3600
case sixHours = 21600
case oneDay = 86400
case threeDays = 259200
case sevenDays = 604800
}
var accountName: String { "@".appending(accountService.account.acct) }
func mute() {
accountService.mute(notifications: notifications, duration: duration.rawValue)
.receive(on: DispatchQueue.main)
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { [weak self] in
guard let self = self else { return }
self.loading = false
if $0 == .finished {
self.eventsSubject.send(.muted)
}
} receiveValue: { _ in }
.store(in: &cancellables)
}
}
extension MuteViewModel.Duration: Identifiable {
public var id: Int { rawValue }
}

View file

@ -0,0 +1,105 @@
// Copyright © 2021 Metabolist. All rights reserved.
import SwiftUI
import ViewModels
struct MuteView: View {
@StateObject var viewModel: MuteViewModel
@Environment(\.presentationMode) private var presentationMode
fileprivate var dismissHostingController: (() -> Void)?
var body: some View {
Form {
VStack(alignment: .leading, spacing: .defaultSpacing) {
Text("account.mute.confirm-\(viewModel.accountName)")
Text("account.mute.confirm.explanation")
}
Toggle("account.mute.confirm.hide-notifications", isOn: $viewModel.notifications)
Picker("account.mute.confirm.duration", selection: $viewModel.duration) {
ForEach(MuteViewModel.Duration.allCases) { duration in
Text(verbatim: duration.title).tag(duration)
}
}
Group {
if viewModel.loading {
ProgressView()
} else {
Button("account.mute") {
viewModel.mute()
}
}
}
}
.alertItem($viewModel.alertItem)
.onReceive(viewModel.events) {
switch $0 {
case .muted:
dismiss()
}
}
.navigationTitle("account.mute.target-\(viewModel.accountName)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("cancel") {
dismiss()
}
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
private extension MuteView {
func dismiss() {
if let dismissHostingController = dismissHostingController {
dismissHostingController()
} else {
presentationMode.wrappedValue.dismiss()
}
}
}
final class MuteViewController: UIHostingController<MuteView> {
init(viewModel: MuteViewModel) {
super.init(rootView: MuteView(viewModel: viewModel))
rootView.dismissHostingController = { [weak self] in self?.dismiss(animated: true) }
}
@available(*, unavailable)
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension MuteViewModel.Duration {
static let dateComponentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
return formatter
}()
var title: String {
switch self {
case .indefinite:
return NSLocalizedString("account.mute.indefinite", comment: "")
default:
return Self.dateComponentsFormatter.string(from: TimeInterval(rawValue)) ?? String(rawValue)
}
}
}
#if DEBUG
import PreviewViewModels
struct MuteView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
MuteView(viewModel: .preview)
}
}
}
#endif

View file

@ -52,10 +52,8 @@ struct ReportView: View {
.navigationTitle("report.target-\(viewModel.accountName)") .navigationTitle("report.target-\(viewModel.accountName)")
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button { Button("cancel") {
dismiss() dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
} }
} }
} }
@ -75,7 +73,7 @@ private extension ReportView {
} }
} }
class ReportViewController: UIHostingController<ReportView> { final class ReportViewController: UIHostingController<ReportView> {
init(viewModel: ReportViewModel) { init(viewModel: ReportViewModel) {
super.init(rootView: ReportView(viewModel: viewModel)) super.init(rootView: ReportView(viewModel: viewModel))

View file

@ -681,13 +681,13 @@ private extension StatusView {
secondSectionItems.append(UIAction( secondSectionItems.append(UIAction(
title: NSLocalizedString("account.unmute", comment: ""), title: NSLocalizedString("account.unmute", comment: ""),
image: UIImage(systemName: "speaker")) { _ in image: UIImage(systemName: "speaker")) { _ in
viewModel.accountViewModel.unmute() viewModel.accountViewModel.confirmUnmute()
}) })
} else { } else {
secondSectionItems.append(UIAction( secondSectionItems.append(UIAction(
title: NSLocalizedString("account.mute", comment: ""), title: NSLocalizedString("account.mute", comment: ""),
image: UIImage(systemName: "speaker.slash")) { _ in image: UIImage(systemName: "speaker.slash")) { _ in
viewModel.accountViewModel.mute() viewModel.accountViewModel.confirmMute()
}) })
} }