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
import ActiveLabel
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
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 {
os_log ( " %{public}s[%{public}ld], %{public}s: deinit " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
}
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-05 10:07:17 +02:00
ThemeService . shared . currentTheme
. receive ( on : RunLoop . main )
. 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 ( )
barAppearance . configureWithTransparentBackground ( )
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 ! isReplyBarButtonItemHidden {
items . append ( self . replyBarButtonItem )
}
if ! isMoreMenuBarButtonItemHidden {
items . append ( self . moreMenuBarButtonItem )
}
}
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
overlayScrollView . refreshControl = refreshControl
refreshControl . addTarget ( self , action : #selector ( ProfileViewController . refreshControlValueChanged ( _ : ) ) , for : . valueChanged )
let postsUserTimelineViewModel = UserTimelineViewModel ( context : context , domain : viewModel . domain . value , userID : viewModel . userID . value , queryFilter : UserTimelineViewModel . QueryFilter ( ) )
2021-04-06 10:43:08 +02:00
bind ( userTimelineViewModel : postsUserTimelineViewModel )
2021-04-01 08:39:15 +02:00
let repliesUserTimelineViewModel = 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 : 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
profileSegmentedViewController . pagingViewController . viewModel = {
let profilePagingViewModel = ProfilePagingViewModel (
postsUserTimelineViewModel : postsUserTimelineViewModel ,
repliesUserTimelineViewModel : repliesUserTimelineViewModel ,
mediaUserTimelineViewModel : mediaUserTimelineViewModel
)
profilePagingViewModel . viewControllers . forEach { viewController in
if let viewController = viewController as ? NeedsDependency {
viewController . context = context
viewController . coordinator = coordinator
}
}
return profilePagingViewModel
} ( )
profileHeaderViewController . pageSegmentedControl . removeAllSegments ( )
profileSegmentedViewController . pagingViewController . viewModel . barItems . forEach { item in
let index = profileHeaderViewController . pageSegmentedControl . numberOfSegments
profileHeaderViewController . pageSegmentedControl . insertSegment ( withTitle : item . title , at : index , animated : false )
}
profileHeaderViewController . pageSegmentedControl . selectedSegmentIndex = 0
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
profileSegmentedViewController . pagingViewController . pagingDelegate = self
// b i n d v i e w m o d e l
2021-06-29 13:27:40 +02:00
Publishers . CombineLatest3 (
viewModel . name ,
viewModel . emojiDict ,
viewModel . statusesCount
2021-04-09 13:44:48 +02:00
)
. receive ( on : DispatchQueue . main )
2021-06-29 13:27:40 +02:00
. sink { [ weak self ] name , emojiDict , 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
}
2021-07-06 08:55:24 +02:00
let subtitle = L10n . Plural . Count . MetricFormatted . post ( formattedStatusCount , statusesCount )
2021-06-29 13:27:40 +02:00
self . titleView . update ( title : title , subtitle : subtitle , emojiDict : emojiDict )
2021-04-09 13:44:48 +02:00
self . titleView . isHidden = false
}
. 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 )
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 )
2021-04-09 11:31:43 +02:00
viewModel . avatarImageURL
. receive ( on : DispatchQueue . main )
. map { url in ProfileHeaderViewModel . ProfileInfo . ImageResource . url ( url ) }
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . displayProfileInfo . avatarImageResource )
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
viewModel . name
2021-04-09 11:31:43 +02:00
. map { $0 ? ? " " }
2021-04-01 08:39:15 +02:00
. receive ( on : DispatchQueue . main )
2021-04-09 11:31:43 +02:00
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . displayProfileInfo . name )
2021-04-01 08:39:15 +02:00
. store ( in : & disposeBag )
2021-06-29 13:27:40 +02:00
viewModel . fields
2021-05-27 07:56:55 +02:00
. removeDuplicates ( )
. map { fields -> [ ProfileFieldItem . FieldValue ] in
fields . map { ProfileFieldItem . FieldValue ( name : $0 . name , value : $0 . value ) }
}
. receive ( on : DispatchQueue . main )
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . displayProfileInfo . fields )
. store ( in : & disposeBag )
2021-06-24 13:20:41 +02:00
viewModel . accountForEdit
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . accountForEdit )
. store ( in : & disposeBag )
2021-05-27 07:56:55 +02:00
viewModel . emojiDict
. receive ( on : DispatchQueue . main )
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . emojiDict )
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
viewModel . username
. map { username in username . flatMap { " @ " + $0 } ? ? " " }
. receive ( on : DispatchQueue . main )
2021-04-02 12:13:45 +02:00
. assign ( to : \ . text , on : profileHeaderViewController . profileHeaderView . usernameLabel )
2021-04-01 08:39:15 +02:00
. store ( in : & disposeBag )
2021-04-30 08:55:02 +02:00
Publishers . CombineLatest (
viewModel . relationshipActionOptionSet ,
viewModel . context . blockDomainService . blockedDomains
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] relationshipActionOptionSet , domains in
guard let self = self else { return }
guard let mastodonUser = self . viewModel . mastodonUser . value else {
self . moreMenuBarButtonItem . menu = nil
return
2021-04-02 12:13:45 +02:00
}
2021-05-06 12:19:24 +02:00
guard let currentMastodonUser = self . viewModel . currentMastodonUser . value else {
self . moreMenuBarButtonItem . menu = nil
return
}
2021-04-30 08:55:02 +02:00
guard let currentDomain = self . viewModel . domain . value else { return }
let isMuting = relationshipActionOptionSet . contains ( . muting )
let isBlocking = relationshipActionOptionSet . contains ( . blocking )
let isDomainBlocking = domains . contains ( mastodonUser . domainFromAcct )
let needsShareAction = self . viewModel . isMeBarButtonItemsHidden . value
let isInSameDomain = mastodonUser . domainFromAcct = = currentDomain
2021-05-06 12:19:24 +02:00
let isMyself = currentMastodonUser . id = = mastodonUser . id
2021-04-30 08:55:02 +02:00
self . moreMenuBarButtonItem . menu = UserProviderFacade . createProfileActionMenu (
for : mastodonUser ,
2021-05-06 12:19:24 +02:00
isMyself : isMyself ,
2021-04-30 08:55:02 +02:00
isMuting : isMuting ,
isBlocking : isBlocking ,
isInSameDomain : isInSameDomain ,
isDomainBlocking : isDomainBlocking ,
provider : self ,
cell : nil ,
sourceView : nil ,
barButtonItem : self . moreMenuBarButtonItem ,
shareUser : needsShareAction ? mastodonUser : nil ,
shareStatus : nil )
}
. 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 }
self . profileHeaderViewController . profileHeaderView . relationshipActionButton . isHidden = isHidden
}
. store ( in : & disposeBag )
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 )
2021-04-09 11:31:43 +02:00
viewModel . isEditing
. handleEvents ( receiveOutput : { [ weak self ] isEditing in
guard let self = self else { return }
2021-06-24 09:14:50 +02:00
// 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
2021-05-21 09:23:02 +02:00
if ! isEditing {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 1 ) {
self . profileSegmentedViewController . pagingViewController . becomeFirstResponder ( )
}
}
2021-04-09 11:31:43 +02:00
// 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 . pageSegmentedControl . isEnabled = ! isEditing
self . profileSegmentedViewController . view . isUserInteractionEnabled = ! isEditing
let animator = UIViewPropertyAnimator ( duration : 0.33 , curve : . easeInOut )
animator . addAnimations {
self . profileSegmentedViewController . view . alpha = isEditing ? 0.2 : 1.0
2021-04-09 13:44:48 +02:00
self . profileHeaderViewController . profileHeaderView . statusDashboardView . alpha = isEditing ? 0.2 : 1.0
2021-04-09 11:31:43 +02:00
}
animator . startAnimation ( )
} )
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . isEditing )
. store ( in : & disposeBag )
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
self . profileHeaderViewController . pageSegmentedControl . isEnabled = ! isNeedSetHidden
2021-04-08 10:53:32 +02:00
self . viewModel . needsPagePinToTop . value = isNeedSetHidden
}
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
viewModel . bioDescription
. receive ( on : DispatchQueue . main )
2021-04-09 11:31:43 +02:00
. assign ( to : \ . value , on : profileHeaderViewController . viewModel . displayProfileInfo . note )
2021-04-01 08:39:15 +02:00
. store ( in : & disposeBag )
viewModel . statusesCount
. 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
self . profileHeaderViewController . profileHeaderView . statusDashboardView . postDashboardMeterView . accessibilityLabel = L10n . Scene . Profile . Dashboard . Accessibility . countPosts ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
viewModel . followingCount
. 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
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followingDashboardMeterView . accessibilityLabel = L10n . Scene . Profile . Dashboard . Accessibility . countFollowing ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
viewModel . followersCount
. 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
self . profileHeaderViewController . profileHeaderView . statusDashboardView . followersDashboardMeterView . accessibilityLabel = L10n . Scene . Profile . Dashboard . Accessibility . countFollowers ( count ? ? 0 )
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
2021-07-06 11:53:01 +02:00
viewModel . needsPagingEnabled
2021-06-24 09:14:50 +02:00
. receive ( on : RunLoop . main )
. sink { [ weak self ] needsPaingEnabled in
guard let self = self else { return }
self . profileSegmentedViewController . pagingViewController . isScrollEnabled = needsPaingEnabled
}
. store ( in : & disposeBag )
2021-06-24 10:50:20 +02:00
viewModel . needsImageOverlayBlurred
. receive ( on : RunLoop . main )
. 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
}
}
. store ( in : & disposeBag )
2021-04-02 12:13:45 +02:00
profileHeaderViewController . profileHeaderView . delegate = self
2021-02-23 09:45:00 +01:00
}
2021-04-02 12:13:45 +02:00
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 ( )
}
2021-04-01 08:39:15 +02:00
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
2021-04-02 12:13:45 +02:00
2021-04-01 08:39:15 +02:00
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
guard let currentViewController = profileSegmentedViewController . pagingViewController . currentViewController as ? ScrollViewContainer else { return }
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize ( scrollView : currentViewController . scrollView )
currentViewController . scrollView . panGestureRecognizer . require ( toFail : overlayScrollView . panGestureRecognizer )
}
override func viewDidDisappear ( _ animated : Bool ) {
super . viewDidDisappear ( animated )
2021-04-09 11:31:43 +02:00
2021-04-01 08:39:15 +02:00
currentPostTimelineTableViewContentSizeObservation = nil
}
}
2021-04-06 10:43:08 +02:00
extension ProfileViewController {
private func bind ( userTimelineViewModel : UserTimelineViewModel ) {
viewModel . domain . assign ( to : \ . value , on : userTimelineViewModel . domain ) . store ( in : & disposeBag )
viewModel . userID . assign ( to : \ . value , on : userTimelineViewModel . userID ) . 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 )
2021-04-08 10:53:32 +02:00
viewModel . suspended . assign ( to : \ . value , on : userTimelineViewModel . isSuspended ) . store ( in : & disposeBag )
viewModel . name . assign ( to : \ . value , on : userTimelineViewModel . userDisplayName ) . store ( in : & disposeBag )
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 )
2021-04-08 12:52:35 +02:00
guard let mastodonUser = viewModel . mastodonUser . value else { return }
let activityViewController = UserProviderFacade . createActivityViewControllerForMastodonUser ( mastodonUser : mastodonUser , dependency : self )
coordinator . present (
scene : . activityViewController (
activityViewController : activityViewController ,
sourceView : nil ,
barButtonItem : sender
) ,
from : self ,
transition : . activityViewControllerPresent ( animated : true , completion : nil )
)
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 )
2021-04-02 12:50:08 +02:00
guard let mastodonUser = viewModel . mastodonUser . value else { return }
let composeViewModel = ComposeViewModel (
context : context ,
composeKind : . mention ( mastodonUserObjectID : mastodonUser . objectID )
)
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 {
postTimelineView . scrollView . contentOffset . y = 0
}
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
customScrollViewContainerController . scrollView . contentOffset . y = contentOffsetY
}
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
let headerScrollProgress = containerScrollView . contentOffset . y / 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 {
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 )
}
func profileHeaderViewController ( _ viewController : ProfileHeaderViewController , pageSegmentedControlValueChanged segmentedControl : UISegmentedControl , selectedSegmentIndex index : Int ) {
profileSegmentedViewController . pagingViewController . scrollToPage (
. at ( index : index ) ,
animated : true
)
}
2021-06-24 13:20:41 +02:00
2021-05-27 07:56:55 +02:00
func profileHeaderViewController ( _ viewController : ProfileHeaderViewController , profileFieldCollectionViewCell : ProfileFieldCollectionViewCell , activeLabel : ActiveLabel , didSelectActiveEntity entity : ActiveEntity ) {
2021-06-24 13:20:41 +02:00
// h a n d l e p r o f i l e f i e l d s i n t e r a c t i o n
2021-05-27 07:56:55 +02:00
switch entity . type {
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 . hashtag ( let hashtag , _ ) :
let hashtagTimelineViewModel = HashtagTimelineViewModel ( context : context , hashtag : hashtag )
coordinator . present ( scene : . hashtagTimeline ( viewModel : hashtagTimelineViewModel ) , from : nil , transition : . show )
2021-06-24 13:20:41 +02:00
case . mention ( _ , let userInfo ) :
guard let href = userInfo ? [ " href " ] as ? String else {
// c u r r e n t l y w e c a n n o t p r e s e n t p r o f i l e s c e n e w i t h o u t u s e r I D
return
}
guard let url = URL ( string : href ) else { return }
coordinator . present ( scene : . safari ( url : url ) , from : nil , transition : . safariPresent ( animated : true , completion : nil ) )
2021-05-27 07:56:55 +02:00
default :
break
}
}
2021-04-01 08:39:15 +02:00
}
// 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 )
2021-04-15 05:21:33 +02:00
// u p d a t e s e g e m e n t e d c o n t r o l
if index < profileHeaderViewController . pageSegmentedControl . numberOfSegments {
profileHeaderViewController . pageSegmentedControl . selectedSegmentIndex = index
}
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
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize ( scrollView : postTimelineViewController . scrollView )
postTimelineViewController . scrollView . panGestureRecognizer . require ( toFail : overlayScrollView . panGestureRecognizer )
}
}
// 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 {
2021-04-28 14:10:17 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , avatarImageViewDidPressed imageView : UIImageView ) {
guard let mastodonUser = viewModel . mastodonUser . value else { return }
guard let avatar = imageView . image else { return }
let meta = MediaPreviewViewModel . ProfileAvatarImagePreviewMeta (
accountObjectID : mastodonUser . objectID ,
preloadThumbnailImage : avatar
)
let pushTransitionItem = MediaPreviewTransitionItem (
source : . profileAvatar ( profileHeaderView ) ,
previewableViewController : self
)
pushTransitionItem . aspectRatio = CGSize ( width : 100 , height : 100 )
pushTransitionItem . sourceImageView = imageView
pushTransitionItem . sourceImageViewCornerRadius = ProfileHeaderView . avatarImageViewCornerRadius
pushTransitionItem . initialFrame = {
let initialFrame = imageView . superview ! . convert ( imageView . frame , to : nil )
assert ( initialFrame != . zero )
return initialFrame
} ( )
pushTransitionItem . image = avatar
let mediaPreviewViewModel = MediaPreviewViewModel (
context : context ,
meta : meta ,
pushTransitionItem : pushTransitionItem
)
DispatchQueue . main . async {
self . coordinator . present ( scene : . mediaPreview ( viewModel : mediaPreviewViewModel ) , from : self , transition : . custom ( transitioningDelegate : self . mediaPreviewTransitionController ) )
}
}
2021-04-28 14:36:10 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , bannerImageViewDidPressed imageView : UIImageView ) {
2021-04-29 11:05:35 +02:00
// n o t p r e v i e w h e a d e r b a n n e r w h e n e d i t i n g
guard ! viewModel . isEditing . value else { return }
2021-04-28 14:36:10 +02:00
guard let mastodonUser = viewModel . mastodonUser . value else { return }
guard let header = imageView . image else { return }
let meta = MediaPreviewViewModel . ProfileBannerImagePreviewMeta (
accountObjectID : mastodonUser . objectID ,
preloadThumbnailImage : header
)
let pushTransitionItem = MediaPreviewTransitionItem (
source : . profileBanner ( profileHeaderView ) ,
previewableViewController : self
)
pushTransitionItem . aspectRatio = header . size
pushTransitionItem . sourceImageView = imageView
pushTransitionItem . initialFrame = {
let initialFrame = imageView . superview ! . convert ( imageView . frame , to : nil )
assert ( initialFrame != . zero )
return initialFrame
} ( )
pushTransitionItem . image = header
let mediaPreviewViewModel = MediaPreviewViewModel (
context : context ,
meta : meta ,
pushTransitionItem : pushTransitionItem
)
DispatchQueue . main . async {
self . coordinator . present ( scene : . mediaPreview ( viewModel : mediaPreviewViewModel ) , from : self , transition : . custom ( transitioningDelegate : self . mediaPreviewTransitionController ) )
}
}
2021-04-02 12:13:45 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , relationshipButtonDidPressed button : ProfileRelationshipActionButton ) {
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 }
2021-06-24 13:20:41 +02:00
2021-04-09 11:31:43 +02:00
if profileHeaderViewController . viewModel . isProfileInfoEdited ( ) {
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
profileHeaderViewController . viewModel . updateProfileInfo ( )
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] completion in
guard let self = self else { return }
2021-06-24 13:20:41 +02:00
defer {
// 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
switch completion {
case . failure ( let error ) :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: update profile info fail: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , error . localizedDescription )
case . finished :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: update profile info success " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
} receiveValue : { [ weak self ] _ in
guard let self = self else { return }
self . viewModel . isEditing . value = false
}
. store ( in : & disposeBag )
} 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 ( )
. receive ( on : RunLoop . 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 . 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 :
2021-04-02 12:13:45 +02:00
UserProviderFacade . toggleUserFollowRelationship ( provider : self )
. sink { _ in
2021-04-08 10:53:32 +02:00
// TODO: h a n d l e e r r o r
2021-04-02 12:13:45 +02:00
} receiveValue : { _ in
2021-04-08 10:53:32 +02:00
// d o n o t h i n g
2021-04-02 12:13:45 +02:00
}
. store ( in : & disposeBag )
case . muting :
guard let mastodonUser = viewModel . mastodonUser . value else { return }
let name = mastodonUser . displayNameWithFallback
let alertController = UIAlertController (
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnmuteUser . message ( name ) ,
preferredStyle : . alert
)
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 }
2021-05-06 12:03:58 +02:00
UserProviderFacade . toggleUserMuteRelationship ( provider : self , cell : nil )
2021-04-02 12:13:45 +02:00
. sink { _ in
// d o n o t h i n g
} receiveValue : { _ in
// d o n o t h i n g
}
. store ( in : & self . context . disposeBag )
}
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 mastodonUser = viewModel . mastodonUser . value else { return }
let name = mastodonUser . displayNameWithFallback
let alertController = UIAlertController (
title : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUsre . title ,
message : L10n . Scene . Profile . RelationshipActionAlert . ConfirmUnblockUsre . message ( name ) ,
preferredStyle : . alert
)
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 }
2021-05-06 12:03:58 +02:00
UserProviderFacade . toggleUserBlockRelationship ( provider : self , cell : nil )
2021-04-02 12:13:45 +02:00
. sink { _ in
// d o n o t h i n g
} receiveValue : { _ in
// d o n o t h i n g
}
. store ( in : & self . context . disposeBag )
}
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-04-02 12:13:45 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , activeLabel : ActiveLabel , entityDidPressed entity : ActiveEntity ) {
switch entity . type {
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 ) )
2021-05-27 07:56:55 +02:00
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 )
2021-04-02 12:13:45 +02:00
default :
break
}
}
2021-04-01 08:39:15 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , profileStatusDashboardView : ProfileStatusDashboardView , postDashboardMeterViewDidPressed dashboardMeterView : ProfileStatusDashboardMeterView ) {
2021-05-27 07:56:55 +02:00
2021-04-01 08:39:15 +02:00
}
2021-05-27 07:56:55 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , profileStatusDashboardView : ProfileStatusDashboardView , followingDashboardMeterViewDidPressed followingDashboardMeterView : ProfileStatusDashboardMeterView ) {
2021-04-01 08:39:15 +02:00
}
2021-05-27 07:56:55 +02:00
func profileHeaderView ( _ profileHeaderView : ProfileHeaderView , profileStatusDashboardView : ProfileStatusDashboardView , followersDashboardMeterViewDidPressed followersDashboardMeterView : ProfileStatusDashboardMeterView ) {
2021-04-01 08:39:15 +02:00
}
}
// 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 overlayScrollView }
2021-02-23 09:45:00 +01:00
}
2021-05-21 09:23:02 +02:00
2021-05-21 11:42:14 +02:00
extension ProfileViewController {
override var keyCommands : [ UIKeyCommand ] ? {
if ! viewModel . isEditing . value {
return segmentedControlNavigateKeyCommands
}
return nil
}
}
// 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
extension ProfileViewController : SegmentedControlNavigateable {
var navigateableSegmentedControl : UISegmentedControl {
profileHeaderViewController . pageSegmentedControl
}
@objc func segmentedControlNavigateKeyCommandHandlerRelay ( _ sender : UIKeyCommand ) {
segmentedControlNavigateKeyCommandHandler ( sender )
}
}