2021-04-19 14:34:08 +02:00
//
// R e p o r t V i e w C o n t r o l l e r . s w i f t
// M a s t o d o n
//
// C r e a t e d b y i h u g o o n 2 0 2 1 / 4 / 2 0 .
//
import AVKit
import Combine
import CoreData
import CoreDataStack
import os . log
import UIKit
import TwitterTextEditor
2021-04-25 09:36:40 +02:00
import MastodonSDK
2021-04-19 14:34:08 +02:00
class ReportViewController : UIViewController , NeedsDependency {
static let kAnimationDuration : TimeInterval = 0.33
weak var context : AppContext ! { willSet { precondition ( ! isViewLoaded ) } }
weak var coordinator : SceneCoordinator ! { willSet { precondition ( ! isViewLoaded ) } }
var viewModel : ReportViewModel ! { willSet { precondition ( ! isViewLoaded ) } }
var disposeBag = Set < AnyCancellable > ( )
let didToggleSelected = PassthroughSubject < Item , Never > ( )
let comment = CurrentValueSubject < String ? , Never > ( nil )
let step1Continue = PassthroughSubject < Void , Never > ( )
let step1Skip = PassthroughSubject < Void , Never > ( )
let step2Continue = PassthroughSubject < Void , Never > ( )
let step2Skip = PassthroughSubject < Void , Never > ( )
let cancel = PassthroughSubject < Void , Never > ( )
// M A K K : - U I
2021-04-22 17:02:24 +02:00
lazy var header : ReportHeaderView = {
let view = ReportHeaderView ( )
2021-04-19 14:34:08 +02:00
view . translatesAutoresizingMaskIntoConstraints = false
return view
} ( )
2021-04-22 17:02:24 +02:00
lazy var footer : ReportFooterView = {
let view = ReportFooterView ( )
2021-04-19 14:34:08 +02:00
view . translatesAutoresizingMaskIntoConstraints = false
return view
} ( )
lazy var contentView : UIView = {
let view = UIView ( )
view . translatesAutoresizingMaskIntoConstraints = false
view . setContentHuggingPriority ( . defaultLow , for : . vertical )
2021-04-23 03:52:22 +02:00
view . backgroundColor = Asset . Colors . Background . systemElevatedBackground . color
2021-04-19 14:34:08 +02:00
return view
} ( )
lazy var stackview : UIStackView = {
let view = UIStackView ( )
view . axis = . vertical
view . alignment = . fill
view . distribution = . fill
view . translatesAutoresizingMaskIntoConstraints = false
return view
} ( )
lazy var tableView : UITableView = {
let tableView = ControlContainableTableView ( )
tableView . register ( ReportedStatusTableViewCell . self , forCellReuseIdentifier : String ( describing : ReportedStatusTableViewCell . self ) )
tableView . rowHeight = UITableView . automaticDimension
tableView . separatorStyle = . none
tableView . backgroundColor = . clear
tableView . translatesAutoresizingMaskIntoConstraints = false
tableView . delegate = self
2021-04-27 11:44:01 +02:00
tableView . prefetchDataSource = self
2021-04-25 12:28:47 +02:00
tableView . allowsMultipleSelection = true
2021-04-19 14:34:08 +02:00
return tableView
} ( )
lazy var textView : UITextView = {
let textView = UITextView ( )
textView . font = . preferredFont ( forTextStyle : . body )
textView . isScrollEnabled = false
2021-04-22 17:02:24 +02:00
textView . placeholder = L10n . Scene . Report . textPlaceholder
2021-04-19 14:34:08 +02:00
textView . backgroundColor = . clear
textView . delegate = self
2021-04-25 13:49:49 +02:00
textView . isScrollEnabled = true
textView . keyboardDismissMode = . onDrag
2021-04-19 14:34:08 +02:00
return textView
} ( )
2021-04-25 13:49:49 +02:00
lazy var bottomSpacing : UIView = {
let view = UIView ( )
view . translatesAutoresizingMaskIntoConstraints = false
return view
} ( )
var bottomConstraint : NSLayoutConstraint !
2021-04-19 14:34:08 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
setupView ( )
2021-04-25 09:36:40 +02:00
viewModel . setupDiffableDataSource (
for : tableView ,
dependency : self
)
2021-04-19 14:34:08 +02:00
bindViewModel ( )
bindActions ( )
}
// M A K R : - P r i v a t e m e t h o d s
private func setupView ( ) {
view . backgroundColor = Asset . Colors . Background . secondarySystemBackground . color
setupNavigation ( )
stackview . addArrangedSubview ( header )
stackview . addArrangedSubview ( contentView )
stackview . addArrangedSubview ( footer )
2021-04-25 13:49:49 +02:00
stackview . addArrangedSubview ( bottomSpacing )
2021-04-19 14:34:08 +02:00
contentView . addSubview ( tableView )
view . addSubview ( stackview )
NSLayoutConstraint . activate ( [
stackview . topAnchor . constraint ( equalTo : view . safeAreaLayoutGuide . topAnchor ) ,
stackview . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
stackview . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
stackview . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
tableView . topAnchor . constraint ( equalTo : contentView . topAnchor ) ,
tableView . leadingAnchor . constraint ( equalTo : contentView . leadingAnchor ) ,
tableView . bottomAnchor . constraint ( equalTo : contentView . bottomAnchor ) ,
2021-04-25 13:49:49 +02:00
tableView . trailingAnchor . constraint ( equalTo : contentView . trailingAnchor ) ,
2021-04-19 14:34:08 +02:00
] )
2021-04-25 13:49:49 +02:00
self . bottomConstraint = bottomSpacing . heightAnchor . constraint ( equalToConstant : 0 )
bottomConstraint . isActive = true
2021-04-19 14:34:08 +02:00
header . step = . one
}
private func bindActions ( ) {
footer . nextStepButton . addTarget ( self , action : #selector ( continueButtonDidClick ) , for : . touchUpInside )
footer . skipButton . addTarget ( self , action : #selector ( skipButtonDidClick ) , for : . touchUpInside )
}
private func bindViewModel ( ) {
let input = ReportViewModel . Input (
didToggleSelected : didToggleSelected . eraseToAnyPublisher ( ) ,
comment : comment . eraseToAnyPublisher ( ) ,
step1Continue : step1Continue . eraseToAnyPublisher ( ) ,
step1Skip : step1Skip . eraseToAnyPublisher ( ) ,
step2Continue : step2Continue . eraseToAnyPublisher ( ) ,
step2Skip : step2Skip . eraseToAnyPublisher ( ) ,
2021-04-25 09:36:40 +02:00
cancel : cancel . eraseToAnyPublisher ( )
2021-04-19 14:34:08 +02:00
)
let output = viewModel . transform ( input : input )
output ? . currentStep
. receive ( on : DispatchQueue . main )
. sink ( receiveValue : { [ weak self ] ( step ) in
guard step = = . two else { return }
guard let self = self else { return }
self . header . step = . two
self . footer . step = . two
self . switchToStep2Content ( )
} )
. store ( in : & disposeBag )
output ? . continueEnableSubject
. receive ( on : DispatchQueue . main )
. filter { [ weak self ] _ in
guard let step = self ? . viewModel . currentStep . value , step = = . one else { return false }
return true
}
. assign ( to : \ . nextStepButton . isEnabled , on : footer )
. store ( in : & disposeBag )
output ? . sendEnableSubject
. receive ( on : DispatchQueue . main )
. filter { [ weak self ] _ in
guard let step = self ? . viewModel . currentStep . value , step = = . two else { return false }
return true
}
. assign ( to : \ . nextStepButton . isEnabled , on : footer )
. store ( in : & disposeBag )
2021-04-25 09:36:40 +02:00
output ? . reportResult
. print ( )
2021-04-19 14:34:08 +02:00
. receive ( on : DispatchQueue . main )
2021-04-25 09:36:40 +02:00
. sink ( receiveCompletion : { _ in
} , receiveValue : { [ weak self ] data in
let ( success , error ) = data
if success {
self ? . dismiss ( animated : true , completion : nil )
} else if let error = error {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: fail to file a report : %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , error . localizedDescription )
let alertController = UIAlertController ( for : error , title : nil , preferredStyle : . alert )
let okAction = UIAlertAction ( title : " OK " , style : . default , handler : nil )
alertController . addAction ( okAction )
self ? . coordinator . present (
scene : . alertController ( alertController : alertController ) ,
from : nil ,
transition : . alertController ( animated : true , completion : nil )
)
}
} )
. store ( in : & disposeBag )
2021-04-25 13:49:49 +02:00
Publishers . CombineLatest (
KeyboardResponderService . shared . state . eraseToAnyPublisher ( ) ,
KeyboardResponderService . shared . endFrame . eraseToAnyPublisher ( )
)
. sink ( receiveValue : { [ weak self ] state , endFrame in
guard let self = self else { return }
guard state = = . dock else {
self . bottomConstraint . constant = 0.0
return
}
let contentFrame = self . view . convert ( self . view . frame , to : nil )
let padding = contentFrame . maxY - endFrame . minY
guard padding > 0 else {
self . bottomConstraint . constant = 0.0
2021-06-22 11:10:21 +02:00
UIView . animate ( withDuration : 0.33 ) {
self . view . layoutIfNeeded ( )
}
2021-04-25 13:49:49 +02:00
return
}
self . bottomConstraint . constant = padding
2021-06-22 11:10:21 +02:00
UIView . animate ( withDuration : 0.33 ) {
self . view . layoutIfNeeded ( )
}
2021-04-25 13:49:49 +02:00
} )
. store ( in : & disposeBag )
2021-04-19 14:34:08 +02:00
}
private func setupNavigation ( ) {
navigationItem . rightBarButtonItem
= UIBarButtonItem ( barButtonSystemItem : UIBarButtonItem . SystemItem . cancel ,
target : self ,
action : #selector ( doneButtonDidClick ) )
2021-06-22 14:52:30 +02:00
navigationItem . rightBarButtonItem ? . tintColor = Asset . Colors . brandBlue . color
2021-04-19 14:34:08 +02:00
// f e t c h o l d m a s t o d o n u s e r
let beReportedUser : MastodonUser ? = {
guard let domain = context . authenticationService . activeMastodonAuthenticationBox . value ? . domain else {
return nil
}
let request = MastodonUser . sortedFetchRequest
2021-04-26 09:58:49 +02:00
request . predicate = MastodonUser . predicate ( domain : domain , id : viewModel . user . id )
2021-04-19 14:34:08 +02:00
request . fetchLimit = 1
request . returnsObjectsAsFaults = false
do {
return try viewModel . statusFetchedResultsController . fetchedResultsController . managedObjectContext . fetch ( request ) . first
} catch {
assertionFailure ( error . localizedDescription )
return nil
}
} ( )
navigationItem . title = L10n . Scene . Report . title (
2021-04-22 17:02:24 +02:00
beReportedUser ? . displayNameWithFallback ? ? " "
2021-04-19 14:34:08 +02:00
)
}
private func switchToStep2Content ( ) {
self . contentView . addSubview ( self . textView )
self . textView . translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint . activate ( [
self . textView . topAnchor . constraint ( equalTo : self . contentView . topAnchor ) ,
self . textView . leadingAnchor . constraint (
equalTo : self . contentView . readableContentGuide . leadingAnchor ,
constant : ReportView . horizontalMargin
) ,
self . textView . bottomAnchor . constraint ( equalTo : self . contentView . bottomAnchor ) ,
2021-04-25 13:49:49 +02:00
self . contentView . trailingAnchor . constraint (
equalTo : self . textView . trailingAnchor ,
constant : ReportView . horizontalMargin
2021-04-19 14:34:08 +02:00
) ,
] )
self . textView . layoutIfNeeded ( )
UIView . transition (
with : contentView ,
duration : ReportViewController . kAnimationDuration ,
options : UIView . AnimationOptions . transitionCrossDissolve ) {
[ weak self ] in
guard let self = self else { return }
self . contentView . addSubview ( self . textView )
self . tableView . isHidden = true
} completion : { ( _ ) in
}
}
// M a r k : - A c t i o n s
@objc func doneButtonDidClick ( ) {
dismiss ( animated : true , completion : nil )
}
@objc func continueButtonDidClick ( ) {
if viewModel . currentStep . value = = . one {
step1Continue . send ( )
} else {
step2Continue . send ( )
}
}
@objc func skipButtonDidClick ( ) {
if viewModel . currentStep . value = = . one {
step1Skip . send ( )
} else {
step2Skip . send ( )
}
}
}
2021-04-27 11:44:01 +02:00
// MARK: - U I T a b l e V i e w D e l e g a t e
2021-04-19 14:34:08 +02:00
extension ReportViewController : UITableViewDelegate {
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
guard let item = viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) else {
return
}
2021-04-25 12:28:47 +02:00
didToggleSelected . send ( item )
}
func tableView ( _ tableView : UITableView , didDeselectRowAt indexPath : IndexPath ) {
guard let item = viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) else {
return
}
2021-04-19 14:34:08 +02:00
didToggleSelected . send ( item )
}
}
2021-04-27 11:44:01 +02:00
// MARK: - U I T a b l e V i e w D a t a S o u r c e P r e f e t c h i n g
extension ReportViewController : UITableViewDataSourcePrefetching {
func tableView ( _ tableView : UITableView , prefetchRowsAt indexPaths : [ IndexPath ] ) {
viewModel . prefetchData ( prefetchRowsAt : indexPaths )
}
}
// MARK: - U I T e x t V i e w D e l e g a t e
2021-04-19 14:34:08 +02:00
extension ReportViewController : UITextViewDelegate {
func textViewDidChange ( _ textView : UITextView ) {
self . comment . send ( textView . text )
}
}