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
2022-10-08 07:43:06 +02:00
import MastodonCore
2022-01-27 14:23:39 +01:00
import MastodonUI
2022-10-08 07:43:06 +02:00
import MastodonLocalization
2022-01-27 14:23:39 +01:00
import CoreDataStack
2022-05-13 11:23:35 +02:00
import TabBarPager
import XLPagerTabStrip
2022-01-27 14:23:39 +01:00
protocol ProfileViewModelEditable {
2022-05-26 17:19:47 +02:00
var isEdited : Bool { get }
2022-01-27 14:23:39 +01:00
}
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
} ( )
2022-05-13 11:23:35 +02:00
2021-04-07 08:24:28 +02:00
private ( set ) lazy var settingBarButtonItem : UIBarButtonItem = {
2022-05-06 09:17:26 +02:00
let barButtonItem = UIBarButtonItem (
image : Asset . ObjectsAndTools . gear . image . withRenderingMode ( . alwaysTemplate ) ,
style : . plain ,
target : self ,
action : #selector ( ProfileViewController . settingBarButtonItemPressed ( _ : ) )
)
2021-04-07 08:24:28 +02:00
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2022-05-13 11:23:35 +02:00
2021-04-07 08:24:28 +02:00
private ( set ) lazy var shareBarButtonItem : UIBarButtonItem = {
2022-05-06 09:17:26 +02:00
let barButtonItem = UIBarButtonItem (
image : Asset . Arrow . squareAndArrowUp . image . withRenderingMode ( . alwaysTemplate ) ,
style : . plain ,
target : self ,
action : #selector ( ProfileViewController . shareBarButtonItemPressed ( _ : ) )
)
2021-04-07 08:24:28 +02:00
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2022-05-13 11:23:35 +02:00
2021-04-07 08:24:28 +02:00
private ( set ) lazy var favoriteBarButtonItem : UIBarButtonItem = {
2022-05-06 09:17:26 +02:00
let barButtonItem = UIBarButtonItem (
image : Asset . ObjectsAndTools . star . image . withRenderingMode ( . alwaysTemplate ) ,
style : . plain ,
target : self ,
action : #selector ( ProfileViewController . favoriteBarButtonItemPressed ( _ : ) )
)
2021-04-07 08:24:28 +02:00
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2022-07-29 22:31:38 +02:00
private ( set ) lazy var bookmarkBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem (
image : Asset . ObjectsAndTools . bookmark . image . withRenderingMode ( . alwaysTemplate ) ,
style : . plain ,
target : self ,
action : #selector ( ProfileViewController . bookmarkBarButtonItemPressed ( _ : ) )
)
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2022-05-13 11:23:35 +02:00
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
} ( )
2022-05-13 11:23:35 +02:00
2021-04-02 12:13:45 +02:00
let moreMenuBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( image : UIImage ( systemName : " ellipsis.circle " ) , style : . plain , target : nil , action : nil )
barButtonItem . tintColor = . white
return barButtonItem
} ( )
2022-05-13 11:23:35 +02:00
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
} ( )
2022-05-13 11:23:35 +02:00
private ( set ) lazy var tabBarPagerController = TabBarPagerController ( )
2021-04-09 11:31:43 +02:00
private ( set ) lazy var profileHeaderViewController : ProfileHeaderViewController = {
let viewController = ProfileHeaderViewController ( )
2022-05-26 17:19:47 +02:00
viewController . context = context
viewController . coordinator = coordinator
2022-10-09 14:07:57 +02:00
viewController . viewModel = ProfileHeaderViewModel ( context : context , authContext : viewModel . authContext )
2021-04-09 11:31:43 +02:00
return viewController
} ( )
2021-04-01 08:39:15 +02:00
2022-05-13 11:23:35 +02:00
private ( set ) lazy var profilePagingViewController : ProfilePagingViewController = {
let profilePagingViewController = ProfilePagingViewController ( )
profilePagingViewController . viewModel = {
let profilePagingViewModel = ProfilePagingViewModel (
postsUserTimelineViewModel : viewModel . postsUserTimelineViewModel ,
repliesUserTimelineViewModel : viewModel . repliesUserTimelineViewModel ,
mediaUserTimelineViewModel : viewModel . mediaUserTimelineViewModel ,
profileAboutViewModel : viewModel . profileAboutViewModel
)
profilePagingViewModel . viewControllers . forEach { viewController in
if let viewController = viewController as ? NeedsDependency {
viewController . context = context
viewController . coordinator = coordinator
}
}
return profilePagingViewModel
} ( )
return profilePagingViewController
} ( )
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
}
}
2021-02-23 09:45:00 +01:00
extension ProfileViewController {
2022-05-26 17:19:47 +02:00
override var preferredStatusBarStyle : UIStatusBarStyle {
return . lightContent
}
override func viewSafeAreaInsetsDidChange ( ) {
super . viewSafeAreaInsetsDidChange ( )
profileHeaderViewController . updateHeaderContainerSafeAreaInset ( view . safeAreaInsets )
}
2021-04-02 12:13:45 +02:00
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
2022-05-13 11:23:35 +02:00
navigationItem . titleView = titleView
2021-04-07 08:24:28 +02:00
2022-05-26 17:19:47 +02:00
let editingAndUpdatingPublisher = Publishers . CombineLatest (
viewModel . $ isEditing ,
viewModel . $ isUpdating
)
// n o t e : n o t a d d . s h a r e ( ) h e r e
2021-04-02 12:13:45 +02:00
2022-05-26 17:19:47 +02:00
let barButtonItemHiddenPublisher = Publishers . CombineLatest3 (
viewModel . $ isMeBarButtonItemsHidden ,
viewModel . $ isReplyBarButtonItemHidden ,
viewModel . $ isMoreMenuBarButtonItemHidden
)
editingAndUpdatingPublisher
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isEditing , isUpdating in
guard let self = self else { return }
self . cancelEditingBarButtonItem . isEnabled = ! isUpdating
}
. store ( in : & disposeBag )
Publishers . CombineLatest4 (
viewModel . relationshipViewModel . $ isSuspended ,
profileHeaderViewController . viewModel . $ isTitleViewDisplaying ,
editingAndUpdatingPublisher . eraseToAnyPublisher ( ) ,
barButtonItemHiddenPublisher . eraseToAnyPublisher ( )
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isSuspended , isTitleViewDisplaying , tuple1 , tuple2 in
guard let self = self else { return }
let ( isEditing , _ ) = tuple1
let ( isMeBarButtonItemsHidden , isReplyBarButtonItemHidden , isMoreMenuBarButtonItemHidden ) = tuple2
var items : [ UIBarButtonItem ] = [ ]
defer {
self . navigationItem . rightBarButtonItems = ! items . isEmpty ? items : nil
}
guard ! isSuspended else {
return
}
guard ! isEditing else {
items . append ( self . cancelEditingBarButtonItem )
return
}
guard ! isTitleViewDisplaying else {
return
}
guard isMeBarButtonItemsHidden else {
items . append ( self . settingBarButtonItem )
items . append ( self . shareBarButtonItem )
items . append ( self . favoriteBarButtonItem )
2022-07-29 22:31:38 +02:00
items . append ( self . bookmarkBarButtonItem )
2022-05-26 17:19:47 +02:00
return
}
if ! isMoreMenuBarButtonItemHidden {
items . append ( self . moreMenuBarButtonItem )
}
if ! isReplyBarButtonItemHidden {
items . append ( self . replyBarButtonItem )
}
}
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
2022-05-13 11:23:35 +02:00
addChild ( tabBarPagerController )
tabBarPagerController . view . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( tabBarPagerController . view )
tabBarPagerController . didMove ( toParent : self )
2021-04-01 08:39:15 +02:00
NSLayoutConstraint . activate ( [
2022-05-13 11:23:35 +02:00
tabBarPagerController . view . topAnchor . constraint ( equalTo : view . topAnchor ) ,
tabBarPagerController . view . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
tabBarPagerController . view . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
tabBarPagerController . view . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
2021-04-01 08:39:15 +02:00
] )
2022-05-13 11:23:35 +02:00
tabBarPagerController . delegate = self
tabBarPagerController . dataSource = self
2022-01-27 14:23:39 +01:00
2022-05-13 11:23:35 +02:00
tabBarPagerController . relayScrollView . refreshControl = refreshControl
refreshControl . addTarget ( self , action : #selector ( ProfileViewController . refreshControlValueChanged ( _ : ) ) , for : . valueChanged )
2022-05-26 17:19:47 +02:00
// s e t u p d e l e g a t e
profileHeaderViewController . delegate = self
profilePagingViewController . viewModel . profileAboutViewController . delegate = self
bindViewModel ( )
bindTitleView ( )
bindMoreBarButtonItem ( )
bindPager ( )
2022-01-27 14:23:39 +01:00
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
2022-05-26 17:19:47 +02:00
setNeedsStatusBarAppearanceUpdate ( )
2022-01-27 14:23:39 +01:00
}
}
extension ProfileViewController {
2022-05-26 17:19:47 +02:00
private func bindViewModel ( ) {
// h e a d e r
let headerViewModel = profileHeaderViewController . viewModel !
viewModel . $ user
. assign ( to : \ . user , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . $ isEditing
. assign ( to : \ . isEditing , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . $ isUpdating
. assign ( to : \ . isUpdating , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . relationshipViewModel . $ optionSet
. map { $0 ? ? . none }
. assign ( to : \ . relationshipActionOptionSet , on : headerViewModel )
. store ( in : & disposeBag )
viewModel . $ accountForEdit
. assign ( to : \ . accountForEdit , on : headerViewModel )
. store ( in : & disposeBag )
// t i m e l i n e
[
viewModel . postsUserTimelineViewModel ,
viewModel . repliesUserTimelineViewModel ,
viewModel . mediaUserTimelineViewModel ,
] . forEach { userTimelineViewModel in
viewModel . relationshipViewModel . $ isBlocking . assign ( to : \ . isBlocking , on : userTimelineViewModel ) . store ( in : & disposeBag )
viewModel . relationshipViewModel . $ isBlockingBy . assign ( to : \ . isBlockedBy , on : userTimelineViewModel ) . store ( in : & disposeBag )
viewModel . relationshipViewModel . $ isSuspended . assign ( to : \ . isSuspended , on : userTimelineViewModel ) . store ( in : & disposeBag )
}
// a b o u t
2022-06-14 07:44:32 +02:00
let aboutViewModel = viewModel . profileAboutViewModel
viewModel . $ user
. assign ( to : \ . user , on : aboutViewModel )
. store ( in : & disposeBag )
2022-05-26 17:19:47 +02:00
viewModel . $ isEditing
. assign ( to : \ . isEditing , on : aboutViewModel )
. store ( in : & disposeBag )
viewModel . $ accountForEdit
. assign ( to : \ . accountForEdit , on : aboutViewModel )
. store ( in : & disposeBag )
}
2022-01-27 14:23:39 +01:00
2022-05-26 17:19:47 +02:00
private func bindTitleView ( ) {
Publishers . CombineLatest3 (
profileHeaderViewController . profileHeaderView . viewModel . $ name ,
profileHeaderViewController . profileHeaderView . viewModel . $ emojiMeta ,
profileHeaderViewController . profileHeaderView . viewModel . $ statusesCount
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] name , emojiMeta , statusesCount in
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
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 {
}
}
. store ( in : & disposeBag )
profileHeaderViewController . profileHeaderView . viewModel . $ name
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] name in
guard let self = self else { return }
self . navigationItem . title = name
}
. store ( in : & disposeBag )
}
private func bindMoreBarButtonItem ( ) {
Publishers . CombineLatest (
viewModel . $ user ,
viewModel . relationshipViewModel . $ optionSet
)
. asyncMap { [ weak self ] user , relationshipSet -> UIMenu ? in
guard let self = self else { return nil }
guard let user = user else {
return nil
}
let name = user . displayNameWithFallback
let _ = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
2022-11-06 10:16:56 +01:00
var menuActions : [ MastodonMenu . Action ] = [
. muteUser ( . init ( name : name , isMuting : self . viewModel . relationshipViewModel . isMuting ) ) ,
. blockUser ( . init ( name : name , isBlocking : self . viewModel . relationshipViewModel . isBlocking ) ) ,
. reportUser ( . init ( name : name ) ) ,
. shareUser ( . init ( name : name ) ) ,
]
if let me = self . viewModel ? . me , me . following . contains ( user ) {
2022-11-07 18:52:02 +01:00
let showReblogs = me . showingReblogsBy . contains ( user )
let context = MastodonMenu . HideReblogsActionContext ( showReblogs : showReblogs )
menuActions . insert ( . hideReblogs ( context ) , at : 1 )
2022-11-06 10:16:56 +01:00
}
2022-05-26 17:19:47 +02:00
let menu = MastodonMenu . setupMenu (
2022-11-06 10:16:56 +01:00
actions : menuActions ,
2022-05-26 17:19:47 +02:00
delegate : self
)
return menu
}
. sink { [ weak self ] completion in
guard let self = self else { return }
switch completion {
case . failure :
self . moreMenuBarButtonItem . menu = nil
case . finished :
break
}
} receiveValue : { [ weak self ] menu in
guard let self = self else { return }
2022-11-04 16:28:14 +01:00
OperationQueue . main . addOperation {
self . moreMenuBarButtonItem . menu = menu
}
2022-05-26 17:19:47 +02:00
}
. store ( in : & disposeBag )
}
private func bindPager ( ) {
viewModel . $ isPagingEnabled
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isPagingEnabled in
guard let self = self else { return }
self . profilePagingViewController . containerView . isScrollEnabled = isPagingEnabled
self . profilePagingViewController . buttonBarView . isUserInteractionEnabled = isPagingEnabled
}
. store ( in : & disposeBag )
viewModel . $ isEditing
. receive ( on : DispatchQueue . main )
. 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 . profilePagingViewController . 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 ) }
if isEditing ,
let index = self . profilePagingViewController . viewControllers . firstIndex ( where : { type ( of : $0 ) is ProfileAboutViewController . Type } ) ,
self . profilePagingViewController . canMoveTo ( index : index )
{
self . profilePagingViewController . moveToViewController ( at : index )
}
}
. store ( in : & disposeBag )
}
2021-07-23 13:10:27 +02:00
2022-05-13 11:23:35 +02:00
// p r i v a t e 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 ( ) {
//
// P u b l i s h e r s . C o m b i n e L a t e s t 3 (
// v i e w M o d e l . i s B l o c k i n g . e r a s e T o A n y P u b l i s h e r ( ) ,
// v i e w M o d e l . i s B l o c k e d B y . e r a s e T o A n y P u b l i s h e r ( ) ,
// v i e w M o d e l . s u s p e n d e d . e r a s e T o A n y P u b l i s h e r ( )
// )
// . r e c e i v e ( o n : D i s p a t c h Q u e u e . m a i n )
// . s i n k { [ w e a k s e l f ] i s B l o c k i n g , i s B l o c k e d B y , s u s p e n d e d i n
// g u a r d l e t s e l f = s e l f e l s e { r e t u r n }
// l e t i s N e e d S e t H i d d e n = i s B l o c k i n g | | i s B l o c k e d B y | | s u s p e n d e d
// s e l f . 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 . v i e w M o d e l . n e e d s S e t u p B o t t o m S h a d o w . v a l u e = ! i s N e e d S e t H i d d e n
// s e l f . 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 r o f i l e H e a d e r V i e w . b i o C o n t a i n e r V i e w . i s H i d d e n = i s N e e d S e t H i d d e n
// s e l f . 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 . v i e w M o d e l . n e e d s F i l e d C o l l e c t i o n V i e w H i d d e n . v a l u e = i s N e e d S e t H i d d e n
// s e l f . 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 . b u t t o n B a r . i s U s e r I n t e r a c t i o n E n a b l e d = ! i s N e e d S e t H i d d e n
// s e l f . v i e w M o d e l . n e e d s P a g e P i n T o T o p . v a l u e = i s N e e d S e t H i d d e n
// }
// . s t o r e ( i n : & d i s p o s e B a g )
// } / / 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
2022-05-26 17:19:47 +02:00
private func handleMetaPress ( _ meta : Meta ) {
switch meta {
case . url ( _ , _ , let url , _ ) :
guard let url = URL ( string : url ) else { return }
2022-10-09 14:07:57 +02:00
_ = coordinator . present ( scene : . safari ( url : url ) , from : nil , transition : . safariPresent ( animated : true , completion : nil ) )
2022-05-26 17:19:47 +02:00
case . mention ( _ , _ , let userInfo ) :
guard let href = userInfo ? [ " href " ] as ? String ,
let url = URL ( string : href ) else { return }
2022-10-09 14:07:57 +02:00
_ = coordinator . present ( scene : . safari ( url : url ) , from : nil , transition : . safariPresent ( animated : true , completion : nil ) )
2022-05-26 17:19:47 +02:00
case . hashtag ( _ , let hashtag , _ ) :
2022-10-09 14:07:57 +02:00
let hashtagTimelineViewModel = HashtagTimelineViewModel ( context : context , authContext : viewModel . authContext , hashtag : hashtag )
_ = coordinator . present ( scene : . hashtagTimeline ( viewModel : hashtagTimelineViewModel ) , from : nil , transition : . show )
2022-05-26 17:19:47 +02:00
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 {
2022-05-13 11:23:35 +02:00
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 )
2022-05-26 17:19:47 +02:00
viewModel . isEditing = false
2021-04-09 11:31:43 +02:00
}
2022-05-13 11:23:35 +02:00
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 )
2022-05-26 17:19:47 +02:00
guard let setting = context . settingService . currentSetting . value else { return }
2022-10-09 14:07:57 +02:00
let settingsViewModel = SettingsViewModel ( context : context , authContext : viewModel . authContext , setting : setting )
2022-05-26 17:19:47 +02:00
coordinator . present ( scene : . settings ( viewModel : settingsViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-07 08:24:28 +02:00
}
2022-05-13 11:23:35 +02:00
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-05-26 17:19:47 +02:00
guard let user = viewModel . user else { return }
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 }
2022-10-10 13:14:52 +02:00
_ = self . coordinator . present (
2022-05-26 17:19:47 +02:00
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
}
2022-05-13 11:23:35 +02:00
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 )
2022-10-09 14:07:57 +02:00
let favoriteViewModel = FavoriteViewModel ( context : context , authContext : viewModel . authContext )
2022-10-10 13:14:52 +02:00
_ = coordinator . present ( scene : . favorite ( viewModel : favoriteViewModel ) , from : self , transition : . show )
2021-04-07 08:24:28 +02:00
}
2022-07-29 22:31:38 +02:00
@objc private func bookmarkBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2022-10-09 14:07:57 +02:00
let bookmarkViewModel = BookmarkViewModel ( context : context , authContext : viewModel . authContext )
2022-10-10 13:14:52 +02:00
_ = coordinator . present ( scene : . bookmark ( viewModel : bookmarkViewModel ) , from : self , transition : . show )
2022-07-29 22:31:38 +02:00
}
2022-05-13 11:23:35 +02:00
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-05-26 17:19:47 +02:00
guard let mastodonUser = viewModel . user else { return }
let composeViewModel = ComposeViewModel (
context : context ,
2022-10-10 13:14:52 +02:00
authContext : viewModel . authContext ,
kind : . mention ( user : mastodonUser . asRecrod )
2022-05-26 17:19:47 +02:00
)
2022-10-10 13:14:52 +02:00
_ = coordinator . present ( scene : . compose ( viewModel : composeViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-02 12:13:45 +02:00
}
2022-05-13 11:23:35 +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 )
2022-05-26 17:19:47 +02:00
if let userTimelineViewController = profilePagingViewController . currentViewController as ? UserTimelineViewController {
userTimelineViewController . viewModel . stateMachine . enter ( UserTimelineViewModel . State . Reloading . self )
}
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.3 ) {
sender . endRefreshing ( )
}
2021-04-01 08:39:15 +02:00
}
}
2022-05-13 11:23:35 +02:00
// MARK: - T a b B a r P a g e r D e l e g a t e
extension ProfileViewController : TabBarPagerDelegate {
2021-04-28 14:10:17 +02:00
2022-05-13 11:23:35 +02:00
func tabBarMinimalHeight ( ) -> CGFloat {
return ProfileHeaderViewController . headerMinHeight
2021-04-28 14:36:10 +02:00
}
2022-05-13 11:23:35 +02:00
func resetPageContentOffset ( _ tabBarPagerController : TabBarPagerController ) {
for viewController in profilePagingViewController . viewModel . viewControllers {
viewController . pageScrollView . contentOffset = . zero
2021-11-01 12:54:07 +01:00
}
2021-04-01 08:39:15 +02:00
}
2022-05-13 11:23:35 +02:00
func tabBarPagerController ( _ tabBarPagerController : TabBarPagerController , didScroll scrollView : UIScrollView ) {
2022-05-26 17:19:47 +02:00
// t r y t o f i n d s o m e p a t t e r n s :
// p r i n t ( " " "
// - - - - -
// h e a d e r M i n H e i g h t : \ ( 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 . h e a d e r M i n H e i g h t )
// s c r o l l V i e w . c o n t e n t O f f s e t . y : \ ( s c r o l l V i e w . c o n t e n t O f f s e t . y )
// s c r o l l V i e w . c o n t e n t S i z e . h e i g h t : \ ( s c r o l l V i e w . c o n t e n t S i z e . h e i g h t )
// s c r o l l V i e w . f r a m e : \ ( s c r o l l V i e w . f r a m e )
// s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . t o p : \ ( s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . t o p )
// s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . b o t t o m : \ ( s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . b o t t o m )
// " " "
// )
2022-05-13 11:23:35 +02:00
// e l a s t i c a l l y b a n n e r
2022-05-26 17:19:47 +02:00
// m a k e b a n n e r t o p s n a p t o w i n d o w t o p
// d o n o t r e l y o n t h e v i e w f r a m e b e c a s e t h e h e a d e r f r a m e i s . z e r o d u r i n g t h e i n i t i a l c a l l
profileHeaderViewController . profileHeaderView . bannerImageViewTopLayoutConstraint . constant = min ( 0 , scrollView . contentOffset . y )
if profileHeaderViewController . profileHeaderView . frame != . zero {
// m a k e b a n n e r b o t t o m n o t h i g h e r t h a n n a v i g a t i o n b a r b o t t o m
let bannerContainerInWindow = profileHeaderViewController . profileHeaderView . convert (
profileHeaderViewController . profileHeaderView . bannerContainerView . frame ,
to : nil
)
let bannerContainerBottomOffset = bannerContainerInWindow . origin . y + bannerContainerInWindow . height
// p r i n t ( " b a n n e r C o n t a i n e r B o t t o m O f f s e t : \ ( b a n n e r C o n t a i n e r B o t t o m O f f s e t ) " )
let height = profileHeaderViewController . view . frame . height - bannerContainerInWindow . height
// m a k e a v a t a h i d d e n w h e n s c r o l l 0 . 5 x a v a t a r h e i g h t
let throttle = height != . zero ? 0.5 * ProfileHeaderView . avatarImageViewSize . height / height : 0
let progress : CGFloat
if bannerContainerBottomOffset < tabBarPagerController . containerScrollView . safeAreaInsets . top {
let offset = bannerContainerBottomOffset - tabBarPagerController . containerScrollView . safeAreaInsets . top
profileHeaderViewController . profileHeaderView . bannerImageViewBottomLayoutConstraint . constant = offset
// t h e p r o g r e s s f o r h e a d e r m o v e f r o m b a n n e r b o t t o m t o h e a d e r b o t t o m ( f r o m 0 t o 1 )
progress = height != . zero ? abs ( offset ) / height : 0
} else {
profileHeaderViewController . profileHeaderView . bannerImageViewBottomLayoutConstraint . constant = 0
progress = 0
}
2022-06-09 10:41:54 +02:00
// s e t u p f o l l o w s y o u m a s k
// 1 . s e t m a s k s i z e
profileHeaderViewController . profileHeaderView . followsYouMaskView . frame = profileHeaderViewController . profileHeaderView . followsYouBlurEffectView . bounds
// 2 . c h e c k f o l l o w s y o u v i e w o v e r f l o w n a v i g a t i o n b a r o r n o t
let followsYouBlurEffectViewInWindow = profileHeaderViewController . profileHeaderView . convert (
profileHeaderViewController . profileHeaderView . followsYouBlurEffectView . frame ,
to : nil
)
if followsYouBlurEffectViewInWindow . minY < tabBarPagerController . containerScrollView . safeAreaInsets . top {
let offestY = tabBarPagerController . containerScrollView . safeAreaInsets . top - followsYouBlurEffectViewInWindow . minY
let height = profileHeaderViewController . profileHeaderView . followsYouMaskView . frame . height
profileHeaderViewController . profileHeaderView . followsYouMaskView . frame . origin . y = min ( offestY , height )
} else {
profileHeaderViewController . profileHeaderView . followsYouMaskView . frame . origin . y = . zero
}
2022-05-26 17:19:47 +02:00
// s e t u p t i t l e V i e w o f f s e t a n d f a d e a v a t a r
profileHeaderViewController . updateHeaderScrollProgress ( progress , throttle : throttle )
// s e t u p b u t t o n B a r s h a d o w
profilePagingViewController . updateButtonBarShadow ( progress : progress )
2022-05-11 12:40:21 +02:00
}
}
2022-05-26 17:19:47 +02:00
2022-05-11 12:40:21 +02:00
}
2022-05-13 11:23:35 +02:00
// MARK: - T a b B a r P a g e r D a t a S o u r c e
extension ProfileViewController : TabBarPagerDataSource {
func headerViewController ( ) -> UIViewController & TabBarPagerHeader {
return profileHeaderViewController
2022-05-11 12:40:21 +02:00
}
2022-05-13 11:23:35 +02:00
func pageViewController ( ) -> UIViewController & TabBarPageViewController {
return profilePagingViewController
2022-05-11 12:40:21 +02:00
}
}
2022-02-16 12:47:51 +01:00
2022-05-13 11:23:35 +02:00
// / / MARK: - U I S c r o l l V i e w D e l e g a t 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 : U I S c r o l l V i e w D e l e g a t e {
//
// f u n c s c r o l l V i e w D i d S c r o l l ( _ s c r o l l V i e w : U I S c r o l l V i e w ) {
// c o n t e n t O f f s e t s [ p r o f i l e S e g m e n t e d V i e w C o n t r o l l e r . p a g i n g V i e w C o n t r o l l e r . c u r r e n t I n d e x ! ] = s c r o l l V i e w . c o n t e n t O f f s e t . y
// l e t t o p M a x C o n t e n t O f f s e t Y = p r o f i l e S e g m e n t e d V i e w C o n t r o l l e r . v i e w . f r a m e . m i n Y - 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 . h e a d e r M i n H e i g h t - c o n t a i n e r S c r o l l V i e w . s a f e A r e a I n s e t s . t o p
// i f s c r o l l V i e w . c o n t e n t O f f s e t . y < t o p M a x C o n t e n t O f f s e t Y {
// s e l f . c o n t a i n e r S c r o l l V i e w . c o n t e n t O f f s e t . y = s c r o l l V i e w . c o n t e n t O f f s e t . y
// f o r p o s t T i m e l i n e V i e w i n p r o f i l e S e g m e n t e d V i e w C o n t r o l l e r . p a g i n g V i e w C o n t r o l l e r . v i e w M o d e l . v i e w C o n t r o l l e r s {
// p o s t T i m e l i n e V i e w . s c r o l l V i e w ? . c o n t e n t O f f s e t . y = 0
// }
// c o n t e n t O f f s e t s . r e m o v e A l l ( )
// } e l s e {
// c o n t a i n e r S c r o l l V i e w . c o n t e n t O f f s e t . y = t o p M a x C o n t e n t O f f s e t Y
// i f v i e w M o d e l . n e e d s P a g e P i n T o T o p . v a l u e {
// / / d o n o t h i n g
// } e l s e {
// i f l e t c u s t o m S c r o l l V i e w C o n t a i n e r C o n t r o l l e r = p r o f i l e S e g m e n t e d V i e w C o n t r o l l e r . p a g i n g V i e w C o n t r o l l e r . c u r r e n t V i e w C o n t r o l l e r a s ? S c r o l l V i e w C o n t a i n e r {
// l e t c o n t e n t O f f s e t Y = s c r o l l V i e w . c o n t e n t O f f s e t . y - c o n t a i n e r S c r o l l V i e w . c o n t e n t O f f s e t . y
// c u s t o m S c r o l l V i e w C o n t a i n e r C o n t r o l l e r . s c r o l l V i e w ? . c o n t e n t O f f s e t . y = c o n t e n t O f f s e t Y
// }
// }
//
// }
// }
//
// }
2022-05-26 17:19:47 +02:00
2022-10-09 14:07:57 +02:00
// MARK: - A u t h C o n t e x t P r o v i d e r
extension ProfileViewController : AuthContextProvider {
var authContext : AuthContext { viewModel . authContext }
}
2022-05-26 17:19:47 +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 {
func profileHeaderViewController (
_ profileHeaderViewController : ProfileHeaderViewController ,
profileHeaderView : ProfileHeaderView ,
relationshipButtonDidPressed button : ProfileRelationshipActionButton
) {
let relationshipActionSet = viewModel . relationshipViewModel . optionSet ? ? . none
// 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
if relationshipActionSet . contains ( . edit ) {
// d o n o t h i n g w h e n u p d a t i n g
guard ! viewModel . isUpdating else { return }
guard let profileHeaderViewModel = profileHeaderViewController . viewModel else { return }
guard let profileAboutViewModel = profilePagingViewController . viewModel . profileAboutViewController . viewModel else { return }
let isEdited = profileHeaderViewModel . isEdited || profileAboutViewModel . isEdited
if isEdited {
// u p d a t e p r o f i l e w h e n e d i t e d
viewModel . isUpdating = true
Task { @ MainActor in
do {
// TODO: h a n d l e e r r o r
_ = try await viewModel . updateProfileInfo (
headerProfileInfo : profileHeaderViewModel . profileInfoEditing ,
aboutProfileInfo : profileAboutViewModel . profileInfoEditing
)
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : update profile info success " )
self . viewModel . isEditing = false
} catch {
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : update profile info fail: \( 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 )
alertController . addAction ( okAction )
self . present ( alertController , animated : true )
}
// f i n i s h u p d a t i n g
self . viewModel . isUpdating = false
} // e n d T a s k
} else {
// 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 = true
viewModel . fetchEditProfileInfo ( )
. receive ( on : DispatchQueue . main )
. 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 = 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 )
2022-11-06 10:16:56 +01:00
_ = self . coordinator . present (
2022-05-26 17:19:47 +02:00
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 . toggle ( )
}
} receiveValue : { [ weak self ] response in
guard let self = self else { return }
self . viewModel . accountForEdit = response . value
}
. store ( in : & disposeBag )
}
} else {
guard let relationshipAction = relationshipActionSet . highPriorityAction ( except : . editOptions ) else { return }
switch relationshipAction {
case . none :
break
case . follow , . request , . pending , . following :
guard let user = viewModel . user else { return }
2022-11-06 10:16:56 +01:00
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
2022-05-26 17:19:47 +02:00
Task {
try await DataSourceFacade . responseToUserFollowAction (
dependency : self ,
2022-11-06 10:16:56 +01:00
user : record
2022-05-26 17:19:47 +02:00
)
}
case . muting :
guard let user = viewModel . user else { return }
let name = user . displayNameWithFallback
let alertController = UIAlertController (
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . message ( name ) ,
preferredStyle : . alert
)
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
let unmuteAction = UIAlertAction ( title : L10n . Common . Controls . Friendship . unmute , style : . default ) { [ weak self ] _ in
guard let self = self else { return }
Task {
try await DataSourceFacade . responseToUserMuteAction (
dependency : self ,
2022-10-09 14:07:57 +02:00
user : record
2022-05-26 17:19:47 +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 :
guard let user = viewModel . user else { return }
let name = user . displayNameWithFallback
let alertController = UIAlertController (
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUser . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUser . message ( name ) ,
preferredStyle : . alert
)
let record = ManagedObjectRecord < MastodonUser > ( objectID : user . objectID )
let unblockAction = UIAlertAction ( title : L10n . Common . Controls . Friendship . unblock , style : . default ) { [ weak self ] _ in
guard let self = self else { return }
Task {
try await DataSourceFacade . responseToUserBlockAction (
dependency : self ,
2022-10-09 14:07:57 +02:00
user : record
2022-05-26 17:19:47 +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 )
2022-11-06 10:16:56 +01:00
case . blocked , . showReblogs , . isMyself , . followingBy , . blockingBy , . suspended , . edit , . editing , . updating :
2022-05-26 17:19:47 +02:00
break
}
}
}
func profileHeaderViewController (
_ profileHeaderViewController : ProfileHeaderViewController ,
profileHeaderView : ProfileHeaderView ,
metaTextView : MetaTextView ,
metaDidPressed meta : Meta
) {
handleMetaPress ( meta )
}
}
// 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 )
}
}
// 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 user = viewModel . user else { return }
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
2022-10-09 14:07:57 +02:00
)
2022-05-26 17:19:47 +02:00
)
} // e n d T a s k
}
}
// MARK: - S c r o l l V i e w C o n t a i n e r
extension ProfileViewController : ScrollViewContainer {
var scrollView : UIScrollView {
return tabBarPagerController . containerScrollView
}
}
2022-06-02 13:33:10 +02:00
extension ProfileViewController {
override var keyCommands : [ UIKeyCommand ] ? {
if ! viewModel . isEditing {
return pagerTabStripNavigateKeyCommands
}
return nil
}
}
// MARK: - P a g e r T a b S t r i p N a v i g a t e a b l e
extension ProfileViewController : PagerTabStripNavigateable {
var navigateablePageViewController : PagerTabStripViewController {
return profilePagingViewController
}
@objc func pagerTabStripNavigateKeyCommandHandlerRelay ( _ sender : UIKeyCommand ) {
pagerTabStripNavigateKeyCommandHandler ( sender )
}
}
2022-05-13 11:23:35 +02:00