2021-02-07 07:42:50 +01:00
//
// H o m e T i m e l i n 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 s x i a o j i a n o n 2 0 2 1 / 2 / 5 .
//
import os . log
import UIKit
import AVKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import AlamofireImage
2021-11-03 07:24:52 +01:00
import StoreKit
2022-01-27 14:23:39 +01:00
import MastodonAsset
2022-10-08 07:43:06 +02:00
import MastodonCore
2022-04-13 14:43:16 +02:00
import MastodonUI
2022-10-08 07:43:06 +02:00
import MastodonLocalization
2021-02-07 07:42:50 +01:00
2021-04-28 09:02:34 +02:00
final class HomeTimelineViewController : UIViewController , NeedsDependency , MediaPreviewableViewController {
2021-02-07 07:42:50 +01:00
2021-09-28 13:58:14 +02:00
let logger = Logger ( subsystem : " HomeTimelineViewController " , category : " UI " )
2021-02-07 07:42:50 +01:00
weak var context : AppContext ! { willSet { precondition ( ! isViewLoaded ) } }
weak var coordinator : SceneCoordinator ! { willSet { precondition ( ! isViewLoaded ) } }
var disposeBag = Set < AnyCancellable > ( )
2022-10-09 14:07:57 +02:00
var viewModel : HomeTimelineViewModel !
2021-02-07 07:42:50 +01:00
2021-04-28 09:02:34 +02:00
let mediaPreviewTransitionController = MediaPreviewTransitionController ( )
2021-07-09 07:16:58 +02:00
let friendsAssetImageView : UIImageView = {
let imageView = UIImageView ( )
imageView . image = Asset . Asset . friends . image
imageView . contentMode = . scaleAspectFill
return imageView
} ( )
2021-04-28 09:02:34 +02:00
2021-04-21 08:46:31 +02:00
lazy var emptyView : UIStackView = {
let emptyView = UIStackView ( )
emptyView . axis = . vertical
emptyView . distribution = . fill
emptyView . isLayoutMarginsRelativeArrangement = true
return emptyView
} ( )
2021-03-29 11:44:52 +02:00
let titleView = HomeTimelineNavigationBarTitleView ( )
2021-02-23 09:45:00 +01:00
let settingBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
2021-10-29 08:58:09 +02:00
barButtonItem . tintColor = ThemeService . tintColor
2022-05-06 09:17:26 +02:00
barButtonItem . image = Asset . ObjectsAndTools . gear . image . withRenderingMode ( . alwaysTemplate )
2022-03-03 12:51:12 +01:00
barButtonItem . accessibilityLabel = L10n . Common . Controls . Actions . settings
2021-02-23 09:45:00 +01:00
return barButtonItem
} ( )
2021-02-07 07:42:50 +01:00
let tableView : UITableView = {
let tableView = ControlContainableTableView ( )
2021-02-23 08:16:55 +01:00
tableView . register ( StatusTableViewCell . self , forCellReuseIdentifier : String ( describing : StatusTableViewCell . self ) )
2021-02-07 07:42:50 +01:00
tableView . register ( TimelineMiddleLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) )
tableView . register ( TimelineBottomLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineBottomLoaderTableViewCell . self ) )
tableView . rowHeight = UITableView . automaticDimension
tableView . separatorStyle = . none
tableView . backgroundColor = . clear
return tableView
} ( )
2021-03-29 11:44:52 +02:00
let publishProgressView : UIProgressView = {
let progressView = UIProgressView ( progressViewStyle : . bar )
progressView . alpha = 0
return progressView
} ( )
2022-11-14 23:09:59 +01:00
let refreshControl = RefreshControl ( )
2021-02-07 07:42:50 +01:00
deinit {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
2021-02-23 09:45:00 +01:00
2021-02-07 07:42:50 +01:00
}
extension HomeTimelineViewController {
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2021-07-05 10:07:17 +02:00
2021-02-22 09:20:44 +01:00
title = L10n . Scene . HomeTimeline . title
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
. receive ( on : RunLoop . main )
. sink { [ weak self ] theme in
guard let self = self else { return }
self . view . backgroundColor = theme . secondarySystemBackgroundColor
}
. store ( in : & disposeBag )
2022-02-10 09:43:26 +01:00
viewModel . $ displaySettingBarButtonItem
2021-09-24 13:58:50 +02:00
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] displaySettingBarButtonItem in
guard let self = self else { return }
2021-09-26 12:29:08 +02:00
#if DEBUG
// d i s p l a y d e b u g m e n u
2022-05-06 09:17:26 +02:00
self . navigationItem . rightBarButtonItem = {
2021-09-26 12:29:08 +02:00
let barButtonItem = UIBarButtonItem ( )
barButtonItem . image = UIImage ( systemName : " ellipsis.circle " )
barButtonItem . menu = self . debugMenu
return barButtonItem
} ( )
#else
2022-05-06 09:17:26 +02:00
self . navigationItem . rightBarButtonItem = displaySettingBarButtonItem ? self . settingBarButtonItem : nil
2021-09-26 12:29:08 +02:00
#endif
2021-09-24 13:58:50 +02:00
}
. store ( in : & disposeBag )
2021-10-28 13:17:41 +02:00
#if DEBUG
// l o n g p r e s s t o t r i g g e r d e b u g m e n u
settingBarButtonItem . menu = debugMenu
#else
settingBarButtonItem . target = self
settingBarButtonItem . action = #selector ( HomeTimelineViewController . settingBarButtonItemPressed ( _ : ) )
#endif
2022-03-18 17:30:00 +01:00
#if SNAPSHOT
titleView . logoButton . menu = self . debugMenu
titleView . button . menu = self . debugMenu
#endif
2021-03-29 11:44:52 +02:00
navigationItem . titleView = titleView
titleView . delegate = self
viewModel . homeTimelineNavigationBarTitleViewModel . state
2021-06-17 10:31:34 +02:00
. removeDuplicates ( )
2021-06-17 13:43:16 +02:00
. receive ( on : DispatchQueue . main )
2021-03-29 11:44:52 +02:00
. sink { [ weak self ] state in
guard let self = self else { return }
self . titleView . configure ( state : state )
}
. store ( in : & disposeBag )
2021-11-03 07:24:52 +01:00
viewModel . homeTimelineNavigationBarTitleViewModel . state
. removeDuplicates ( )
. filter { $0 = = . publishedButton }
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] _ in
guard let self = self else { return }
guard UserDefaults . shared . lastVersionPromptedForReview = = nil else { return }
guard UserDefaults . shared . processCompletedCount > 3 else { return }
guard let windowScene = self . view . window ? . windowScene else { return }
let version = UIApplication . appVersion ( )
UserDefaults . shared . lastVersionPromptedForReview = version
SKStoreReviewController . requestReview ( in : windowScene )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
tableView . refreshControl = refreshControl
refreshControl . addTarget ( self , action : #selector ( HomeTimelineViewController . refreshControlValueChanged ( _ : ) ) , for : . valueChanged )
tableView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( tableView )
2022-11-17 17:45:27 +01:00
tableView . pinToParent ( )
2021-03-29 11:44:52 +02:00
2022-10-31 13:41:19 +01:00
// / / l a y o u t p u b l i s h p r o g r e s s
2021-03-29 11:44:52 +02:00
publishProgressView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( publishProgressView )
NSLayoutConstraint . activate ( [
publishProgressView . topAnchor . constraint ( equalTo : view . layoutMarginsGuide . topAnchor ) ,
publishProgressView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
publishProgressView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
] )
2021-02-07 07:42:50 +01:00
viewModel . tableView = tableView
tableView . delegate = self
viewModel . setupDiffableDataSource (
2022-01-27 14:23:39 +01:00
tableView : tableView ,
2021-03-03 09:12:48 +01:00
statusTableViewCellDelegate : self ,
2021-02-07 07:42:50 +01:00
timelineMiddleLoaderTableViewCellDelegate : self
)
2022-02-10 09:43:26 +01:00
// s e t u p b a t c h f e t c h
viewModel . listBatchFetchViewModel . setup ( scrollView : tableView )
viewModel . listBatchFetchViewModel . shouldFetch
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] _ in
guard let self = self else { return }
guard self . view . window != nil else { return }
self . viewModel . loadOldestStateMachine . enter ( HomeTimelineViewModel . LoadOldestState . Loading . self )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
// b i n d r e f r e s h c o n t r o l
2022-01-27 14:23:39 +01:00
viewModel . didLoadLatest
2021-02-07 07:42:50 +01:00
. receive ( on : DispatchQueue . main )
2022-01-27 14:23:39 +01:00
. sink { [ weak self ] _ in
2021-02-07 07:42:50 +01:00
guard let self = self else { return }
2022-01-27 14:23:39 +01:00
UIView . animate ( withDuration : 0.5 ) { [ weak self ] in
guard let self = self else { return }
self . refreshControl . endRefreshing ( )
} completion : { _ in }
2021-02-07 07:42:50 +01:00
}
. store ( in : & disposeBag )
2021-03-29 11:44:52 +02:00
2022-10-31 13:41:19 +01:00
context . publisherService . $ currentPublishProgress
2021-03-29 11:44:52 +02:00
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] progress in
guard let self = self else { return }
2022-10-31 13:41:19 +01:00
let progress = Float ( progress )
2021-03-29 11:44:52 +02:00
guard progress > 0 else {
let dismissAnimator = UIViewPropertyAnimator ( duration : 0.1 , curve : . easeInOut )
dismissAnimator . addAnimations {
self . publishProgressView . alpha = 0
}
dismissAnimator . addCompletion { _ in
self . publishProgressView . setProgress ( 0 , animated : false )
}
dismissAnimator . startAnimation ( )
return
}
if self . publishProgressView . alpha = = 0 {
let progressAnimator = UIViewPropertyAnimator ( duration : 0.1 , curve : . easeOut )
progressAnimator . addAnimations {
self . publishProgressView . alpha = 1
}
progressAnimator . startAnimation ( )
}
self . publishProgressView . setProgress ( progress , animated : true )
}
. store ( in : & disposeBag )
2021-04-22 09:45:32 +02:00
viewModel . timelineIsEmpty
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isEmpty in
if isEmpty {
self ? . showEmptyView ( )
} else {
self ? . emptyView . removeFromSuperview ( )
}
}
. store ( in : & disposeBag )
2021-09-28 13:58:14 +02:00
NotificationCenter . default
. publisher ( for : . statusBarTapped , object : nil )
. throttle ( for : 0.5 , scheduler : DispatchQueue . main , latest : false )
. sink { [ weak self ] notification in
guard let self = self else { return }
guard let _ = self . view . window else { return } // d i s p l a y i n g
// h t t p s : / / d e v e l o p e r . l i m n e o s . n e t / i n d e x . p h p ? i o s = 1 3 . 1 . 3 & f r a m e w o r k = U I K i t C o r e . f r a m e w o r k & h e a d e r = U I S t a t u s B a r T a p A c t i o n . h
guard let action = notification . object as AnyObject ? ,
let xPosition = action . value ( forKey : " xPosition " ) as ? Double
else { return }
let viewFrameInWindow = self . view . convert ( self . view . frame , to : nil )
guard xPosition >= viewFrameInWindow . minX && xPosition <= viewFrameInWindow . maxX else { return }
// w o r k s o n i O S 1 4
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : receive notification \( xPosition ) " )
// c h e c k i f s c r o l l t o t o p
guard self . shouldRestoreScrollPosition ( ) else { return }
self . restorePositionWhenScrollToTop ( )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
}
2021-04-01 08:39:15 +02:00
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
2022-01-27 14:23:39 +01:00
refreshControl . endRefreshing ( )
tableView . deselectRow ( with : transitionCoordinator , animated : animated )
2021-04-13 13:46:42 +02:00
2021-04-01 08:39:15 +02:00
// n e e d s t r i g g e r m a n u a l l y a f t e r o n b o a r d i n g d i s m i s s
2022-04-13 14:43:16 +02:00
setNeedsStatusBarAppearanceUpdate ( )
2021-04-01 08:39:15 +02:00
}
2021-02-07 07:42:50 +01:00
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
viewModel . viewDidAppear . send ( )
2022-02-10 09:43:26 +01:00
if let timestamp = viewModel . lastAutomaticFetchTimestamp {
2021-09-28 13:58:14 +02:00
let now = Date ( )
if now . timeIntervalSince ( timestamp ) > 60 {
2022-02-10 09:43:26 +01:00
self . viewModel . lastAutomaticFetchTimestamp = now
2021-09-28 13:58:14 +02:00
self . viewModel . homeTimelineNeedRefresh . send ( )
} else {
// d o n o t h i n g
}
} else {
2021-07-07 14:13:33 +02:00
self . viewModel . homeTimelineNeedRefresh . send ( )
2021-02-07 07:42:50 +01:00
}
}
override func viewWillTransition ( to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) {
super . viewWillTransition ( to : size , with : coordinator )
coordinator . animate { _ in
// d o n o t h i n g
} completion : { _ in
// f i x A u t o L a y o u t c e l l h e i g h t n o t u p d a t e a f t e r r o t a t e i s s u e
self . viewModel . cellFrameCache . removeAllObjects ( )
self . tableView . reloadData ( )
}
}
}
extension HomeTimelineViewController {
2021-04-21 08:46:31 +02:00
func showEmptyView ( ) {
if emptyView . superview != nil {
return
}
view . addSubview ( emptyView )
emptyView . translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint . activate ( [
2021-07-09 07:16:58 +02:00
emptyView . topAnchor . constraint ( equalTo : view . layoutMarginsGuide . topAnchor ) ,
emptyView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
emptyView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
emptyView . bottomAnchor . constraint ( equalTo : view . layoutMarginsGuide . bottomAnchor )
2021-04-21 08:46:31 +02:00
] )
2021-04-21 11:58:56 +02:00
if emptyView . arrangedSubviews . count > 0 {
return
}
2021-04-21 08:46:31 +02:00
let findPeopleButton : PrimaryActionButton = {
let button = PrimaryActionButton ( )
button . setTitle ( L10n . Common . Controls . Actions . findPeople , for : . normal )
button . addTarget ( self , action : #selector ( HomeTimelineViewController . findPeopleButtonPressed ( _ : ) ) , for : . touchUpInside )
return button
} ( )
NSLayoutConstraint . activate ( [
findPeopleButton . heightAnchor . constraint ( equalToConstant : 46 )
] )
let manuallySearchButton : HighlightDimmableButton = {
let button = HighlightDimmableButton ( )
button . titleLabel ? . font = UIFontMetrics ( forTextStyle : . headline ) . scaledFont ( for : . systemFont ( ofSize : 15 , weight : . semibold ) )
button . setTitle ( L10n . Common . Controls . Actions . manuallySearch , for : . normal )
2022-06-02 11:31:23 +02:00
button . setTitleColor ( Asset . Colors . brand . color , for : . normal )
2021-04-21 08:46:31 +02:00
button . addTarget ( self , action : #selector ( HomeTimelineViewController . manuallySearchButtonPressed ( _ : ) ) , for : . touchUpInside )
return button
} ( )
2021-07-09 07:16:58 +02:00
let topPaddingView = UIView ( )
let bottomPaddingView = UIView ( )
emptyView . addArrangedSubview ( topPaddingView )
emptyView . addArrangedSubview ( friendsAssetImageView )
emptyView . addArrangedSubview ( bottomPaddingView )
topPaddingView . translatesAutoresizingMaskIntoConstraints = false
bottomPaddingView . translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint . activate ( [
topPaddingView . heightAnchor . constraint ( equalTo : bottomPaddingView . heightAnchor , multiplier : 0.8 ) ,
] )
let buttonContainerStackView = UIStackView ( )
emptyView . addArrangedSubview ( buttonContainerStackView )
buttonContainerStackView . isLayoutMarginsRelativeArrangement = true
buttonContainerStackView . layoutMargins = UIEdgeInsets ( top : 0 , left : 32 , bottom : 22 , right : 32 )
buttonContainerStackView . axis = . vertical
buttonContainerStackView . spacing = 17
buttonContainerStackView . addArrangedSubview ( findPeopleButton )
buttonContainerStackView . addArrangedSubview ( manuallySearchButton )
2021-04-21 08:46:31 +02:00
}
}
extension HomeTimelineViewController {
@objc private func findPeopleButtonPressed ( _ sender : PrimaryActionButton ) {
2022-10-09 14:07:57 +02:00
let suggestionAccountViewModel = SuggestionAccountViewModel ( context : context , authContext : viewModel . authContext )
2022-02-10 12:30:41 +01:00
suggestionAccountViewModel . delegate = viewModel
2022-10-09 14:07:57 +02:00
_ = coordinator . present (
2022-02-10 12:30:41 +01:00
scene : . suggestionAccount ( viewModel : suggestionAccountViewModel ) ,
from : self ,
transition : . modal ( animated : true , completion : nil )
)
2021-04-21 08:46:31 +02:00
}
@objc private func manuallySearchButtonPressed ( _ sender : UIButton ) {
2021-07-15 14:38:32 +02:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2022-10-09 15:40:02 +02:00
let searchDetailViewModel = SearchDetailViewModel ( authContext : viewModel . authContext )
2022-11-17 23:02:43 +01:00
_ = coordinator . present ( scene : . searchDetail ( viewModel : searchDetailViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-21 08:46:31 +02:00
}
2021-02-07 07:42:50 +01:00
2021-02-23 09:45:00 +01: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 }
2022-10-09 14:07:57 +02:00
let settingsViewModel = SettingsViewModel ( context : context , authContext : viewModel . authContext , setting : setting )
2022-11-17 23:02:43 +01:00
_ = coordinator . present ( scene : . settings ( viewModel : settingsViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-02-23 09:45:00 +01:00
}
2022-05-06 09:17:26 +02:00
2022-11-14 23:09:59 +01:00
@objc private func refreshControlValueChanged ( _ sender : RefreshControl ) {
2021-02-07 07:42:50 +01:00
guard viewModel . loadLatestStateMachine . enter ( HomeTimelineViewModel . LoadLatestState . Loading . self ) else {
sender . endRefreshing ( )
return
}
}
2021-02-26 11:27:47 +01:00
@objc func signOutAction ( _ sender : UIAction ) {
2022-02-11 12:27:14 +01:00
Task { @ MainActor in
2022-10-09 14:07:57 +02:00
try await context . authenticationService . signOutMastodonUser ( authenticationBox : viewModel . authContext . mastodonAuthenticationBox )
2022-02-11 12:27:14 +01:00
self . coordinator . setup ( )
2021-02-26 11:27:47 +01:00
}
}
2021-02-07 07:42:50 +01:00
}
// MARK: - U I S c r o l l V i e w D e l e g a t e
extension HomeTimelineViewController {
func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
2021-09-28 13:58:14 +02:00
switch scrollView {
case tableView :
viewModel . homeTimelineNavigationBarTitleViewModel . handleScrollViewDidScroll ( scrollView )
default :
break
}
}
func scrollViewShouldScrollToTop ( _ scrollView : UIScrollView ) -> Bool {
switch scrollView {
case tableView :
2021-09-29 11:05:40 +02:00
let indexPath = IndexPath ( row : 0 , section : 0 )
guard viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) != nil else {
return true
}
// s a v e p o s i t i o n
2021-09-28 13:58:14 +02:00
savePositionBeforeScrollToTop ( )
2021-09-29 11:05:40 +02:00
// o v e r r i d e b y c u s t o m s c r o l l T o R o w
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
return false
2021-09-28 13:58:14 +02:00
default :
assertionFailure ( )
return true
}
}
private func savePositionBeforeScrollToTop ( ) {
2021-09-29 11:05:40 +02:00
// c h e c k s a v e a c t i o n i n t e r v a l
// s h o u l d n o t f a s t t h a n 0 . 5 s t o p r e v e n t s a v e w h e n s c r o l l T o T o p o n - f l y i n g
2022-01-27 14:23:39 +01:00
if let record = viewModel . scrollPositionRecord {
2021-09-29 11:05:40 +02:00
let now = Date ( )
guard now . timeIntervalSince ( record . timestamp ) > 0.5 else {
// s k i p t h i s s a v e a c t i o n
return
}
}
2021-09-28 13:58:14 +02:00
guard let diffableDataSource = viewModel . diffableDataSource else { return }
guard let anchorIndexPaths = tableView . indexPathsForVisibleRows ? . sorted ( ) else { return }
guard ! anchorIndexPaths . isEmpty else { return }
let anchorIndexPath = anchorIndexPaths [ anchorIndexPaths . count / 2 ]
guard let anchorItem = diffableDataSource . itemIdentifier ( for : anchorIndexPath ) else { return }
2021-04-13 13:46:42 +02:00
2021-09-28 13:58:14 +02:00
let offset : CGFloat = {
guard let anchorCell = tableView . cellForRow ( at : anchorIndexPath ) else { return 0 }
let cellFrameInView = tableView . convert ( anchorCell . frame , to : view )
return cellFrameInView . origin . y
} ( )
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : save position record for \( anchorIndexPath ) with offset: \( offset ) " )
2022-01-27 14:23:39 +01:00
viewModel . scrollPositionRecord = HomeTimelineViewModel . ScrollPositionRecord (
2021-09-28 13:58:14 +02:00
item : anchorItem ,
offset : offset ,
timestamp : Date ( )
)
}
private func shouldRestoreScrollPosition ( ) -> Bool {
// c h e c k i f s c r o l l t o t o p
guard self . tableView . safeAreaInsets . top > 0 else { return false }
let zeroOffset = - self . tableView . safeAreaInsets . top
return abs ( self . tableView . contentOffset . y - zeroOffset ) < 2.0
}
private func restorePositionWhenScrollToTop ( ) {
guard let diffableDataSource = self . viewModel . diffableDataSource else { return }
2022-01-27 14:23:39 +01:00
guard let record = self . viewModel . scrollPositionRecord ,
2021-09-28 13:58:14 +02:00
let indexPath = diffableDataSource . indexPath ( for : record . item )
else { return }
2022-01-27 14:23:39 +01:00
tableView . scrollToRow ( at : indexPath , at : . middle , animated : true )
viewModel . scrollPositionRecord = nil
2021-02-07 07:42:50 +01: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 HomeTimelineViewController : AuthContextProvider {
var authContext : AuthContext { viewModel . authContext }
}
2021-02-07 07:42:50 +01:00
// MARK: - U I T a b l e V i e w D e l e g a t e
2022-01-27 14:23:39 +01:00
extension HomeTimelineViewController : UITableViewDelegate , AutoGenerateTableViewDelegate {
// s o u r c e r y : i n l i n e : H o m e T i m e l i n e V i e w C o n t r o l l e r . A u t o G e n e r a t e T a b l e V i e w D e l e g a t e
// G e n e r a t e d u s i n g S o u r c e r y
// D O N O T E D I T
2021-04-13 13:46:42 +02:00
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
aspectTableView ( tableView , didSelectRowAt : indexPath )
2021-03-10 14:19:56 +01:00
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , contextMenuConfigurationForRowAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
return aspectTableView ( tableView , contextMenuConfigurationForRowAt : indexPath , point : point )
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return aspectTableView ( tableView , previewForHighlightingContextMenuWithConfiguration : configuration )
}
func tableView ( _ tableView : UITableView , previewForDismissingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return aspectTableView ( tableView , previewForDismissingContextMenuWithConfiguration : configuration )
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , willPerformPreviewActionForMenuWith configuration : UIContextMenuConfiguration , animator : UIContextMenuInteractionCommitAnimating ) {
aspectTableView ( tableView , willPerformPreviewActionForMenuWith : configuration , animator : animator )
}
2022-01-27 14:23:39 +01:00
// s o u r c e r y : e n d
2021-02-07 07:42:50 +01:00
}
// MARK: - T i m e l i n e M i d d l e L o a d e r T a b l e V i e w C e l l D e l e g a t e
extension HomeTimelineViewController : TimelineMiddleLoaderTableViewCellDelegate {
func timelineMiddleLoaderTableViewCell ( _ cell : TimelineMiddleLoaderTableViewCell , loadMoreButtonDidPressed button : UIButton ) {
guard let diffableDataSource = viewModel . diffableDataSource else { return }
guard let indexPath = tableView . indexPath ( for : cell ) else { return }
guard let item = diffableDataSource . itemIdentifier ( for : indexPath ) else { return }
2022-01-27 14:23:39 +01:00
Task {
await viewModel . loadMore ( item : item )
2021-02-07 07:42:50 +01:00
}
}
}
// MARK: - S c r o l l V i e w C o n t a i n e r
extension HomeTimelineViewController : ScrollViewContainer {
2022-05-13 11:23:35 +02:00
var scrollView : UIScrollView { return tableView }
2021-02-07 07:42:50 +01:00
func scrollToTop ( animated : Bool ) {
2022-01-29 12:51:40 +01:00
if scrollView . contentOffset . y < scrollView . frame . height ,
viewModel . loadLatestStateMachine . canEnterState ( HomeTimelineViewModel . LoadLatestState . Loading . self ) ,
( scrollView . contentOffset . y + scrollView . adjustedContentInset . top ) = = 0.0 ,
! refreshControl . isRefreshing {
scrollView . scrollRectToVisible ( CGRect ( origin : CGPoint ( x : 0 , y : - refreshControl . frame . height ) , size : CGSize ( width : 1 , height : 1 ) ) , animated : animated )
DispatchQueue . main . async { [ weak self ] in
guard let self = self else { return }
self . refreshControl . beginRefreshing ( )
self . refreshControl . sendActions ( for : . valueChanged )
}
} else {
let indexPath = IndexPath ( row : 0 , section : 0 )
guard viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) != nil else { return }
// s a v e p o s i t i o n
savePositionBeforeScrollToTop ( )
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
}
2021-02-07 07:42:50 +01:00
}
}
2021-02-24 09:11:48 +01:00
// MARK: - S t a t u s T a b l e V i e w C e l l D e l e g a t e
2022-01-27 14:23:39 +01:00
extension HomeTimelineViewController : StatusTableViewCellDelegate { }
2021-03-29 11:44:52 +02:00
// MARK: - H o m e T i m e l i n e N a v i g a t i o n B a r T i t l e V i e w D e l e g a t e
extension HomeTimelineViewController : HomeTimelineNavigationBarTitleViewDelegate {
2021-06-21 12:58:58 +02:00
func homeTimelineNavigationBarTitleView ( _ titleView : HomeTimelineNavigationBarTitleView , logoButtonDidPressed sender : UIButton ) {
2021-09-28 13:58:14 +02:00
if shouldRestoreScrollPosition ( ) {
restorePositionWhenScrollToTop ( )
} else {
savePositionBeforeScrollToTop ( )
scrollToTop ( animated : true )
}
2021-06-21 12:58:58 +02:00
}
2021-03-29 11:44:52 +02:00
func homeTimelineNavigationBarTitleView ( _ titleView : HomeTimelineNavigationBarTitleView , buttonDidPressed sender : UIButton ) {
switch titleView . state {
case . newPostButton :
guard let diffableDataSource = viewModel . diffableDataSource else { return }
let indexPath = IndexPath ( row : 0 , section : 0 )
guard diffableDataSource . itemIdentifier ( for : indexPath ) != nil else { return }
2021-09-29 11:05:40 +02:00
savePositionBeforeScrollToTop ( )
2021-03-29 11:44:52 +02:00
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
case . offlineButton :
// TODO: r e t r y
break
case . publishedButton :
break
default :
break
}
}
}
2021-05-21 09:23:02 +02:00
2022-02-16 12:47:51 +01:00
extension HomeTimelineViewController {
override var keyCommands : [ UIKeyCommand ] ? {
return navigationKeyCommands + statusNavigationKeyCommands
}
}
// MARK: - S t a t u s T a b l e V i e w C o n t r o l l e r N a v i g a t e a b l e
extension HomeTimelineViewController : StatusTableViewControllerNavigateable {
@objc func navigateKeyCommandHandlerRelay ( _ sender : UIKeyCommand ) {
navigateKeyCommandHandler ( sender )
}
@objc func statusKeyCommandHandlerRelay ( _ sender : UIKeyCommand ) {
statusKeyCommandHandler ( sender )
}
}