mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
Mute UI
This commit is contained in:
parent
1580fb0032
commit
5de072aa8f
13 changed files with 232 additions and 14 deletions
|
@ -24,6 +24,12 @@
|
|||
"account.hide-reblogs" = "Hide boosts";
|
||||
"account.locked.accessibility-label" = "Locked account";
|
||||
"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.request" = "Request";
|
||||
"account.request.cancel" = "Cancel follow request";
|
||||
|
@ -38,6 +44,7 @@
|
|||
"account.unblock.confirm-%@" = "Unblock %@?";
|
||||
"account.unfollow" = "Unfollow";
|
||||
"account.unmute" = "Unmute";
|
||||
"account.unmute.confirm-%@" = "Unmute %@?";
|
||||
"activity.open-in-default-browser" = "Open in default browser";
|
||||
"add" = "Add";
|
||||
"apns-default-message" = "New notification";
|
||||
|
|
|
@ -9,7 +9,7 @@ public enum RelationshipEndpoint {
|
|||
case accountsUnfollow(id: Account.Id)
|
||||
case accountsBlock(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 accountsPin(id: Account.Id)
|
||||
case accountsUnpin(id: Account.Id)
|
||||
|
@ -40,7 +40,7 @@ extension RelationshipEndpoint: Endpoint {
|
|||
return [id, "block"]
|
||||
case let .accountsUnblock(id):
|
||||
return [id, "unblock"]
|
||||
case let .accountsMute(id):
|
||||
case let .accountsMute(id, _, _):
|
||||
return [id, "mute"]
|
||||
case let .accountsUnmute(id):
|
||||
return [id, "unmute"]
|
||||
|
@ -72,6 +72,8 @@ extension RelationshipEndpoint: Endpoint {
|
|||
|
||||
public var jsonBody: [String: Any]? {
|
||||
switch self {
|
||||
case let .accountsMute(_, notifications, duration):
|
||||
return ["notifications": notifications, "duration": duration]
|
||||
case let .note(note, _):
|
||||
return ["comment": note]
|
||||
default:
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.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 */; };
|
||||
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07F4D9725D493E300F61133 /* MuteView.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 */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -487,6 +489,7 @@
|
|||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||
D07F4D9725D493E300F61133 /* MuteView.swift */,
|
||||
D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */,
|
||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||
|
@ -1015,6 +1018,7 @@
|
|||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0477F2C25C6EBAD005C5368 /* OpenInDefaultBrowserActivity.swift in Sources */,
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
|
||||
D0477F1525C68BAC005C5368 /* PrefetchRequestModifier.swift in Sources */,
|
||||
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */,
|
||||
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
||||
|
|
|
@ -62,8 +62,8 @@ public extension AccountService {
|
|||
relationshipAction(.accountsUnblock(id: account.id))
|
||||
}
|
||||
|
||||
func mute() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.accountsMute(id: account.id))
|
||||
func mute(notifications: Bool, duration: Int) -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.accountsMute(id: account.id, notifications: notifications, duration: duration))
|
||||
.collect()
|
||||
.flatMap { _ in contentDatabase.mute(id: account.id) }
|
||||
.eraseToAnyPublisher()
|
||||
|
|
|
@ -95,13 +95,13 @@ private extension ProfileViewController {
|
|||
actions.append(UIAction(
|
||||
title: NSLocalizedString("account.unmute", comment: ""),
|
||||
image: UIImage(systemName: "speaker")) { _ in
|
||||
accountViewModel.unmute()
|
||||
accountViewModel.confirmUnmute()
|
||||
})
|
||||
} else {
|
||||
actions.append(UIAction(
|
||||
title: NSLocalizedString("account.mute", comment: ""),
|
||||
image: UIImage(systemName: "speaker.slash")) { _ in
|
||||
accountViewModel.mute()
|
||||
accountViewModel.confirmMute()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -443,6 +443,10 @@ private extension TableViewController {
|
|||
compose(inReplyToViewModel: inReplyToViewModel, redraft: redraft)
|
||||
case let .confirmDelete(statusViewModel, 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):
|
||||
confirmBlock(accountViewModel: accountViewModel)
|
||||
case let .confirmUnblock(accountViewModel):
|
||||
|
@ -571,6 +575,21 @@ private extension TableViewController {
|
|||
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) {
|
||||
let alertController = UIAlertController(
|
||||
title: nil,
|
||||
|
|
|
@ -85,6 +85,15 @@ public extension ReportViewModel {
|
|||
identityContext: .preview)
|
||||
}
|
||||
|
||||
public extension MuteViewModel {
|
||||
static let preview = MuteViewModel(
|
||||
accountService: AccountService(
|
||||
account: .preview,
|
||||
mastodonAPIClient: .preview,
|
||||
contentDatabase: .preview),
|
||||
identityContext: .preview)
|
||||
}
|
||||
|
||||
public extension DomainBlocksViewModel {
|
||||
static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ public enum CollectionItemEvent {
|
|||
case attachment(AttachmentViewModel, StatusViewModel)
|
||||
case compose(inReplyTo: StatusViewModel?, redraft: Status?)
|
||||
case confirmDelete(StatusViewModel, redraft: Bool)
|
||||
case confirmMute(AccountViewModel)
|
||||
case confirmUnmute(AccountViewModel)
|
||||
case confirmBlock(AccountViewModel)
|
||||
case confirmUnblock(AccountViewModel)
|
||||
case confirmDomainBlock(AccountViewModel)
|
||||
|
|
|
@ -95,6 +95,10 @@ public extension AccountViewModel {
|
|||
ReportViewModel(accountService: accountService, identityContext: identityContext)
|
||||
}
|
||||
|
||||
func muteViewModel() -> MuteViewModel {
|
||||
MuteViewModel(accountService: accountService, identityContext: identityContext)
|
||||
}
|
||||
|
||||
func follow() {
|
||||
ignorableOutputEvent(accountService.follow())
|
||||
}
|
||||
|
@ -127,8 +131,12 @@ public extension AccountViewModel {
|
|||
ignorableOutputEvent(accountService.unblock())
|
||||
}
|
||||
|
||||
func mute() {
|
||||
ignorableOutputEvent(accountService.mute())
|
||||
func confirmMute() {
|
||||
eventsSubject.send(Just(.confirmMute(self)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func confirmUnmute() {
|
||||
eventsSubject.send(Just(.confirmUnmute(self)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func unmute() {
|
||||
|
|
|
@ -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 }
|
||||
}
|
105
Views/SwiftUI/MuteView.swift
Normal file
105
Views/SwiftUI/MuteView.swift
Normal 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
|
|
@ -52,10 +52,8 @@ struct ReportView: View {
|
|||
.navigationTitle("report.target-\(viewModel.accountName)")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button {
|
||||
Button("cancel") {
|
||||
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) {
|
||||
super.init(rootView: ReportView(viewModel: viewModel))
|
||||
|
||||
|
|
|
@ -681,13 +681,13 @@ private extension StatusView {
|
|||
secondSectionItems.append(UIAction(
|
||||
title: NSLocalizedString("account.unmute", comment: ""),
|
||||
image: UIImage(systemName: "speaker")) { _ in
|
||||
viewModel.accountViewModel.unmute()
|
||||
viewModel.accountViewModel.confirmUnmute()
|
||||
})
|
||||
} else {
|
||||
secondSectionItems.append(UIAction(
|
||||
title: NSLocalizedString("account.mute", comment: ""),
|
||||
image: UIImage(systemName: "speaker.slash")) { _ in
|
||||
viewModel.accountViewModel.mute()
|
||||
viewModel.accountViewModel.confirmMute()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue