2021-01-28 09:10:30 +01:00
//
// T i m e l i n e S e c t i o n . 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 / 1 / 2 7 .
//
import Combine
import CoreData
import CoreDataStack
import os . log
import UIKit
2021-04-19 12:06:02 +02:00
import AVKit
2021-07-07 11:51:47 +02:00
import AlamofireImage
2021-06-28 13:41:41 +02:00
import MastodonMeta
2021-07-09 13:07:12 +02:00
import MastodonSDK
import NaturalLanguage
2022-01-27 14:23:39 +01:00
import MastodonUI
2021-06-28 13:41:41 +02:00
2021-02-24 09:11:48 +01:00
enum StatusSection : Equatable , Hashable {
2021-01-28 09:10:30 +01:00
case main
}
2021-02-24 09:11:48 +01:00
extension StatusSection {
2021-06-19 12:33:29 +02:00
2021-07-15 03:56:26 +02:00
static let logger = Logger ( subsystem : " StatusSection " , category : " logic " )
2022-01-27 14:23:39 +01:00
struct Configuration {
weak var statusTableViewCellDelegate : StatusTableViewCellDelegate ?
weak var timelineMiddleLoaderTableViewCellDelegate : TimelineMiddleLoaderTableViewCellDelegate ?
2022-02-15 12:44:45 +01:00
let filterContext : Mastodon . Entity . Filter . Context ?
let activeFilters : Published < [ Mastodon . Entity . Filter ] > . Publisher ?
2022-01-27 14:23:39 +01:00
}
2021-07-15 03:56:26 +02:00
2022-01-27 14:23:39 +01:00
static func diffableDataSource (
tableView : UITableView ,
context : AppContext ,
configuration : Configuration
) -> UITableViewDiffableDataSource < StatusSection , StatusItem > {
tableView . register ( StatusTableViewCell . self , forCellReuseIdentifier : String ( describing : StatusTableViewCell . self ) )
tableView . register ( TimelineMiddleLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) )
tableView . register ( StatusThreadRootTableViewCell . self , forCellReuseIdentifier : String ( describing : StatusThreadRootTableViewCell . self ) )
tableView . register ( TimelineBottomLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineBottomLoaderTableViewCell . self ) )
return UITableViewDiffableDataSource ( tableView : tableView ) { tableView , indexPath , item -> UITableViewCell ? in
2021-01-28 09:10:30 +01:00
switch item {
2022-01-27 14:23:39 +01:00
case . feed ( let record ) :
2021-02-23 08:16:55 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusTableViewCell . self ) , for : indexPath ) as ! StatusTableViewCell
2022-01-27 14:23:39 +01:00
context . managedObjectContext . performAndWait {
guard let feed = record . object ( in : context . managedObjectContext ) else { return }
configure (
context : context ,
tableView : tableView ,
cell : cell ,
viewModel : StatusTableViewCell . ViewModel ( value : . feed ( feed ) ) ,
configuration : configuration
)
2021-02-07 07:42:50 +01:00
}
return cell
2022-01-27 14:23:39 +01:00
case . feedLoader ( let record ) :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) , for : indexPath ) as ! TimelineMiddleLoaderTableViewCell
context . managedObjectContext . performAndWait {
guard let feed = record . object ( in : context . managedObjectContext ) else { return }
configure (
2021-03-10 07:36:28 +01:00
cell : cell ,
2022-01-27 14:23:39 +01:00
feed : feed ,
configuration : configuration
2021-03-10 07:36:28 +01:00
)
2021-05-12 12:26:53 +02:00
}
2021-04-13 13:46:42 +02:00
return cell
2022-01-27 14:23:39 +01:00
case . status ( let record ) :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusTableViewCell . self ) , for : indexPath ) as ! StatusTableViewCell
context . managedObjectContext . performAndWait {
guard let status = record . object ( in : context . managedObjectContext ) else { return }
configure (
context : context ,
tableView : tableView ,
cell : cell ,
viewModel : StatusTableViewCell . ViewModel ( value : . status ( status ) ) ,
configuration : configuration
)
}
2021-02-07 07:42:50 +01:00
return cell
2022-01-27 14:23:39 +01:00
case . thread ( let thread ) :
let cell = dequeueConfiguredReusableCell (
context : context ,
tableView : tableView ,
indexPath : indexPath ,
configuration : ThreadCellRegistrationConfiguration (
thread : thread ,
configuration : configuration
)
)
2021-02-04 08:09:58 +01:00
return cell
2021-04-13 13:46:42 +02:00
case . topLoader :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineBottomLoaderTableViewCell . self ) , for : indexPath ) as ! TimelineBottomLoaderTableViewCell
2022-01-27 14:23:39 +01:00
cell . activityIndicatorView . startAnimating ( )
2021-04-13 13:46:42 +02:00
return cell
2021-02-03 06:01:50 +01:00
case . bottomLoader :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineBottomLoaderTableViewCell . self ) , for : indexPath ) as ! TimelineBottomLoaderTableViewCell
2022-01-27 14:23:39 +01:00
cell . activityIndicatorView . startAnimating ( )
2021-07-15 11:26:04 +02:00
return cell
2022-01-27 14:23:39 +01:00
}
}
} // e n d f u n c
}
extension StatusSection {
struct ThreadCellRegistrationConfiguration {
let thread : StatusItem . Thread
let configuration : Configuration
}
static func dequeueConfiguredReusableCell (
context : AppContext ,
tableView : UITableView ,
indexPath : IndexPath ,
configuration : ThreadCellRegistrationConfiguration
) -> UITableViewCell {
let managedObjectContext = context . managedObjectContext
switch configuration . thread {
case . root ( let threadContext ) :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusThreadRootTableViewCell . self ) , for : indexPath ) as ! StatusThreadRootTableViewCell
managedObjectContext . performAndWait {
guard let status = threadContext . status . object ( in : managedObjectContext ) else { return }
StatusSection . configure (
context : context ,
tableView : tableView ,
cell : cell ,
viewModel : StatusThreadRootTableViewCell . ViewModel ( value : . status ( status ) ) ,
configuration : configuration . configuration
)
}
return cell
case . reply ( let threadContext ) ,
. leaf ( let threadContext ) :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusTableViewCell . self ) , for : indexPath ) as ! StatusTableViewCell
managedObjectContext . performAndWait {
guard let status = threadContext . status . object ( in : managedObjectContext ) else { return }
StatusSection . configure (
context : context ,
tableView : tableView ,
cell : cell ,
viewModel : StatusTableViewCell . ViewModel ( value : . status ( status ) ) ,
configuration : configuration . configuration
)
}
return cell
}
}
}
extension StatusSection {
public static func setupStatusPollDataSource (
context : AppContext ,
statusView : StatusView
) {
let managedObjectContext = context . managedObjectContext
statusView . pollTableViewDiffableDataSource = UITableViewDiffableDataSource < PollSection , PollItem > ( tableView : statusView . pollTableView ) { tableView , indexPath , item in
switch item {
case . option ( let record ) :
// F i x c e l l r e u s e a n i m a t i o n i s s u e
let cell : PollOptionTableViewCell = {
let _cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : PollOptionTableViewCell . self ) + " @ \( indexPath . row ) # \( indexPath . section ) " ) as ? PollOptionTableViewCell
_cell ? . prepareForReuse ( )
return _cell ? ? PollOptionTableViewCell ( )
} ( )
context . authenticationService . activeMastodonAuthenticationBox
. map { $0 as UserIdentifier ? }
. assign ( to : \ . userIdentifier , on : cell . pollOptionView . viewModel )
. store ( in : & cell . disposeBag )
managedObjectContext . performAndWait {
guard let option = record . object ( in : managedObjectContext ) else {
assertionFailure ( )
return
}
cell . pollOptionView . configure ( pollOption : option )
// t r i g g e r u p d a t e i f n e e d s
let needsUpdatePoll : Bool = {
// c h e c k f i r s t o p t i o n i n p o l l t o t r i g g e r u p d a t e p o l l o n l y o n c e
guard option . index = = 0 else { return false }
let poll = option . poll
guard ! poll . expired else {
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : poll expired. Skip update poll \( poll . id ) " )
return false
}
let now = Date ( )
let timeIntervalSinceUpdate = now . timeIntervalSince ( poll . updatedAt )
#if DEBUG
let autoRefreshTimeInterval : TimeInterval = 3 // s p e e d u p t e s t i n g
#else
let autoRefreshTimeInterval : TimeInterval = 30
#endif
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : skip update poll \( poll . id ) due to recent updated " )
return false
}
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : update poll \( poll . id ) … " )
return true
} ( )
if needsUpdatePoll , let authenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value
{
let pollRecord : ManagedObjectRecord < Poll > = . init ( objectID : option . poll . objectID )
Task { [ weak context ] in
guard let context = context else { return }
_ = try await context . apiService . poll (
poll : pollRecord ,
authenticationBox : authenticationBox
)
}
}
} // e n d m a n a g e d O b j e c t C o n t e x t . p e r f o r m A n d W a i t
2021-04-06 10:43:08 +02:00
return cell
2021-01-28 09:10:30 +01:00
}
}
2022-01-27 14:23:39 +01:00
var _snapshot = NSDiffableDataSourceSnapshot < PollSection , PollItem > ( )
_snapshot . appendSections ( [ . main ] )
if #available ( iOS 15.0 , * ) {
statusView . pollTableViewDiffableDataSource ? . applySnapshotUsingReloadData ( _snapshot )
} else {
statusView . pollTableViewDiffableDataSource ? . apply ( _snapshot , animatingDifferences : false )
}
}
}
extension StatusSection {
static func configure (
context : AppContext ,
tableView : UITableView ,
cell : StatusTableViewCell ,
viewModel : StatusTableViewCell . ViewModel ,
configuration : Configuration
) {
setupStatusPollDataSource (
context : context ,
statusView : cell . statusView
)
context . authenticationService . activeMastodonAuthenticationBox
. map { $0 as UserIdentifier ? }
. assign ( to : \ . userIdentifier , on : cell . statusView . viewModel )
. store ( in : & cell . disposeBag )
cell . configure (
tableView : tableView ,
viewModel : viewModel ,
delegate : configuration . statusTableViewCellDelegate
)
2022-02-15 12:44:45 +01:00
cell . statusView . viewModel . filterContext = configuration . filterContext
configuration . activeFilters ?
. assign ( to : \ . activeFilters , on : cell . statusView . viewModel )
. store ( in : & cell . disposeBag )
2022-01-27 14:23:39 +01:00
}
static func configure (
context : AppContext ,
tableView : UITableView ,
cell : StatusThreadRootTableViewCell ,
viewModel : StatusThreadRootTableViewCell . ViewModel ,
configuration : Configuration
) {
setupStatusPollDataSource (
context : context ,
statusView : cell . statusView
)
context . authenticationService . activeMastodonAuthenticationBox
. map { $0 as UserIdentifier ? }
. assign ( to : \ . userIdentifier , on : cell . statusView . viewModel )
. store ( in : & cell . disposeBag )
cell . configure (
tableView : tableView ,
viewModel : viewModel ,
delegate : configuration . statusTableViewCellDelegate
)
2022-02-15 12:44:45 +01:00
cell . statusView . viewModel . filterContext = configuration . filterContext
configuration . activeFilters ?
. assign ( to : \ . activeFilters , on : cell . statusView . viewModel )
. store ( in : & cell . disposeBag )
2022-01-27 14:23:39 +01:00
}
static func configure (
cell : TimelineMiddleLoaderTableViewCell ,
feed : Feed ,
configuration : Configuration
) {
cell . configure (
feed : feed ,
delegate : configuration . timelineMiddleLoaderTableViewCellDelegate
)
2021-01-28 09:10:30 +01:00
}
2022-01-27 14:23:39 +01:00
2021-03-03 12:34:29 +01:00
}