2021-02-23 09:45:00 +01:00
//
// P r o f i l e 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 M a i n a s u K C i r n o o n 2 0 2 1 - 2 - 2 3 .
//
2021-04-01 08:39:15 +02:00
import os . log
2021-02-23 09:45:00 +01:00
import UIKit
2021-04-01 08:39:15 +02:00
import Combine
2021-07-23 13:10:27 +02:00
import MastodonMeta
import MetaTextKit
2022-01-27 14:23:39 +01:00
import MastodonAsset
import MastodonLocalization
import MastodonUI
import Tabman
import CoreDataStack
protocol ProfileViewModelEditable {
func isEdited ( ) -> Bool
}
2021-02-23 09:45:00 +01:00
2021-04-28 14:10:17 +02:00
final class ProfileViewController : UIViewController , NeedsDependency , MediaPreviewableViewController {
2021-02-23 09:45:00 +01:00
2022-02-18 11:55:26 +01:00
public static let containerViewMarginForRegularHorizontalSizeClass : CGFloat = 64
public static let containerViewMarginForCompactHorizontalSizeClass : CGFloat = 16
2022-01-27 14:23:39 +01:00
let logger = Logger ( subsystem : " ProfileViewController " , category : " ViewController " )
2021-02-23 09:45:00 +01:00
weak var context : AppContext ! { willSet { precondition ( ! isViewLoaded ) } }
weak var coordinator : SceneCoordinator ! { willSet { precondition ( ! isViewLoaded ) } }
2021-04-01 08:39:15 +02:00
var disposeBag = Set < AnyCancellable > ( )
var viewModel : ProfileViewModel !
2021-04-28 14:10:17 +02:00
let mediaPreviewTransitionController = MediaPreviewTransitionController ( )
2021-04-09 11:31:43 +02:00
private ( set ) lazy var cancelEditingBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( title : L10n . Common . Controls . Actions . cancel , style : . plain , target : self , action : #selector ( ProfileViewController . cancelEditingBarButtonItemPressed ( _ : ) ) )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2021-04-07 08:24:28 +02:00
private ( set ) lazy var settingBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " gear " ) , style : . plain , target : self , action : #selector ( ProfileViewController . settingBarButtonItemPressed ( _ : ) ) )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
private ( set ) lazy var shareBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " square.and.arrow.up " ) , style : . plain , target : self , action : #selector ( ProfileViewController . shareBarButtonItemPressed ( _ : ) ) )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
private ( set ) lazy var favoriteBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " star " ) , style : . plain , target : self , action : #selector ( ProfileViewController . favoriteBarButtonItemPressed ( _ : ) ) )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2021-04-02 12:13:45 +02:00
private ( set ) lazy var replyBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " arrowshape.turn.up.left " ) , style : . plain , target : self , action : #selector ( ProfileViewController . replyBarButtonItemPressed ( _ : ) ) )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
let moreMenuBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " ellipsis.circle " ) , style : . plain , target : nil , action : nil )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2021-04-01 08:39:15 +02:00
let refreshControl : UIRefreshControl = {
let refreshControl = UIRefreshControl ( )
2021-06-22 13:33:36 +02:00
refreshControl . tintColor = . white
2021-04-01 08:39:15 +02:00
return refreshControl
} ( )
let containerScrollView : UIScrollView = {
let scrollView = UIScrollView ( )
scrollView . scrollsToTop = false
scrollView . showsVerticalScrollIndicator = false
scrollView . preservesSuperviewLayoutMargins = true
scrollView . delaysContentTouches = false
return scrollView
} ( )
let overlayScrollView : UIScrollView = {
let scrollView = UIScrollView ( )
scrollView . showsVerticalScrollIndicator = false
scrollView . backgroundColor = . clear
scrollView . delaysContentTouches = false
return scrollView
} ( )
private ( set ) lazy var profileSegmentedViewController = ProfileSegmentedViewController ( )
2021-04-09 11:31:43 +02:00
private ( set ) lazy var profileHeaderViewController : ProfileHeaderViewController = {
let viewController = ProfileHeaderViewController ( )
viewController . viewModel = ProfileHeaderViewModel ( context : context )
return viewController
} ( )
2021-04-01 08:39:15 +02:00
private var profileBannerImageViewLayoutConstraint : NSLayoutConstraint !
private var contentOffsets : [ Int : CGFloat ] = [ : ]
var currentPostTimelineTableViewContentSizeObservation : NSKeyValueObservation ?
2021-04-09 13:44:48 +02:00
// t i t l e v i e w n e s t e d i n h e a d e r
var titleView : DoubleTitleLabelNavigationBarTitleView {
profileHeaderViewController . titleView
}
2021-04-01 08:39:15 +02:00
deinit {
2021-09-30 08:19:15 +02:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2021-04-01 08:39:15 +02:00
}
}
extension ProfileViewController {
func observeTableViewContentSize ( scrollView : UIScrollView ) -> NSKeyValueObservation {
updateOverlayScrollViewContentSize ( scrollView : scrollView )
return scrollView . observe ( \ . contentSize , options : . new ) { scrollView , change in
self . updateOverlayScrollViewContentSize ( scrollView : scrollView )
}
}
func updateOverlayScrollViewContentSize ( scrollView : UIScrollView ) {
let bottomPageHeight = max ( scrollView . contentSize . height , self . containerScrollView . frame . height - ProfileHeaderViewController . headerMinHeight - self . containerScrollView . safeAreaInsets . bottom )
let headerViewHeight : CGFloat = profileHeaderViewController . view . frame . height
let contentSize = CGSize (
width : self . containerScrollView . contentSize . width ,
height : bottomPageHeight + headerViewHeight
)
self . overlayScrollView . contentSize = contentSize
2021-04-02 12:13:45 +02:00
// o s _ l o g ( . i n f o , l o g : . d e b u g , " % { p u b l i c } s [ % { p u b l i c } l d ] , % { p u b l i c } s : c o n t e n t S i z e : % s " , ( ( # f i l e a s N S S t r i n g ) . l a s t P a t h C o m p o n e n t ) , # l i n e , # f u n c t i o n , c o n t e n t S i z e . d e b u g D e s c r i p t i o n )
2021-04-01 08:39:15 +02:00
}
2021-02-23 09:45:00 +01:00
}
extension ProfileViewController {
2021-04-01 08:39:15 +02:00
override var preferredStatusBarStyle : UIStatusBarStyle {
2021-04-02 12:13:45 +02:00
return . lightContent
2021-04-01 08:39:15 +02:00
}
override func viewSafeAreaInsetsDidChange ( ) {
super . viewSafeAreaInsetsDidChange ( )
profileHeaderViewController . updateHeaderContainerSafeAreaInset ( view . safeAreaInsets )
}
2021-04-02 12:13:45 +02:00
override var isViewLoaded : Bool {
return super . isViewLoaded
}
2021-02-23 09:45:00 +01:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2021-07-06 12:00:39 +02:00
view . backgroundColor = ThemeService . shared . currentTheme . value . secondarySystemBackgroundColor
2021-07-05 10:07:17 +02:00
ThemeService . shared . currentTheme
2022-01-27 14:23:39 +01:00
. receive ( on : DispatchQueue . main )
2021-07-05 10:07:17 +02:00
. sink { [ weak self ] theme in
guard let self = self else { return }
self . view . backgroundColor = theme . secondarySystemBackgroundColor
}
. store ( in : & disposeBag )
2021-04-02 12:13:45 +02:00
2021-04-01 08:39:15 +02:00
let barAppearance = UINavigationBarAppearance ( )
2021-07-07 13:07:47 +02:00
if isModal {
barAppearance . configureWithDefaultBackground ( )
} else {
barAppearance . configureWithTransparentBackground ( )
}
2021-04-01 08:39:15 +02:00
navigationItem . standardAppearance = barAppearance
navigationItem . compactAppearance = barAppearance
navigationItem . scrollEdgeAppearance = barAppearance
2021-04-09 13:44:48 +02:00
navigationItem . titleView = titleView
2021-04-09 11:31:43 +02:00
let editingAndUpdatingPublisher = Publishers . CombineLatest (
viewModel . isEditing . eraseToAnyPublisher ( ) ,
viewModel . isUpdating . eraseToAnyPublisher ( )
)
2021-04-09 11:46:20 +02:00
// n o t e : n o t a d d . s h a r e ( ) h e r e
let barButtonItemHiddenPublisher = Publishers . CombineLatest3 (
viewModel . isMeBarButtonItemsHidden . eraseToAnyPublisher ( ) ,
viewModel . isReplyBarButtonItemHidden . eraseToAnyPublisher ( ) ,
viewModel . isMoreMenuBarButtonItemHidden . eraseToAnyPublisher ( )
)
2021-04-09 11:31:43 +02:00
editingAndUpdatingPublisher
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isEditing , isUpdating in
guard let self = self else { return }
self . cancelEditingBarButtonItem . isEnabled = ! isUpdating
}
. store ( in : & disposeBag )
2021-04-29 11:13:13 +02:00
Publishers . CombineLatest4 (
2021-04-09 11:31:43 +02:00
viewModel . suspended . eraseToAnyPublisher ( ) ,
2021-04-29 11:13:13 +02:00
profileHeaderViewController . viewModel . isTitleViewDisplaying . eraseToAnyPublisher ( ) ,
2021-04-09 11:31:43 +02:00
editingAndUpdatingPublisher . eraseToAnyPublisher ( ) ,
barButtonItemHiddenPublisher . eraseToAnyPublisher ( )
)
2021-04-02 12:13:45 +02:00
. receive ( on : DispatchQueue . main )
2021-04-29 11:13:13 +02:00
. sink { [ weak self ] suspended , isTitleViewDisplaying , tuple1 , tuple2 in
2021-04-02 12:13:45 +02:00
guard let self = self else { return }
2021-04-09 11:31:43 +02:00
let ( isEditing , _ ) = tuple1
let ( isMeBarButtonItemsHidden , isReplyBarButtonItemHidden , isMoreMenuBarButtonItemHidden ) = tuple2
2021-04-02 12:13:45 +02:00
var items : [ UIBarButtonItem ] = [ ]
2021-04-07 08:24:28 +02:00
defer {
self . navigationItem . rightBarButtonItems = ! items . isEmpty ? items : nil
}
2021-04-08 10:53:32 +02:00
guard ! suspended else {
return
}
2021-04-09 11:31:43 +02:00
guard ! isEditing else {
items . append ( self . cancelEditingBarButtonItem )
return
}
2021-04-29 11:13:13 +02:00
guard ! isTitleViewDisplaying else {
return
}
2021-04-07 08:24:28 +02:00
guard isMeBarButtonItemsHidden else {
items . append ( self . settingBarButtonItem )
items . append ( self . shareBarButtonItem )
items . append ( self . favoriteBarButtonItem )
return
}
2021-04-02 12:13:45 +02:00
if ! isMoreMenuBarButtonItemHidden {
items . append ( self . moreMenuBarButtonItem )
}
2022-01-27 14:23:39 +01:00
if ! isReplyBarButtonItemHidden {
items . append ( self . replyBarButtonItem )
}
2021-04-02 12:13:45 +02:00
}
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
overlayScrollView . refreshControl = refreshControl
refreshControl . addTarget ( self , action : #selector ( ProfileViewController . refreshControlValueChanged ( _ : ) ) , for : . valueChanged )
2021-07-07 06:12:04 +02:00
let postsUserTimelineViewModel = UserTimelineViewModel ( context : context , domain : viewModel . domain . value , userID : viewModel . userID . value , queryFilter : UserTimelineViewModel . QueryFilter ( excludeReplies : true ) )
2021-04-06 10:43:08 +02:00
bind ( userTimelineViewModel : postsUserTimelineViewModel )
2021-07-07 06:12:04 +02:00
let repliesUserTimelineViewModel = UserTimelineViewModel ( context : context , domain : viewModel . domain . value , userID : viewModel . userID . value , queryFilter : UserTimelineViewModel . QueryFilter ( excludeReplies : false ) )
2021-04-06 10:43:08 +02:00
bind ( userTimelineViewModel : repliesUserTimelineViewModel )
2021-04-01 08:39:15 +02:00
let mediaUserTimelineViewModel = UserTimelineViewModel ( context : context , domain : viewModel . domain . value , userID : viewModel . userID . value , queryFilter : UserTimelineViewModel . QueryFilter ( onlyMedia : true ) )
2021-04-06 10:43:08 +02:00
bind ( userTimelineViewModel : mediaUserTimelineViewModel )
2021-04-01 08:39:15 +02:00
2022-01-27 14:23:39 +01:00
let profileAboutViewModel = ProfileAboutViewModel ( context : context )
2021-04-01 08:39:15 +02:00
profileSegmentedViewController . pagingViewController . viewModel = {
let profilePagingViewModel = ProfilePagingViewModel (
postsUserTimelineViewModel : postsUserTimelineViewModel ,
repliesUserTimelineViewModel : repliesUserTimelineViewModel ,
2022-01-27 14:23:39 +01:00
mediaUserTimelineViewModel : mediaUserTimelineViewModel ,
profileAboutViewModel : profileAboutViewModel
2021-04-01 08:39:15 +02:00
)
profilePagingViewModel . viewControllers . forEach { viewController in
if let viewController = viewController as ? NeedsDependency {
viewController . context = context
viewController . coordinator = coordinator
}
}
return profilePagingViewModel
} ( )
2022-01-27 14:23:39 +01:00
profileSegmentedViewController . pagingViewController . addBar (
profileHeaderViewController . buttonBar ,
dataSource : profileSegmentedViewController . pagingViewController . viewModel ,
at : . custom ( view : profileHeaderViewController . view , layout : { buttonBar in
buttonBar . translatesAutoresizingMaskIntoConstraints = false
self . profileHeaderViewController . view . addSubview ( buttonBar )
NSLayoutConstraint . activate ( [
buttonBar . topAnchor . constraint ( equalTo : self . profileHeaderViewController . profileHeaderView . bottomAnchor ) ,
2022-02-18 11:55:26 +01:00
buttonBar . leadingAnchor . constraint ( equalTo : self . profileHeaderViewController . view . leadingAnchor ) ,
buttonBar . trailingAnchor . constraint ( equalTo : self . profileHeaderViewController . view . trailingAnchor ) ,
2022-01-27 14:23:39 +01:00
buttonBar . bottomAnchor . constraint ( equalTo : self . profileHeaderViewController . view . bottomAnchor ) ,
buttonBar . heightAnchor . constraint ( equalToConstant : ProfileHeaderViewController . segmentedControlHeight ) . priority ( . required - 1 ) ,
] )
} )
)
2022-02-18 11:55:26 +01:00
updateBarButtonInsets ( )
2021-04-01 08:39:15 +02:00
overlayScrollView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( overlayScrollView )
NSLayoutConstraint . activate ( [
overlayScrollView . frameLayoutGuide . topAnchor . constraint ( equalTo : view . topAnchor ) ,
overlayScrollView . frameLayoutGuide . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
view . trailingAnchor . constraint ( equalTo : overlayScrollView . frameLayoutGuide . trailingAnchor ) ,
view . bottomAnchor . constraint ( equalTo : overlayScrollView . frameLayoutGuide . bottomAnchor ) ,
overlayScrollView . contentLayoutGuide . widthAnchor . constraint ( equalTo : view . widthAnchor ) ,
] )
containerScrollView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( containerScrollView )
NSLayoutConstraint . activate ( [
containerScrollView . frameLayoutGuide . topAnchor . constraint ( equalTo : view . topAnchor ) ,
containerScrollView . frameLayoutGuide . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
view . trailingAnchor . constraint ( equalTo : containerScrollView . frameLayoutGuide . trailingAnchor ) ,
view . bottomAnchor . constraint ( equalTo : containerScrollView . frameLayoutGuide . bottomAnchor ) ,
containerScrollView . contentLayoutGuide . widthAnchor . constraint ( equalTo : view . widthAnchor ) ,
] )
// a d d s e g m e n t e d l i s t
addChild ( profileSegmentedViewController )
profileSegmentedViewController . view . translatesAutoresizingMaskIntoConstraints = false
containerScrollView . addSubview ( profileSegmentedViewController . view )
profileSegmentedViewController . didMove ( toParent : self )
NSLayoutConstraint . activate ( [
profileSegmentedViewController . view . leadingAnchor . constraint ( equalTo : containerScrollView . contentLayoutGuide . leadingAnchor ) ,
profileSegmentedViewController . view . trailingAnchor . constraint ( equalTo : containerScrollView . contentLayoutGuide . trailingAnchor ) ,
profileSegmentedViewController . view . bottomAnchor . constraint ( equalTo : containerScrollView . contentLayoutGuide . bottomAnchor ) ,
profileSegmentedViewController . view . heightAnchor . constraint ( equalTo : containerScrollView . frameLayoutGuide . heightAnchor ) ,
] )
// a d d h e a d e r
addChild ( profileHeaderViewController )
profileHeaderViewController . view . translatesAutoresizingMaskIntoConstraints = false
containerScrollView . addSubview ( profileHeaderViewController . view )
profileHeaderViewController . didMove ( toParent : self )
NSLayoutConstraint . activate ( [
profileHeaderViewController . view . topAnchor . constraint ( equalTo : containerScrollView . topAnchor ) ,
profileHeaderViewController . view . leadingAnchor . constraint ( equalTo : containerScrollView . contentLayoutGuide . leadingAnchor ) ,
containerScrollView . contentLayoutGuide . trailingAnchor . constraint ( equalTo : profileHeaderViewController . view . trailingAnchor ) ,
profileSegmentedViewController . view . topAnchor . constraint ( equalTo : profileHeaderViewController . view . bottomAnchor ) ,
] )
containerScrollView . addGestureRecognizer ( overlayScrollView . panGestureRecognizer )
overlayScrollView . layer . zPosition = . greatestFiniteMagnitude // m a k e v i s i o n t o p - m o s t
overlayScrollView . delegate = self
profileHeaderViewController . delegate = self
2022-01-27 14:23:39 +01:00
profileSegmentedViewController . pagingViewController . viewModel . profileAboutViewController . delegate = self
2021-04-01 08:39:15 +02:00
profileSegmentedViewController . pagingViewController . pagingDelegate = self
// b i n d v i e w m o d e l
2022-01-27 14:23:39 +01:00
bindProfile (
headerViewModel : profileHeaderViewController . viewModel ,
aboutViewModel : profileAboutViewModel
)
bindTitleView ( )
bindHeader ( )
bindProfileRelationship ( )
bindProfileDashboard ( )
viewModel . needsPagingEnabled
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] needsPaingEnabled in
guard let self = self else { return }
self . profileSegmentedViewController . pagingViewController . isScrollEnabled = needsPaingEnabled
}
. store ( in : & disposeBag )
profileHeaderViewController . profileHeaderView . delegate = self
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
// s e t b a c k b u t t o n t i n t c o l o r i n S c e n e C o o r d i n a t o r . p r e s e n t ( s c e n e : f r o m : t r a n s i t i o n : )
// f o r c e l a y o u t t o m a k e b a n n e r i m a g e t w e a k t a k e e f f e c t
view . layoutIfNeeded ( )
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
viewModel . viewDidAppear . send ( )
// s e t o v e r l a y s c r o l l v i e w i n i t i a l c o n t e n t s i z e
2022-02-14 07:55:00 +01:00
guard let currentViewController = profileSegmentedViewController . pagingViewController . currentViewController as ? ScrollViewContainer ,
let scrollView = currentViewController . scrollView
else { return }
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize ( scrollView : scrollView )
scrollView . panGestureRecognizer . require ( toFail : overlayScrollView . panGestureRecognizer )
2022-01-27 14:23:39 +01:00
}
override func viewDidDisappear ( _ animated : Bool ) {
super . viewDidDisappear ( animated )
currentPostTimelineTableViewContentSizeObservation = nil
}
2022-02-18 11:55:26 +01:00
override func traitCollectionDidChange ( _ previousTraitCollection : UITraitCollection ? ) {
super . traitCollectionDidChange ( previousTraitCollection )
updateBarButtonInsets ( )
}
}
extension ProfileViewController {
private func updateBarButtonInsets ( ) {
let margin : CGFloat = {
switch traitCollection . userInterfaceIdiom {
case . phone :
return ProfileViewController . containerViewMarginForCompactHorizontalSizeClass
default :
return traitCollection . horizontalSizeClass = = . regular ?
ProfileViewController . containerViewMarginForRegularHorizontalSizeClass :
ProfileViewController . containerViewMarginForCompactHorizontalSizeClass
}
} ( )
profileHeaderViewController . buttonBar . layout . contentInset . left = margin
profileHeaderViewController . buttonBar . layout . contentInset . right = margin
}
2022-01-27 14:23:39 +01:00
}
extension ProfileViewController {
private func bind ( userTimelineViewModel : UserTimelineViewModel ) {
viewModel . domain . assign ( to : \ . domain , on : userTimelineViewModel ) . store ( in : & disposeBag )
viewModel . userID . assign ( to : \ . userID , on : userTimelineViewModel ) . store ( in : & disposeBag )
viewModel . isBlocking . assign ( to : \ . value , on : userTimelineViewModel . isBlocking ) . store ( in : & disposeBag )
viewModel . isBlockedBy . assign ( to : \ . value , on : userTimelineViewModel . isBlockedBy ) . store ( in : & disposeBag )
viewModel . suspended . assign ( to : \ . value , on : userTimelineViewModel . isSuspended ) . store ( in : & disposeBag )
viewModel . name . assign ( to : \ . value , on : userTimelineViewModel . userDisplayName ) . store ( in : & disposeBag )
}
private func bindProfile (
headerViewModel : ProfileHeaderViewModel ,
aboutViewModel : ProfileAboutViewModel
) {
// h e a d e r
viewModel . avatarImageURL
. receive ( on : DispatchQueue . main )
. assign ( to : \ . avatarImageURL , on : headerViewModel . displayProfileInfo )
. store ( in : & disposeBag )
viewModel . name
. map { $0 ? ? " " }
. receive ( on : DispatchQueue . main )
. assign ( to : \ . name , on : headerViewModel . displayProfileInfo )
. store ( in : & disposeBag )
viewModel . bioDescription
. receive ( on : DispatchQueue . main )
. assign ( to : \ . note , on : headerViewModel . displayProfileInfo )
. store ( in : & disposeBag )
// a b o u t
Publishers . CombineLatest (
viewModel . fields . removeDuplicates ( ) ,
viewModel . emojiMeta . removeDuplicates ( )
)
. map { fields , emojiMeta -> [ ProfileFieldItem . FieldValue ] in
fields . map { ProfileFieldItem . FieldValue ( name : $0 . name , value : $0 . value , emojiMeta : emojiMeta ) }
}
. receive ( on : DispatchQueue . main )
. assign ( to : \ . fields , on : aboutViewModel . displayProfileInfo )
. store ( in : & disposeBag )
// c o m m o n
viewModel . accountForEdit
. assign ( to : \ . accountForEdit , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . accountForEdit
. assign ( to : \ . accountForEdit , on : aboutViewModel )
. store ( in : & disposeBag )
viewModel . emojiMeta
. receive ( on : DispatchQueue . main )
. assign ( to : \ . emojiMeta , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . emojiMeta
. receive ( on : DispatchQueue . main )
. assign ( to : \ . emojiMeta , on : aboutViewModel )
. store ( in : & disposeBag )
viewModel . isEditing
. assign ( to : \ . isEditing , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . isEditing
. assign ( to : \ . isEditing , on : aboutViewModel )
. store ( in : & disposeBag )
}
private func bindTitleView ( ) {
2021-06-29 13:27:40 +02:00
Publishers . CombineLatest3 (
viewModel . name ,
2021-07-23 13:10:27 +02:00
viewModel . emojiMeta ,
2021-06-29 13:27:40 +02:00
viewModel . statusesCount
2021-04-09 13:44:48 +02:00
)
. receive ( on : DispatchQueue . main )
2021-07-23 13:10:27 +02:00
. sink { [ weak self ] name , emojiMeta , statusesCount in
2021-04-09 13:44:48 +02:00
guard let self = self else { return }
guard let title = name , let statusesCount = statusesCount ,
let formattedStatusCount = MastodonMetricFormatter ( ) . string ( from : statusesCount ) else {
self . titleView . isHidden = true
return
}
self . titleView . isHidden = false
2021-07-23 13:10:27 +02:00
let subtitle = L10n . Plural . Count . MetricFormatted . post ( formattedStatusCount , statusesCount )
let mastodonContent = MastodonContent ( content : title , emojis : emojiMeta )
do {
let metaContent = try MastodonMetaContent . convert ( document : mastodonContent )
self . titleView . update ( titleMetaContent : metaContent , subtitle : subtitle )
} catch {
}
2021-04-09 13:44:48 +02:00
}
. store ( in : & disposeBag )
2021-04-02 12:13:45 +02:00
viewModel . name
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] name in
guard let self = self else { return }
2021-04-06 10:43:08 +02:00
self . navigationItem . title = name
2021-04-02 12:13:45 +02:00
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
}
private func bindHeader ( ) {
// h e a e r U I
2021-04-01 08:39:15 +02:00
Publishers . CombineLatest (
viewModel . bannerImageURL . eraseToAnyPublisher ( ) ,
viewModel . viewDidAppear . eraseToAnyPublisher ( )
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] bannerImageURL , _ in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
self . profileHeaderViewController . profileHeaderView . bannerImageView . af . cancelImageRequest ( )
let placeholder = UIImage . placeholder ( color : ProfileHeaderView . bannerImageViewPlaceholderColor )
2021-04-01 08:39:15 +02:00
guard let bannerImageURL = bannerImageURL else {
2021-04-02 12:13:45 +02:00
self . profileHeaderViewController . profileHeaderView . bannerImageView . image = placeholder
2021-04-01 08:39:15 +02:00
return
}
2021-04-02 12:13:45 +02:00
self . profileHeaderViewController . profileHeaderView . bannerImageView . af . setImage (
2021-04-01 08:39:15 +02:00
withURL : bannerImageURL ,
placeholderImage : placeholder ,
imageTransition : . crossDissolve ( 0.3 ) ,
runImageTransitionIfCached : false ,
completion : { [ weak self ] response in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
guard let image = response . value else { return }
guard image . size . width > 1 && image . size . height > 1 else {
// r e s t o r e t o p l a c e h o l d e r w h e n i m a g e i n v a l i d
self . profileHeaderViewController . profileHeaderView . bannerImageView . image = placeholder
return
2021-04-01 08:39:15 +02:00
}
}
)
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
viewModel . username
. map { username in username . flatMap { " @ " + $0 } ? ? " " }
2021-04-09 11:31:43 +02:00
. receive ( on : DispatchQueue . main )
2022-01-27 14:23:39 +01:00
. assign ( to : \ . text , on : profileHeaderViewController . profileHeaderView . usernameLabel )
2021-04-09 11:31:43 +02:00
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
viewModel . isEditing
2021-04-01 08:39:15 +02:00
. receive ( on : DispatchQueue . main )
2022-01-27 14:23:39 +01:00
. sink { [ weak self ] isEditing in
guard let self = self else { return }
// s e t f i r s t r e s p o n d e r f o r k e y c o m m a n d
if ! isEditing {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 1 ) {
self . profileSegmentedViewController . pagingViewController . becomeFirstResponder ( )
}
}
// d i s m i s s k e y b o a r d i f n e e d s
if ! isEditing { self . view . endEditing ( true ) }
self . profileHeaderViewController . buttonBar . isUserInteractionEnabled = ! isEditing
if isEditing {
// s c r o l l t o A b o u t p a g e
self . profileSegmentedViewController . pagingViewController . scrollToPage (
. last ,
animated : true ,
completion : nil
)
self . profileSegmentedViewController . pagingViewController . isScrollEnabled = false
} else {
self . profileSegmentedViewController . pagingViewController . isScrollEnabled = true
}
let animator = UIViewPropertyAnimator ( duration : 0.33 , curve : . easeInOut )
animator . addAnimations {
self . profileHeaderViewController . profileHeaderView . statusDashboardView . alpha = isEditing ? 0.2 : 1.0
}
animator . startAnimation ( )
2021-05-27 07:56:55 +02:00
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
viewModel . needsImageOverlayBlurred
2021-04-01 08:39:15 +02:00
. receive ( on : DispatchQueue . main )
2022-01-27 14:23:39 +01:00
. sink { [ weak self ] needsImageOverlayBlurred in
guard let self = self else { return }
UIView . animate ( withDuration : 0.33 ) {
let bannerEffect : UIVisualEffect ? = needsImageOverlayBlurred ? ProfileHeaderView . bannerImageViewOverlayBlurEffect : nil
self . profileHeaderViewController . profileHeaderView . bannerImageViewOverlayVisualEffectView . effect = bannerEffect
let avatarEffect : UIVisualEffect ? = needsImageOverlayBlurred ? ProfileHeaderView . avatarImageViewOverlayBlurEffect : nil
self . profileHeaderViewController . profileHeaderView . avatarImageViewOverlayVisualEffectView . effect = avatarEffect
}
}
2021-04-01 08:39:15 +02:00
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
}
private func bindProfileRelationship ( ) {
2021-04-30 08:55:02 +02:00
Publishers . CombineLatest (
2022-02-10 12:30:41 +01:00
viewModel . $ user ,
2022-01-27 14:23:39 +01:00
viewModel . relationshipActionOptionSet
2021-04-30 08:55:02 +02:00
)
2022-01-27 14:23:39 +01:00
. asyncMap { [ weak self ] user , relationshipSet -> UIMenu ? in
guard let self = self else { return nil }
guard let user = user else {
return nil
2021-04-02 12:13:45 +02:00
}
2022-01-27 14:23:39 +01:00
let name = user . displayNameWithFallback
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
let menu = MastodonMenu . setupMenu (
actions : [
. muteUser ( . init ( name : name , isMuting : self . viewModel . isMuting . value ) ) ,
. blockUser ( . init ( name : name , isBlocking : self . viewModel . isBlocking . value ) ) ,
. reportUser ( . init ( name : name ) ) ,
. shareUser ( . init ( name : name ) ) ,
] ,
delegate : self
)
return menu
}
. sink { [ weak self ] completion in
guard let self = self else { return }
switch completion {
case . failure ( let error ) :
2021-05-06 12:19:24 +02:00
self . moreMenuBarButtonItem . menu = nil
2022-01-27 14:23:39 +01:00
case . finished :
break
2021-05-06 12:19:24 +02:00
}
2022-01-27 14:23:39 +01:00
} receiveValue : { [ weak self ] menu in
guard let self = self else { return }
self . moreMenuBarButtonItem . menu = menu
2021-04-30 08:55:02 +02:00
}
. store ( in : & disposeBag )
2021-04-02 12:13:45 +02:00
viewModel . isRelationshipActionButtonHidden
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isHidden in
guard let self = self else { return }
2022-02-15 10:13:02 +01:00
self . profileHeaderViewController . profileHeaderView . relationshipActionButtonShadowContainer . isHidden = isHidden
2021-04-02 12:13:45 +02:00
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
2021-04-09 11:31:43 +02:00
Publishers . CombineLatest3 (
2021-04-02 12:13:45 +02:00
viewModel . relationshipActionOptionSet . eraseToAnyPublisher ( ) ,
2021-04-09 11:31:43 +02:00
viewModel . isEditing . eraseToAnyPublisher ( ) ,
viewModel . isUpdating . eraseToAnyPublisher ( )
2021-04-02 12:13:45 +02:00
)
. receive ( on : DispatchQueue . main )
2021-04-09 11:31:43 +02:00
. sink { [ weak self ] relationshipActionSet , isEditing , isUpdating in
2021-04-02 12:13:45 +02:00
guard let self = self else { return }
let friendshipButton = self . profileHeaderViewController . profileHeaderView . relationshipActionButton
if relationshipActionSet . contains ( . edit ) {
2021-04-09 11:31:43 +02:00
// c h e c k . e d i t s t a t e a n d s e t . e d i t i n g w h e n i s E d i t i n g
friendshipButton . configure ( actionOptionSet : isUpdating ? . updating : ( isEditing ? . editing : . edit ) )
2021-06-24 13:20:41 +02:00
self . profileHeaderViewController . profileHeaderView . configure ( state : isEditing ? . editing : . normal )
2021-04-02 12:13:45 +02:00
} else {
friendshipButton . configure ( actionOptionSet : relationshipActionSet )
}
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
2021-04-08 10:53:32 +02:00
Publishers . CombineLatest3 (
viewModel . isBlocking . eraseToAnyPublisher ( ) ,
viewModel . isBlockedBy . eraseToAnyPublisher ( ) ,
viewModel . suspended . eraseToAnyPublisher ( )
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isBlocking , isBlockedBy , suspended in
guard let self = self else { return }
let isNeedSetHidden = isBlocking || isBlockedBy || suspended
2021-04-09 11:31:43 +02:00
self . profileHeaderViewController . viewModel . needsSetupBottomShadow . value = ! isNeedSetHidden
2021-04-08 10:53:32 +02:00
self . profileHeaderViewController . profileHeaderView . bioContainerView . isHidden = isNeedSetHidden
2021-07-06 11:53:01 +02:00
self . profileHeaderViewController . viewModel . needsFiledCollectionViewHidden . value = isNeedSetHidden
2022-01-27 14:23:39 +01:00
self . profileHeaderViewController . buttonBar . isUserInteractionEnabled = ! isNeedSetHidden
2021-04-08 10:53:32 +02:00
self . viewModel . needsPagePinToTop . value = isNeedSetHidden
}
. store ( in : & disposeBag )
2022-01-27 14:23:39 +01:00
} // e n d f u n c b i n d P r o f i l e R e l a t i o n s h i p
private func bindProfileDashboard ( ) {
2021-04-01 08:39:15 +02:00
viewModel . statusesCount
2021-10-29 12:56:58 +02:00
. receive ( on : DispatchQueue . main )
2021-04-01 08:39:15 +02:00
. sink { [ weak self ] count in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
let text = count . flatMap { MastodonMetricFormatter ( ) . string ( from : $0 ) } ? ? " - "
self . profileHeaderViewController . profileHeaderView . statusDashboardView . postDashboardMeterView . numberLabel . text = text
2021-05-13 08:27:57 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . postDashboardMeterView . isAccessibilityElement = true
2021-08-09 13:44:04 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . postDashboardMeterView . accessibilityLabel = L10n . Plural . Count . post ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
viewModel . followingCount
2021-10-29 12:56:58 +02:00
. receive ( on : DispatchQueue . main )
2021-04-01 08:39:15 +02:00
. sink { [ weak self ] count in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
let text = count . flatMap { MastodonMetricFormatter ( ) . string ( from : $0 ) } ? ? " - "
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followingDashboardMeterView . numberLabel . text = text
2021-05-13 08:27:57 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followingDashboardMeterView . isAccessibilityElement = true
2021-08-09 13:44:04 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followingDashboardMeterView . accessibilityLabel = L10n . Plural . Count . following ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
viewModel . followersCount
2021-10-29 12:56:58 +02:00
. receive ( on : DispatchQueue . main )
2021-04-01 08:39:15 +02:00
. sink { [ weak self ] count in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
let text = count . flatMap { MastodonMetricFormatter ( ) . string ( from : $0 ) } ? ? " - "
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followersDashboardMeterView . numberLabel . text = text
2021-05-13 08:27:57 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followersDashboardMeterView . isAccessibilityElement = true
2021-08-09 13:44:04 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followersDashboardMeterView . accessibilityLabel = L10n . Plural . Count . follower ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
2021-02-23 09:45:00 +01:00
}
2022-01-27 14:23:39 +01:00
private func handleMetaPress ( _ meta : Meta ) {
switch meta {
case . url ( _ , _ , let url , _ ) :
guard let url = URL ( string : url ) else { return }
coordinator . present ( scene : . safari ( url : url ) , from : nil , transition : . safariPresent ( animated : true , completion : nil ) )
case . mention ( _ , _ , let userInfo ) :
guard let href = userInfo ? [ " href " ] as ? String ,
let url = URL ( string : href ) else { return }
coordinator . present ( scene : . safari ( url : url ) , from : nil , transition : . safariPresent ( animated : true , completion : nil ) )
case . hashtag ( _ , let hashtag , _ ) :
let hashtagTimelineViewModel = HashtagTimelineViewModel ( context : context , hashtag : hashtag )
coordinator . present ( scene : . hashtagTimeline ( viewModel : hashtagTimelineViewModel ) , from : nil , transition : . show )
case . email , . emoji :
break
}
2021-04-01 08:39:15 +02:00
}
2021-04-06 10:43:08 +02:00
}
2021-04-01 08:39:15 +02:00
extension ProfileViewController {
2021-04-09 11:31:43 +02:00
@objc private func cancelEditingBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
viewModel . isEditing . value = false
}
2021-04-07 08:24:28 +02:00
@objc private func settingBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2021-04-26 10:57:50 +02:00
guard let setting = context . settingService . currentSetting . value else { return }
let settingsViewModel = SettingsViewModel ( context : context , setting : setting )
coordinator . present ( scene : . settings ( viewModel : settingsViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-07 08:24:28 +02:00
}
@objc private func shareBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let record : ManagedObjectRecord < MastodonUser > = . init ( objectID : user . objectID )
Task {
let _activityViewController = try await DataSourceFacade . createActivityViewController (
dependency : self ,
user : record
)
guard let activityViewController = _activityViewController else { return }
self . coordinator . present (
scene : . activityViewController (
activityViewController : activityViewController ,
sourceView : nil ,
barButtonItem : sender
) ,
from : self ,
transition : . activityViewControllerPresent ( animated : true , completion : nil )
)
} // e n d T a s k
2021-04-07 08:24:28 +02:00
}
@objc private func favoriteBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
let favoriteViewModel = FavoriteViewModel ( context : context )
coordinator . present ( scene : . favorite ( viewModel : favoriteViewModel ) , from : self , transition : . show )
}
2021-04-02 12:13:45 +02:00
@objc private func replyBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2022-01-27 14:23:39 +01:00
guard let authenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else { return }
2022-02-10 12:30:41 +01:00
guard let mastodonUser = viewModel . user else { return }
2021-04-02 12:50:08 +02:00
let composeViewModel = ComposeViewModel (
context : context ,
2022-01-27 14:23:39 +01:00
composeKind : . mention ( user : . init ( objectID : mastodonUser . objectID ) ) ,
authenticationBox : authenticationBox
2021-04-02 12:50:08 +02:00
)
coordinator . present ( scene : . compose ( viewModel : composeViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-02 12:13:45 +02:00
}
2021-04-01 08:39:15 +02:00
@objc private func refreshControlValueChanged ( _ sender : UIRefreshControl ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
let currentViewController = profileSegmentedViewController . pagingViewController . currentViewController
if let currentViewController = currentViewController as ? UserTimelineViewController {
currentViewController . viewModel . stateMachine . enter ( UserTimelineViewModel . State . Reloading . self )
}
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.3 ) {
sender . endRefreshing ( )
}
}
}
// MARK: - U I S c r o l l V i e w D e l e g a t e
extension ProfileViewController : UIScrollViewDelegate {
func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
contentOffsets [ profileSegmentedViewController . pagingViewController . currentIndex ! ] = scrollView . contentOffset . y
let topMaxContentOffsetY = profileSegmentedViewController . view . frame . minY - ProfileHeaderViewController . headerMinHeight - containerScrollView . safeAreaInsets . top
if scrollView . contentOffset . y < topMaxContentOffsetY {
self . containerScrollView . contentOffset . y = scrollView . contentOffset . y
for postTimelineView in profileSegmentedViewController . pagingViewController . viewModel . viewControllers {
2022-02-14 07:55:00 +01:00
postTimelineView . scrollView ? . contentOffset . y = 0
2021-04-01 08:39:15 +02:00
}
contentOffsets . removeAll ( )
} else {
containerScrollView . contentOffset . y = topMaxContentOffsetY
2021-04-08 10:53:32 +02:00
if viewModel . needsPagePinToTop . value {
// d o n o t h i n g
} else {
if let customScrollViewContainerController = profileSegmentedViewController . pagingViewController . currentViewController as ? ScrollViewContainer {
let contentOffsetY = scrollView . contentOffset . y - containerScrollView . contentOffset . y
2022-02-14 07:55:00 +01:00
customScrollViewContainerController . scrollView ? . contentOffset . y = contentOffsetY
2021-04-08 10:53:32 +02:00
}
2021-04-01 08:39:15 +02:00
}
2021-04-08 10:53:32 +02:00
2021-04-01 08:39:15 +02:00
}
// e l a s t i c a l l y b a n n e r i m a g e
2022-01-27 14:23:39 +01:00
let headerScrollProgress = ( containerScrollView . contentOffset . y - containerScrollView . safeAreaInsets . top ) / topMaxContentOffsetY
2021-05-27 09:27:12 +02:00
let throttle = ProfileHeaderViewController . headerMinHeight / topMaxContentOffsetY
profileHeaderViewController . updateHeaderScrollProgress ( headerScrollProgress , throttle : throttle )
2021-04-01 08:39:15 +02:00
}
}
// MARK: - P r o f i l e H e a d e r V i e w C o n t r o l l e r D e l e g a t e
extension ProfileViewController : ProfileHeaderViewControllerDelegate {
2021-07-23 13:10:27 +02:00
2021-04-01 08:39:15 +02:00
func profileHeaderViewController ( _ viewController : ProfileHeaderViewController , viewLayoutDidUpdate view : UIView ) {
guard let scrollView = ( profileSegmentedViewController . pagingViewController . currentViewController as ? UserTimelineViewController ) ? . scrollView else {
// a s s e r t i o n F a i l u r e ( )
return
}
updateOverlayScrollViewContentSize ( scrollView : scrollView )
}
}
// MARK: - P r o f i l e P a g i n g V i e w C o n t r o l l e r D e l e g a t e
extension ProfileViewController : ProfilePagingViewControllerDelegate {
func profilePagingViewController ( _ viewController : ProfilePagingViewController , didScrollToPostCustomScrollViewContainerController postTimelineViewController : ScrollViewContainer , atIndex index : Int ) {
os_log ( " %{public}s[%{public}ld], %{public}s: select at index: %ld " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , index )
2022-01-27 14:23:39 +01:00
// / / u p d a t e s e g e m e n t e d c o n t r o l
// i f i n d e x < p r o f i l e H e a d e r V i e w C o n t r o l l e r . p a g e S e g m e n t e d C o n t r o l . n u m b e r O f S e g m e n t s {
// p r o f i l e H e a d e r V i e w C o n t r o l l e r . p a g e S e g m e n t e d C o n t r o l . s e l e c t e d S e g m e n t I n d e x = i n d e x
// }
2021-04-15 05:21:33 +02:00
2021-04-01 08:39:15 +02:00
// s a v e c o n t e n t o f f s e t
overlayScrollView . contentOffset . y = contentOffsets [ index ] ? ? containerScrollView . contentOffset . y
// s e t u p o b s e r v e r a n d g e s t u r e f a l l b a c k
2022-02-14 07:55:00 +01:00
if let scrollView = postTimelineViewController . scrollView {
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize ( scrollView : scrollView )
scrollView . panGestureRecognizer . require ( toFail : overlayScrollView . panGestureRecognizer )
}
2021-04-01 08:39:15 +02:00
}
}
// MARK: - P r o f i l e H e a d e r V i e w D e l e g a t e
extension ProfileViewController : ProfileHeaderViewDelegate {
2022-01-27 14:23:39 +01:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , avatarButtonDidPressed button : AvatarButton ) {
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let record : ManagedObjectRecord < MastodonUser > = . init ( objectID : user . objectID )
2021-04-28 14:10:17 +02:00
2022-01-27 14:23:39 +01:00
Task {
try await DataSourceFacade . coordinateToMediaPreviewScene (
dependency : self ,
user : record ,
previewContext : DataSourceFacade . ImagePreviewContext (
imageView : button . avatarImageView ,
containerView : . profileAvatar ( profileHeaderView )
)
)
} // e n d T a s k
2021-04-28 14:10:17 +02:00
}
2021-04-28 14:36:10 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , bannerImageViewDidPressed imageView : UIImageView ) {
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let record : ManagedObjectRecord < MastodonUser > = . init ( objectID : user . objectID )
2021-04-28 14:36:10 +02:00
2022-01-27 14:23:39 +01:00
Task {
try await DataSourceFacade . coordinateToMediaPreviewScene (
dependency : self ,
user : record ,
previewContext : DataSourceFacade . ImagePreviewContext (
imageView : imageView ,
containerView : . profileBanner ( profileHeaderView )
)
)
} // e n d T a s k
2021-04-28 14:36:10 +02:00
}
2022-01-27 14:23:39 +01:00
func profileHeaderView (
_ profileHeaderView : ProfileHeaderView ,
relationshipButtonDidPressed button : ProfileRelationshipActionButton
) {
2021-04-02 12:13:45 +02:00
let relationshipActionSet = viewModel . relationshipActionOptionSet . value
2021-06-24 13:20:41 +02:00
// h a n d l e e d i t l o g i c f o r e d i t a b l e p r o f i l e
// h a n d l e r e l a t i o n s h i p l o g i c f o r n o n - e d i t a b l e p r o f i l e
2021-04-02 12:13:45 +02:00
if relationshipActionSet . contains ( . edit ) {
2021-06-24 13:20:41 +02:00
// d o n o t h i n g w h e n u p d a t i n g
2021-04-09 11:31:43 +02:00
guard ! viewModel . isUpdating . value else { return }
2022-01-27 14:23:39 +01:00
guard let profileHeaderViewModel = profileHeaderViewController . viewModel else { return }
guard let profileAboutViewModel = profileSegmentedViewController . pagingViewController . viewModel . profileAboutViewController . viewModel else { return }
let isEdited = profileHeaderViewModel . isEdited ( )
|| profileAboutViewModel . isEdited ( )
if isEdited {
2021-06-24 13:20:41 +02:00
// u p d a t e p r o f i l e i f c h a n g e d
2021-04-09 11:31:43 +02:00
viewModel . isUpdating . value = true
2022-01-27 14:23:39 +01:00
Task {
do {
_ = try await viewModel . updateProfileInfo (
headerProfileInfo : profileHeaderViewModel . editProfileInfo ,
aboutProfileInfo : profileAboutViewModel . editProfileInfo
)
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : update profile info success " )
2021-04-09 11:31:43 +02:00
self . viewModel . isEditing . value = false
2022-01-27 14:23:39 +01:00
} catch {
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : update profile info fail: \( error . localizedDescription ) " )
2021-04-09 11:31:43 +02:00
}
2022-01-27 14:23:39 +01:00
// f i n i s h u p d a t i n g
self . viewModel . isUpdating . value = false
}
2021-04-09 11:31:43 +02:00
} else {
2021-06-24 13:20:41 +02:00
// s e t ` u p d a t i n g ` t h e n t o g g l e ` e d i t ` s t a t e
viewModel . isUpdating . value = true
viewModel . fetchEditProfileInfo ( )
2022-01-27 14:23:39 +01:00
. receive ( on : DispatchQueue . main )
2021-06-24 13:20:41 +02:00
. sink { [ weak self ] completion in
guard let self = self else { return }
defer {
// f i n i s h u p d a t i n g
self . viewModel . isUpdating . value = false
}
switch completion {
case . failure ( let error ) :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , error . localizedDescription )
let alertController = UIAlertController ( for : error , title : L10n . Common . Alerts . EditProfileFailure . title , preferredStyle : . alert )
let okAction = UIAlertAction ( title : L10n . Common . Controls . Actions . ok , style : . default , handler : nil )
alertController . addAction ( okAction )
self . coordinator . present (
scene : . alertController ( alertController : alertController ) ,
from : nil ,
transition : . alertController ( animated : true , completion : nil )
)
case . finished :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: fetch profile info for edit success " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
// e n t e r e d i t i n g m o d e
self . viewModel . isEditing . value . toggle ( )
}
} receiveValue : { [ weak self ] response in
guard let self = self else { return }
self . viewModel . accountForEdit . value = response . value
}
. store ( in : & disposeBag )
2021-04-09 11:31:43 +02:00
}
2021-04-02 12:13:45 +02:00
} else {
guard let relationshipAction = relationshipActionSet . highPriorityAction ( except : . editOptions ) else { return }
switch relationshipAction {
case . none :
break
2021-06-22 14:54:34 +02:00
case . follow , . request , . pending , . following :
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let reocrd = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
guard let authenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else { return }
Task {
try await DataSourceFacade . responseToUserFollowAction (
dependency : self ,
user : reocrd ,
authenticationBox : authenticationBox
)
}
2021-04-02 12:13:45 +02:00
case . muting :
2022-01-27 14:23:39 +01:00
guard let authenticationBox = self . context . authenticationService . activeMastodonAuthenticationBox . value else { return }
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let name = user . displayNameWithFallback
2021-04-02 12:13:45 +02:00
let alertController = UIAlertController (
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . message ( name ) ,
preferredStyle : . alert
)
2022-01-27 14:23:39 +01:00
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
2021-06-22 14:54:34 +02:00
let unmuteAction = UIAlertAction ( title : L10n . Common . Controls . Friendship . unmute , style : . default ) { [ weak self ] _ in
2021-04-02 12:13:45 +02:00
guard let self = self else { return }
2022-01-27 14:23:39 +01:00
Task {
try await DataSourceFacade . responseToUserMuteAction (
dependency : self ,
user : record ,
authenticationBox : authenticationBox
)
}
2021-04-02 12:13:45 +02:00
}
alertController . addAction ( unmuteAction )
let cancelAction = UIAlertAction ( title : L10n . Common . Controls . Actions . cancel , style : . cancel , handler : nil )
alertController . addAction ( cancelAction )
present ( alertController , animated : true , completion : nil )
case . blocking :
2022-01-27 14:23:39 +01:00
guard let authenticationBox = self . context . authenticationService . activeMastodonAuthenticationBox . value else { return }
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2022-01-27 14:23:39 +01:00
let name = user . displayNameWithFallback
2021-04-02 12:13:45 +02:00
let alertController = UIAlertController (
2022-02-15 07:45:34 +01:00
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUser . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUser . message ( name ) ,
2021-04-02 12:13:45 +02:00
preferredStyle : . alert
)
2022-01-27 14:23:39 +01:00
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
2021-06-22 14:54:34 +02:00
let unblockAction = UIAlertAction ( title : L10n . Common . Controls . Friendship . unblock , style : . default ) { [ weak self ] _ in
2021-04-02 12:13:45 +02:00
guard let self = self else { return }
2022-01-27 14:23:39 +01:00
Task {
try await DataSourceFacade . responseToUserBlockAction (
dependency : self ,
user : record ,
authenticationBox : authenticationBox
)
}
2021-04-02 12:13:45 +02:00
}
alertController . addAction ( unblockAction )
let cancelAction = UIAlertAction ( title : L10n . Common . Controls . Actions . cancel , style : . cancel , handler : nil )
alertController . addAction ( cancelAction )
present ( alertController , animated : true , completion : nil )
case . blocked :
break
default :
assertionFailure ( )
}
}
2021-04-01 08:39:15 +02:00
}
2021-07-23 13:10:27 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , metaTextView : MetaTextView , metaDidPressed meta : Meta ) {
2022-01-27 14:23:39 +01:00
handleMetaPress ( meta )
2021-04-02 12:13:45 +02:00
}
2021-07-23 13:10:27 +02:00
2021-11-01 12:54:07 +01:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , profileStatusDashboardView dashboardView : ProfileStatusDashboardView , dashboardMeterViewDidPressed dashboardMeterView : ProfileStatusDashboardMeterView , meter : ProfileStatusDashboardView . Meter ) {
switch meter {
case . post :
// d o n o t h i n g
break
case . follower :
guard let domain = viewModel . domain . value ,
let userID = viewModel . userID . value
else { return }
let followerListViewModel = FollowerListViewModel (
context : context ,
domain : domain ,
userID : userID
)
coordinator . present (
scene : . follower ( viewModel : followerListViewModel ) ,
from : self ,
transition : . show
)
case . following :
2021-11-02 07:56:42 +01:00
guard let domain = viewModel . domain . value ,
let userID = viewModel . userID . value
else { return }
let followingListViewModel = FollowingListViewModel (
context : context ,
domain : domain ,
userID : userID
)
coordinator . present (
scene : . following ( viewModel : followingListViewModel ) ,
from : self ,
transition : . show
)
2021-11-01 12:54:07 +01:00
}
2021-04-01 08:39:15 +02:00
}
}
2022-01-27 14:23:39 +01:00
// MARK: - P r o f i l e A b o u t V i e w C o n t r o l l e r D e l e g a t e
extension ProfileViewController : ProfileAboutViewControllerDelegate {
func profileAboutViewController ( _ viewController : ProfileAboutViewController , profileFieldCollectionViewCell : ProfileFieldCollectionViewCell , metaLabel : MetaLabel , didSelectMeta meta : Meta ) {
handleMetaPress ( meta )
}
2021-02-23 09:45:00 +01:00
}
2021-05-21 09:23:02 +02:00
2022-01-27 14:23:39 +01:00
// MARK: - M a s t o d o n M e n u D e l e g a t e
extension ProfileViewController : MastodonMenuDelegate {
func menuAction ( _ action : MastodonMenu . Action ) {
guard let authenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else { return }
2022-02-10 12:30:41 +01:00
guard let user = viewModel . user else { return }
2021-05-21 11:42:14 +02:00
2022-01-27 14:23:39 +01:00
let userRecord : ManagedObjectRecord < MastodonUser > = . init ( objectID : user . objectID )
Task {
try await DataSourceFacade . responseToMenuAction (
dependency : self ,
action : action ,
menuContext : DataSourceFacade . MenuContext (
author : userRecord ,
status : nil ,
button : nil ,
barButtonItem : self . moreMenuBarButtonItem
) ,
authenticationBox : authenticationBox
)
} // e n d T a s k
2021-05-21 11:42:14 +02:00
}
}
2022-01-27 14:23:39 +01:00
// MARK: - S c r o l l V i e w C o n t a i n e r
2022-02-14 07:55:00 +01:00
extension ProfileViewController : ScrollViewContainer {
var scrollView : UIScrollView ? {
return overlayScrollView
}
}
2022-01-27 14:23:39 +01:00
// e x t e n s i o n P r o f i l e V i e w C o n t r o l l e r {
2022-02-14 07:55:00 +01:00
//
2022-01-27 14:23:39 +01:00
// o v e r r i d e v a r k e y C o m m a n d s : [ U I K e y C o m m a n d ] ? {
// i f ! v i e w M o d e l . i s E d i t i n g . v a l u e {
// r e t u r n s e g m e n t e d C o n t r o l N a v i g a t e K e y C o m m a n d s
// }
2022-02-14 07:55:00 +01:00
//
2022-01-27 14:23:39 +01:00
// r e t u r n n i l
// }
//
// }
2022-02-16 12:47:51 +01:00
// MARK: - S e g m e n t e d C o n t r o l N a v i g a t e a b l e
// e x t e n s i o n P r o f i l e V i e w C o n t r o l l e r : S e g m e n t e d C o n t r o l N a v i g a t e a b l e {
// v a r n a v i g a t e a b l e S e g m e n t e d C o n t r o l : U I S e g m e n t e d C o n t r o l {
// p r o f i l e H e a d e r V i e w C o n t r o l l e r . p a g e S e g m e n t e d C o n t r o l
// }
//
// @ o b j c f u n c s e g m e n t e d C o n t r o l N a v i g a t e K e y C o m m a n d H a n d l e r R e l a y ( _ s e n d e r : U I K e y C o m m a n d ) {
// s e g m e n t e d C o n t r o l N a v i g a t e K e y C o m m a n d H a n d l e r ( s e n d e r )
// }
// }