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
protocol StatusCell : DisposeBagCollectable {
var statusView : StatusView { get }
var pollCountdownSubscription : AnyCancellable ? { get set }
}
2021-01-28 09:10:30 +01: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-01-28 09:10:30 +01:00
static func tableViewDiffableDataSource (
for tableView : UITableView ,
dependency : NeedsDependency ,
managedObjectContext : NSManagedObjectContext ,
timestampUpdatePublisher : AnyPublisher < Date , Never > ,
2021-03-03 09:12:48 +01:00
statusTableViewCellDelegate : StatusTableViewCellDelegate ,
2021-04-13 13:46:42 +02:00
timelineMiddleLoaderTableViewCellDelegate : TimelineMiddleLoaderTableViewCellDelegate ? ,
threadReplyLoaderTableViewCellDelegate : ThreadReplyLoaderTableViewCellDelegate ?
2021-02-24 09:11:48 +01:00
) -> UITableViewDiffableDataSource < StatusSection , Item > {
2021-04-13 13:46:42 +02:00
UITableViewDiffableDataSource ( tableView : tableView ) { [
weak dependency ,
weak statusTableViewCellDelegate ,
weak timelineMiddleLoaderTableViewCellDelegate ,
weak threadReplyLoaderTableViewCellDelegate
] tableView , indexPath , item -> UITableViewCell ? in
guard let dependency = dependency else { return UITableViewCell ( ) }
2021-03-03 09:12:48 +01:00
guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell ( ) }
2021-01-28 09:10:30 +01:00
switch item {
2021-02-24 09:11:48 +01:00
case . homeTimelineIndex ( objectID : let objectID , let attribute ) :
2021-02-23 08:16:55 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusTableViewCell . self ) , for : indexPath ) as ! StatusTableViewCell
2021-02-07 07:42:50 +01:00
// c o n f i g u r e c e l l
managedObjectContext . performAndWait {
let timelineIndex = managedObjectContext . object ( with : objectID ) as ! HomeTimelineIndex
2021-03-10 07:36:28 +01:00
StatusSection . configure (
cell : cell ,
2021-04-28 13:56:30 +02:00
indexPath : indexPath ,
2021-03-10 07:36:28 +01:00
dependency : dependency ,
2021-03-10 14:19:56 +01:00
readableLayoutFrame : tableView . readableContentGuide . layoutFrame ,
timestampUpdatePublisher : timestampUpdatePublisher ,
2021-04-01 08:39:15 +02:00
status : timelineIndex . status ,
2021-03-10 14:19:56 +01:00
requestUserID : timelineIndex . userID ,
statusItemAttribute : attribute
2021-03-10 07:36:28 +01:00
)
2021-02-07 07:42:50 +01:00
}
2021-03-03 09:12:48 +01:00
cell . delegate = statusTableViewCellDelegate
2021-02-07 07:42:50 +01:00
return cell
2021-04-13 13:46:42 +02:00
case . status ( let objectID , let attribute ) ,
. root ( let objectID , let attribute ) ,
. reply ( let objectID , let attribute ) ,
. leaf ( let objectID , let attribute ) :
2021-02-23 08:16:55 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : StatusTableViewCell . self ) , for : indexPath ) as ! StatusTableViewCell
2021-02-08 11:29:27 +01:00
let activeMastodonAuthenticationBox = dependency . context . authenticationService . activeMastodonAuthenticationBox . value
let requestUserID = activeMastodonAuthenticationBox ? . userID ? ? " "
2021-01-28 09:10:30 +01:00
// c o n f i g u r e c e l l
managedObjectContext . performAndWait {
2021-04-01 08:39:15 +02:00
let status = managedObjectContext . object ( with : objectID ) as ! Status
2021-03-10 07:36:28 +01:00
StatusSection . configure (
cell : cell ,
2021-04-28 13:56:30 +02:00
indexPath : indexPath ,
2021-03-10 07:36:28 +01:00
dependency : dependency ,
2021-03-10 14:19:56 +01:00
readableLayoutFrame : tableView . readableContentGuide . layoutFrame ,
timestampUpdatePublisher : timestampUpdatePublisher ,
2021-04-01 08:39:15 +02:00
status : status ,
2021-03-10 14:19:56 +01:00
requestUserID : requestUserID ,
statusItemAttribute : attribute
2021-03-10 07:36:28 +01:00
)
2021-04-13 13:46:42 +02:00
switch item {
case . root :
StatusSection . configureThreadMeta ( cell : cell , status : status )
ManagedObjectObserver . observe ( object : status . reblog ? ? status )
. receive ( on : DispatchQueue . main )
. sink { _ in
// d o n o t h i n g
} receiveValue : { change in
guard case . update ( let object ) = change . changeType ,
let status = object as ? Status else { return }
StatusSection . configureThreadMeta ( cell : cell , status : status )
}
. store ( in : & cell . disposeBag )
default :
break
}
2021-01-28 09:10:30 +01:00
}
2021-03-03 09:12:48 +01:00
cell . delegate = statusTableViewCellDelegate
2021-04-13 13:46:42 +02:00
return cell
case . leafBottomLoader :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : ThreadReplyLoaderTableViewCell . self ) , for : indexPath ) as ! ThreadReplyLoaderTableViewCell
cell . delegate = threadReplyLoaderTableViewCellDelegate
2021-01-28 09:10:30 +01:00
return cell
2021-04-01 08:39:15 +02:00
case . publicMiddleLoader ( let upperTimelineStatusID ) :
2021-02-07 07:42:50 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) , for : indexPath ) as ! TimelineMiddleLoaderTableViewCell
cell . delegate = timelineMiddleLoaderTableViewCellDelegate
2021-04-01 08:39:15 +02:00
timelineMiddleLoaderTableViewCellDelegate ? . configure ( cell : cell , upperTimelineStatusID : upperTimelineStatusID , timelineIndexobjectID : nil )
2021-02-07 07:42:50 +01:00
return cell
case . homeMiddleLoader ( let upperTimelineIndexObjectID ) :
2021-02-04 08:09:58 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) , for : indexPath ) as ! TimelineMiddleLoaderTableViewCell
cell . delegate = timelineMiddleLoaderTableViewCellDelegate
2021-04-01 08:39:15 +02:00
timelineMiddleLoaderTableViewCellDelegate ? . configure ( cell : cell , upperTimelineStatusID : nil , timelineIndexobjectID : upperTimelineIndexObjectID )
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
cell . startAnimating ( )
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
2021-03-16 12:28:52 +01:00
cell . startAnimating ( )
2021-02-03 06:01:50 +01:00
return cell
2021-04-06 10:43:08 +02:00
case . emptyStateHeader ( let attribute ) :
let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : TimelineHeaderTableViewCell . self ) , for : indexPath ) as ! TimelineHeaderTableViewCell
StatusSection . configureEmptyStateHeader ( cell : cell , attribute : attribute )
return cell
2021-04-23 03:37:18 +02:00
case . reportStatus :
return UITableViewCell ( )
2021-01-28 09:10:30 +01:00
}
}
}
2021-03-03 12:34:29 +01:00
}
extension StatusSection {
2021-04-06 10:43:08 +02:00
2021-01-28 09:10:30 +01:00
static func configure (
2021-04-19 12:06:02 +02:00
cell : StatusCell ,
2021-04-28 13:56:30 +02:00
indexPath : IndexPath ,
2021-03-10 07:36:28 +01:00
dependency : NeedsDependency ,
2021-02-23 12:18:34 +01:00
readableLayoutFrame : CGRect ? ,
2021-02-02 07:10:25 +01:00
timestampUpdatePublisher : AnyPublisher < Date , Never > ,
2021-04-01 08:39:15 +02:00
status : Status ,
2021-02-24 09:11:48 +01:00
requestUserID : String ,
2021-03-05 06:41:48 +01:00
statusItemAttribute : Item . StatusAttribute
2021-04-16 14:06:36 +02:00
) {
2021-02-23 08:16:55 +01:00
// s e t h e a d e r
2021-04-01 08:39:15 +02:00
StatusSection . configureHeader ( cell : cell , status : status )
ManagedObjectObserver . observe ( object : status )
2021-03-10 12:12:53 +01:00
. receive ( on : DispatchQueue . main )
. sink { _ in
// d o n o t h i n g
2021-04-26 11:41:24 +02:00
} receiveValue : { [ weak cell ] change in
guard let cell = cell else { return }
2021-03-10 12:12:53 +01:00
guard case . update ( let object ) = change . changeType ,
2021-04-01 08:39:15 +02:00
let newStatus = object as ? Status else { return }
StatusSection . configureHeader ( cell : cell , status : newStatus )
2021-03-10 12:12:53 +01:00
}
. store ( in : & cell . disposeBag )
2021-02-23 08:16:55 +01:00
2021-03-10 06:36:01 +01:00
// s e t n a m e u s e r n a m e
2021-02-23 12:18:34 +01:00
cell . statusView . nameLabel . text = {
2021-04-01 08:39:15 +02:00
let author = ( status . reblog ? ? status ) . author
2021-02-23 12:18:34 +01:00
return author . displayName . isEmpty ? author . username : author . displayName
} ( )
2021-04-01 08:39:15 +02:00
cell . statusView . usernameLabel . text = " @ " + ( status . reblog ? ? status ) . author . acct
2021-03-10 06:36:01 +01:00
// s e t a v a t a r
2021-04-01 08:39:15 +02:00
if let reblog = status . reblog {
2021-03-10 06:36:01 +01:00
cell . statusView . avatarButton . isHidden = true
cell . statusView . avatarStackedContainerButton . isHidden = false
cell . statusView . avatarStackedContainerButton . topLeadingAvatarStackedImageView . configure ( with : AvatarConfigurableViewConfiguration ( avatarImageURL : reblog . author . avatarImageURL ( ) ) )
2021-04-01 08:39:15 +02:00
cell . statusView . avatarStackedContainerButton . bottomTrailingAvatarStackedImageView . configure ( with : AvatarConfigurableViewConfiguration ( avatarImageURL : status . author . avatarImageURL ( ) ) )
2021-03-10 06:36:01 +01:00
} else {
cell . statusView . avatarButton . isHidden = false
cell . statusView . avatarStackedContainerButton . isHidden = true
2021-04-01 08:39:15 +02:00
cell . statusView . configure ( with : AvatarConfigurableViewConfiguration ( avatarImageURL : status . author . avatarImageURL ( ) ) )
2021-03-10 06:36:01 +01:00
}
2021-02-23 08:16:55 +01:00
2021-02-01 11:06:29 +01:00
// s e t t e x t
2021-04-01 08:39:15 +02:00
cell . statusView . activeTextLabel . configure ( content : ( status . reblog ? ? status ) . content )
2021-02-23 12:18:34 +01:00
// p r e p a r e m e d i a a t t a c h m e n t s
2021-04-01 08:39:15 +02:00
let mediaAttachments = Array ( ( status . reblog ? ? status ) . mediaAttachments ? ? [ ] ) . sorted { $0 . index . compare ( $1 . index ) = = . orderedAscending }
2021-02-23 12:18:34 +01:00
// s e t i m a g e
let mosiacImageViewModel = MosaicImageViewModel ( mediaAttachments : mediaAttachments )
let imageViewMaxSize : CGSize = {
let maxWidth : CGFloat = {
// u s e t i m e l i n e P o s t V i e w w i d t h a s c o n t a i n e r w i d t h
// t h a t w i d t h f o l l o w s r e a d a b l e w i d t h a n d k e e p c o n s t a n t w i d t h a f t e r r o t a t e
let containerFrame = readableLayoutFrame ? ? cell . statusView . frame
var containerWidth = containerFrame . width
containerWidth -= 10
containerWidth -= StatusView . avatarImageSize . width
return containerWidth
} ( )
let scale : CGFloat = {
switch mosiacImageViewModel . metas . count {
2021-03-08 04:42:10 +01:00
case 1 : return 1.3
default : return 0.7
2021-02-23 12:18:34 +01:00
}
} ( )
return CGSize ( width : maxWidth , height : maxWidth * scale )
} ( )
2021-04-16 14:06:36 +02:00
let blurhashImageCache = dependency . context . documentStore . blurhashImageCache
let mosaics : [ MosaicImageViewContainer . ConfigurableMosaic ] = {
if mosiacImageViewModel . metas . count = = 1 {
let meta = mosiacImageViewModel . metas [ 0 ]
let mosaic = cell . statusView . statusMosaicImageViewContainer . setupImageView ( aspectRatio : meta . size , maxSize : imageViewMaxSize )
return [ mosaic ]
} else {
let mosaics = cell . statusView . statusMosaicImageViewContainer . setupImageViews ( count : mosiacImageViewModel . metas . count , maxHeight : imageViewMaxSize . height )
return mosaics
}
} ( )
for ( i , mosiac ) in mosaics . enumerated ( ) {
let ( imageView , blurhashOverlayImageView ) = mosiac
let meta = mosiacImageViewModel . metas [ i ]
let blurhashImageDataKey = meta . url . absoluteString as NSString
if let blurhashImageData = blurhashImageCache . object ( forKey : meta . url . absoluteString as NSString ) ,
let image = UIImage ( data : blurhashImageData as Data ) {
blurhashOverlayImageView . image = image
} else {
meta . blurhashImagePublisher ( )
. receive ( on : DispatchQueue . main )
2021-04-26 11:41:24 +02:00
. sink { [ weak cell ] image in
2021-04-16 14:06:36 +02:00
blurhashOverlayImageView . image = image
image ? . pngData ( ) . flatMap {
blurhashImageCache . setObject ( $0 as NSData , forKey : blurhashImageDataKey )
}
}
. store ( in : & cell . disposeBag )
}
2021-02-23 12:18:34 +01:00
imageView . af . setImage (
withURL : meta . url ,
placeholderImage : UIImage . placeholder ( color : . systemFill ) ,
imageTransition : . crossDissolve ( 0.2 )
2021-04-16 14:06:36 +02:00
) { response in
switch response . result {
case . success :
statusItemAttribute . isImageLoaded . value = true
case . failure :
break
}
}
Publishers . CombineLatest (
statusItemAttribute . isImageLoaded ,
2021-04-19 12:33:11 +02:00
statusItemAttribute . isRevealing
2021-02-23 12:18:34 +01:00
)
2021-04-16 14:06:36 +02:00
. receive ( on : DispatchQueue . main )
2021-04-26 11:41:24 +02:00
. sink { [ weak cell ] isImageLoaded , isMediaRevealing in
guard let cell = cell else { return }
2021-04-16 14:06:36 +02:00
guard isImageLoaded else {
blurhashOverlayImageView . alpha = 1
blurhashOverlayImageView . isHidden = false
return
}
2021-04-16 14:29:08 +02:00
blurhashOverlayImageView . alpha = isMediaRevealing ? 0 : 1
if isMediaRevealing {
let animator = UIViewPropertyAnimator ( duration : 0.33 , curve : . easeInOut )
animator . addAnimations {
blurhashOverlayImageView . alpha = isMediaRevealing ? 0 : 1
}
animator . startAnimation ( )
} else {
cell . statusView . drawContentWarningImageView ( )
2021-04-16 14:06:36 +02:00
}
2021-02-23 12:18:34 +01:00
}
2021-04-16 14:06:36 +02:00
. store ( in : & cell . disposeBag )
2021-02-23 12:18:34 +01:00
}
2021-03-02 12:33:33 +01:00
cell . statusView . statusMosaicImageViewContainer . isHidden = mosiacImageViewModel . metas . isEmpty
2021-03-02 09:27:11 +01:00
2021-03-08 04:42:10 +01:00
// s e t a u d i o
if let audioAttachment = mediaAttachments . filter ( { $0 . type = = . audio } ) . first {
cell . statusView . audioView . isHidden = false
2021-03-11 06:11:13 +01:00
AudioContainerViewModel . configure ( cell : cell , audioAttachment : audioAttachment , audioService : dependency . context . audioPlaybackService )
2021-03-08 04:42:10 +01:00
} else {
cell . statusView . audioView . isHidden = true
}
2021-03-10 07:36:28 +01:00
// s e t G I F & v i d e o
let playerViewMaxSize : CGSize = {
let maxWidth : CGFloat = {
// u s e s t a t u s V i e w w i d t h a s c o n t a i n e r w i d t h
// t h a t w i d t h f o l l o w s r e a d a b l e w i d t h a n d k e e p c o n s t a n t w i d t h a f t e r r o t a t e
let containerFrame = readableLayoutFrame ? ? cell . statusView . frame
return containerFrame . width
} ( )
let scale : CGFloat = 1.3
return CGSize ( width : maxWidth , height : maxWidth * scale )
} ( )
2021-03-11 08:34:30 +01:00
2021-03-10 07:36:28 +01:00
if let videoAttachment = mediaAttachments . filter ( { $0 . type = = . gifv || $0 . type = = . video } ) . first ,
let videoPlayerViewModel = dependency . context . videoPlaybackService . dequeueVideoPlayerViewModel ( for : videoAttachment )
{
2021-04-19 12:06:02 +02:00
var parent : UIViewController ?
var playerViewControllerDelegate : AVPlayerViewControllerDelegate ? = nil
switch cell {
case is StatusTableViewCell :
let statusTableViewCell = cell as ! StatusTableViewCell
parent = statusTableViewCell . delegate ? . parent ( )
playerViewControllerDelegate = statusTableViewCell . delegate ? . playerViewControllerDelegate
2021-04-19 12:16:28 +02:00
case is NotificationStatusTableViewCell :
let notificationTableViewCell = cell as ! NotificationStatusTableViewCell
2021-04-19 12:06:02 +02:00
parent = notificationTableViewCell . delegate ? . parent ( )
2021-04-26 09:58:49 +02:00
case is ReportedStatusTableViewCell :
let reportTableViewCell = cell as ! ReportedStatusTableViewCell
parent = reportTableViewCell . dependency
2021-04-19 12:06:02 +02:00
default :
parent = nil
assertionFailure ( " unknown cell " )
}
2021-03-11 12:23:44 +01:00
let playerContainerView = cell . statusView . playerContainerView
let playerViewController = playerContainerView . setupPlayer (
2021-03-10 07:36:28 +01:00
aspectRatio : videoPlayerViewModel . videoSize ,
maxSize : playerViewMaxSize ,
parent : parent
)
2021-04-19 12:06:02 +02:00
playerViewController . delegate = playerViewControllerDelegate
2021-03-10 07:36:28 +01:00
playerViewController . player = videoPlayerViewModel . player
playerViewController . showsPlaybackControls = videoPlayerViewModel . videoKind != . gif
2021-03-12 08:41:57 +01:00
playerContainerView . setMediaKind ( kind : videoPlayerViewModel . videoKind )
2021-03-15 10:53:06 +01:00
if videoPlayerViewModel . videoKind = = . gif {
playerContainerView . setMediaIndicator ( isHidden : false )
} else {
videoPlayerViewModel . timeControlStatus . sink { timeControlStatus in
UIView . animate ( withDuration : 0.33 ) {
switch timeControlStatus {
case . playing :
playerContainerView . setMediaIndicator ( isHidden : true )
case . paused , . waitingToPlayAtSpecifiedRate :
playerContainerView . setMediaIndicator ( isHidden : false )
@ unknown default :
assertionFailure ( )
}
}
}
. store ( in : & cell . disposeBag )
}
2021-03-11 12:23:44 +01:00
playerContainerView . isHidden = false
2021-03-10 07:36:28 +01:00
} else {
2021-03-11 12:06:15 +01:00
cell . statusView . playerContainerView . playerViewController . player ? . pause ( )
cell . statusView . playerContainerView . playerViewController . player = nil
2021-03-10 07:36:28 +01:00
}
2021-04-16 14:06:36 +02:00
// s e t t e x t c o n t e n t w a r n i n g
StatusSection . configureContentWarningOverlay (
statusView : cell . statusView ,
status : status ,
attribute : statusItemAttribute ,
documentStore : dependency . context . documentStore ,
animated : false
)
// o b s e r v e m o d e l c h a n g e
ManagedObjectObserver . observe ( object : status )
. receive ( on : DispatchQueue . main )
. sink { _ in
// d o n o t h i n g
2021-04-26 11:41:24 +02:00
} receiveValue : { [ weak dependency , weak cell ] change in
guard let cell = cell else { return }
2021-04-16 14:06:36 +02:00
guard let dependency = dependency else { return }
guard case . update ( let object ) = change . changeType ,
let status = object as ? Status else { return }
StatusSection . configureContentWarningOverlay (
statusView : cell . statusView ,
status : status ,
attribute : statusItemAttribute ,
documentStore : dependency . context . documentStore ,
animated : true
)
}
. store ( in : & cell . disposeBag )
2021-03-02 09:27:11 +01:00
// s e t p o l l
2021-04-01 08:39:15 +02:00
let poll = ( status . reblog ? ? status ) . poll
2021-03-09 08:18:43 +01:00
StatusSection . configurePoll (
2021-03-05 06:41:48 +01:00
cell : cell ,
poll : poll ,
requestUserID : requestUserID ,
updateProgressAnimated : false ,
timestampUpdatePublisher : timestampUpdatePublisher
)
2021-03-03 12:34:29 +01:00
if let poll = poll {
ManagedObjectObserver . observe ( object : poll )
. sink { _ in
// d o n o t h i n g
2021-04-26 11:41:24 +02:00
} receiveValue : { [ weak cell ] change in
guard let cell = cell else { return }
2021-03-08 04:42:10 +01:00
guard case . update ( let object ) = change . changeType ,
2021-03-03 12:34:29 +01:00
let newPoll = object as ? Poll else { return }
2021-03-09 08:18:43 +01:00
StatusSection . configurePoll (
2021-03-05 06:41:48 +01:00
cell : cell ,
poll : newPoll ,
requestUserID : requestUserID ,
updateProgressAnimated : true ,
timestampUpdatePublisher : timestampUpdatePublisher
)
2021-03-02 12:10:45 +01:00
}
2021-03-03 12:34:29 +01:00
. store ( in : & cell . disposeBag )
2021-03-02 12:10:45 +01:00
}
2021-03-03 12:34:29 +01:00
2021-04-19 12:06:02 +02:00
if let statusTableViewCell = cell as ? StatusTableViewCell {
2021-04-28 13:56:30 +02:00
// t o o l b a r
StatusSection . configureActionToolBar (
cell : statusTableViewCell ,
indexPath : indexPath ,
dependency : dependency ,
status : status ,
requestUserID : requestUserID
)
// s e p a r a t o r l i n e
2021-04-19 12:06:02 +02:00
statusTableViewCell . separatorLine . isHidden = statusItemAttribute . isSeparatorLineHidden
}
2021-04-13 13:46:42 +02:00
2021-02-01 11:06:29 +01:00
// s e t d a t e
2021-04-01 08:39:15 +02:00
let createdAt = ( status . reblog ? ? status ) . createdAt
2021-02-23 08:16:55 +01:00
cell . statusView . dateLabel . text = createdAt . shortTimeAgoSinceNow
2021-02-02 07:10:25 +01:00
timestampUpdatePublisher
2021-04-26 11:41:24 +02:00
. sink { [ weak cell ] _ in
guard let cell = cell else { return }
2021-02-23 08:16:55 +01:00
cell . statusView . dateLabel . text = createdAt . shortTimeAgoSinceNow
2021-02-02 07:10:25 +01:00
}
. store ( in : & cell . disposeBag )
2021-02-18 05:49:24 +01:00
2021-02-08 11:29:27 +01:00
// o b s e r v e m o d e l c h a n g e
2021-04-01 08:39:15 +02:00
ManagedObjectObserver . observe ( object : status . reblog ? ? status )
2021-02-08 11:29:27 +01:00
. receive ( on : DispatchQueue . main )
. sink { _ in
// d o n o t h i n g
2021-04-26 11:41:24 +02:00
} receiveValue : { [ weak dependency , weak cell ] change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
2021-02-18 05:49:24 +01:00
guard case . update ( let object ) = change . changeType ,
2021-04-01 08:39:15 +02:00
let status = object as ? Status else { return }
2021-04-28 13:56:30 +02:00
guard let cell = cell as ? StatusTableViewCell else { return }
2021-04-26 09:58:49 +02:00
StatusSection . configureActionToolBar (
cell : cell ,
2021-04-28 13:56:30 +02:00
indexPath : indexPath ,
2021-04-26 09:58:49 +02:00
dependency : dependency ,
status : status ,
requestUserID : requestUserID
)
2021-03-09 08:18:43 +01:00
2021-04-01 08:39:15 +02:00
os_log ( " %{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld " , ( #file as NSString ) . lastPathComponent , #line , #function , status . id , status . reblogsCount . intValue )
os_log ( " %{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld " , ( #file as NSString ) . lastPathComponent , #line , #function , status . id , status . favouritesCount . intValue )
2021-02-08 11:29:27 +01:00
}
. store ( in : & cell . disposeBag )
2021-01-28 09:10:30 +01:00
}
2021-04-13 13:46:42 +02:00
2021-04-16 14:06:36 +02:00
static func configureContentWarningOverlay (
statusView : StatusView ,
status : Status ,
attribute : Item . StatusAttribute ,
documentStore : DocumentStore ,
animated : Bool
) {
statusView . contentWarningOverlayView . blurContentWarningTitleLabel . text = {
2021-04-16 14:29:08 +02:00
let spoilerText = ( status . reblog ? ? status ) . spoilerText ? ? " "
2021-04-16 14:06:36 +02:00
if spoilerText . isEmpty {
return L10n . Common . Controls . Status . contentWarning
} else {
return L10n . Common . Controls . Status . contentWarningText ( spoilerText )
}
} ( )
let appStartUpTimestamp = documentStore . appStartUpTimestamp
switch ( status . reblog ? ? status ) . sensitiveType {
case . none :
statusView . revealContentWarningButton . isHidden = true
statusView . contentWarningOverlayView . isHidden = true
statusView . statusMosaicImageViewContainer . contentWarningOverlayView . isHidden = true
statusView . updateContentWarningDisplay ( isHidden : true , animated : false )
case . all :
statusView . revealContentWarningButton . isHidden = false
statusView . contentWarningOverlayView . isHidden = false
statusView . statusMosaicImageViewContainer . contentWarningOverlayView . isHidden = true
2021-04-19 12:33:11 +02:00
statusView . playerContainerView . contentWarningOverlayView . isHidden = true
2021-04-16 14:06:36 +02:00
if let revealedAt = status . revealedAt , revealedAt > appStartUpTimestamp {
statusView . updateRevealContentWarningButton ( isRevealing : true )
statusView . updateContentWarningDisplay ( isHidden : true , animated : animated )
2021-04-19 12:33:11 +02:00
attribute . isRevealing . value = true
2021-04-16 14:06:36 +02:00
} else {
statusView . updateRevealContentWarningButton ( isRevealing : false )
statusView . updateContentWarningDisplay ( isHidden : false , animated : animated )
2021-04-19 12:33:11 +02:00
attribute . isRevealing . value = false
2021-04-16 14:06:36 +02:00
}
case . media ( let isSensitive ) :
if ! isSensitive , documentStore . defaultRevealStatusDict [ status . id ] = = nil {
documentStore . defaultRevealStatusDict [ status . id ] = true
}
statusView . revealContentWarningButton . isHidden = false
statusView . contentWarningOverlayView . isHidden = true
statusView . statusMosaicImageViewContainer . contentWarningOverlayView . isHidden = false
2021-04-20 07:40:14 +02:00
statusView . playerContainerView . contentWarningOverlayView . isHidden = false
2021-04-16 14:06:36 +02:00
statusView . updateContentWarningDisplay ( isHidden : true , animated : false )
func updateContentOverlay ( ) {
let needsReveal : Bool = {
if documentStore . defaultRevealStatusDict [ status . id ] = = true {
return true
}
if let revealedAt = status . revealedAt , revealedAt > appStartUpTimestamp {
return true
}
return false
} ( )
2021-04-19 12:33:11 +02:00
attribute . isRevealing . value = needsReveal
2021-04-16 14:06:36 +02:00
if needsReveal {
statusView . updateRevealContentWarningButton ( isRevealing : true )
2021-04-19 12:33:11 +02:00
statusView . statusMosaicImageViewContainer . contentWarningOverlayView . update ( isRevealing : true , style : . visualEffectView )
statusView . playerContainerView . contentWarningOverlayView . update ( isRevealing : true , style : . visualEffectView )
2021-04-16 14:06:36 +02:00
} else {
statusView . updateRevealContentWarningButton ( isRevealing : false )
2021-04-19 12:33:11 +02:00
statusView . statusMosaicImageViewContainer . contentWarningOverlayView . update ( isRevealing : false , style : . visualEffectView )
statusView . playerContainerView . contentWarningOverlayView . update ( isRevealing : false , style : . visualEffectView )
2021-04-16 14:06:36 +02:00
}
}
if animated {
UIView . animate ( withDuration : 0.33 , delay : 0 , options : . curveEaseInOut ) {
updateContentOverlay ( )
} completion : { _ in
// d o n o t h i n g
}
} else {
updateContentOverlay ( )
}
}
}
2021-04-13 13:46:42 +02:00
static func configureThreadMeta (
cell : StatusTableViewCell ,
status : Status
) {
cell . selectionStyle = . none
cell . threadMetaView . dateLabel . text = {
let formatter = DateFormatter ( )
formatter . dateStyle = . medium
formatter . timeStyle = . short
return formatter . string ( from : status . createdAt )
} ( )
let reblogCountTitle : String = {
let count = status . reblogsCount . intValue
if count > 1 {
return L10n . Scene . Thread . Reblog . multiple ( String ( count ) )
} else {
return L10n . Scene . Thread . Reblog . single ( String ( count ) )
}
} ( )
cell . threadMetaView . reblogButton . setTitle ( reblogCountTitle , for : . normal )
let favoriteCountTitle : String = {
let count = status . favouritesCount . intValue
if count > 1 {
return L10n . Scene . Thread . Favorite . multiple ( String ( count ) )
} else {
return L10n . Scene . Thread . Favorite . single ( String ( count ) )
}
} ( )
cell . threadMetaView . favoriteButton . setTitle ( favoriteCountTitle , for : . normal )
cell . threadMetaView . isHidden = false
}
2021-03-16 04:41:56 +01:00
2021-03-10 12:12:53 +01:00
static func configureHeader (
2021-04-19 12:06:02 +02:00
cell : StatusCell ,
2021-04-01 08:39:15 +02:00
status : Status
2021-03-10 12:12:53 +01:00
) {
2021-04-01 08:39:15 +02:00
if status . reblog != nil {
2021-04-19 11:50:58 +02:00
cell . statusView . headerContainerView . isHidden = false
2021-04-14 09:24:54 +02:00
cell . statusView . headerIconLabel . attributedText = StatusView . iconAttributedString ( image : StatusView . reblogIconImage )
2021-03-10 12:12:53 +01:00
cell . statusView . headerInfoLabel . text = {
2021-04-01 08:39:15 +02:00
let author = status . author
2021-03-10 12:12:53 +01:00
let name = author . displayName . isEmpty ? author . username : author . displayName
2021-03-16 04:41:56 +01:00
return L10n . Common . Controls . Status . userReblogged ( name )
2021-03-10 12:12:53 +01:00
} ( )
2021-04-13 13:46:42 +02:00
} else if status . inReplyToID != nil {
2021-04-19 11:50:58 +02:00
cell . statusView . headerContainerView . isHidden = false
2021-03-16 04:48:33 +01:00
cell . statusView . headerIconLabel . attributedText = StatusView . iconAttributedString ( image : StatusView . replyIconImage )
2021-03-10 12:12:53 +01:00
cell . statusView . headerInfoLabel . text = {
2021-04-13 13:46:42 +02:00
guard let replyTo = status . replyTo else {
return L10n . Common . Controls . Status . userRepliedTo ( " - " )
}
2021-03-10 12:12:53 +01:00
let author = replyTo . author
let name = author . displayName . isEmpty ? author . username : author . displayName
return L10n . Common . Controls . Status . userRepliedTo ( name )
} ( )
} else {
2021-04-19 11:50:58 +02:00
cell . statusView . headerContainerView . isHidden = true
2021-03-10 12:12:53 +01:00
}
}
2021-03-09 08:18:43 +01:00
static func configureActionToolBar (
2021-04-28 13:56:30 +02:00
cell : StatusTableViewCell ,
indexPath : IndexPath ,
2021-04-26 09:58:49 +02:00
dependency : NeedsDependency ,
2021-04-01 08:39:15 +02:00
status : Status ,
2021-03-09 08:18:43 +01:00
requestUserID : String
) {
2021-04-01 08:39:15 +02:00
let status = status . reblog ? ? status
2021-03-09 08:18:43 +01:00
// s e t r e p l y
let replyCountTitle : String = {
2021-04-01 08:39:15 +02:00
let count = status . repliesCount ? . intValue ? ? 0
2021-03-09 08:18:43 +01:00
return StatusSection . formattedNumberTitleForActionButton ( count )
} ( )
cell . statusView . actionToolbarContainer . replyButton . setTitle ( replyCountTitle , for : . normal )
2021-03-15 11:19:45 +01:00
// s e t r e b l o g
2021-04-01 08:39:15 +02:00
let isReblogged = status . rebloggedBy . flatMap { $0 . contains ( where : { $0 . id = = requestUserID } ) } ? ? false
2021-03-15 11:19:45 +01:00
let reblogCountTitle : String = {
2021-04-01 08:39:15 +02:00
let count = status . reblogsCount . intValue
2021-03-09 08:18:43 +01:00
return StatusSection . formattedNumberTitleForActionButton ( count )
} ( )
2021-03-15 11:19:45 +01:00
cell . statusView . actionToolbarContainer . reblogButton . setTitle ( reblogCountTitle , for : . normal )
cell . statusView . actionToolbarContainer . isReblogButtonHighlight = isReblogged
2021-03-09 08:18:43 +01:00
// s e t l i k e
2021-04-01 08:39:15 +02:00
let isLike = status . favouritedBy . flatMap { $0 . contains ( where : { $0 . id = = requestUserID } ) } ? ? false
2021-03-09 08:18:43 +01:00
let favoriteCountTitle : String = {
2021-04-01 08:39:15 +02:00
let count = status . favouritesCount . intValue
2021-03-09 08:18:43 +01:00
return StatusSection . formattedNumberTitleForActionButton ( count )
} ( )
cell . statusView . actionToolbarContainer . favoriteButton . setTitle ( favoriteCountTitle , for : . normal )
cell . statusView . actionToolbarContainer . isFavoriteButtonHighlight = isLike
2021-04-26 09:58:49 +02:00
2021-04-30 08:55:02 +02:00
Publishers . CombineLatest (
dependency . context . blockDomainService . blockedDomains ,
ManagedObjectObserver . observe ( object : status . authorForUserProvider )
. assertNoFailure ( )
)
. receive ( on : DispatchQueue . main )
. sink { [ weak dependency , weak cell ] domains , change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
StatusSection . setupStatusMoreButtonMenu ( cell : cell , indexPath : indexPath , dependency : dependency , status : status )
}
. store ( in : & cell . disposeBag )
2021-04-28 13:56:30 +02:00
self . setupStatusMoreButtonMenu ( cell : cell , indexPath : indexPath , dependency : dependency , status : status )
2021-03-09 08:18:43 +01:00
}
static func configurePoll (
2021-04-19 12:06:02 +02:00
cell : StatusCell ,
2021-03-03 12:34:29 +01:00
poll : Poll ? ,
2021-03-05 06:41:48 +01:00
requestUserID : String ,
updateProgressAnimated : Bool ,
timestampUpdatePublisher : AnyPublisher < Date , Never >
2021-03-03 12:34:29 +01:00
) {
guard let poll = poll ,
2021-03-10 07:36:28 +01:00
let managedObjectContext = poll . managedObjectContext
else {
2021-03-03 12:34:29 +01:00
cell . statusView . pollTableView . isHidden = true
cell . statusView . pollStatusStackView . isHidden = true
cell . statusView . pollVoteButton . isHidden = true
return
}
cell . statusView . pollTableView . isHidden = false
cell . statusView . pollStatusStackView . isHidden = false
cell . statusView . pollVoteCountLabel . text = {
if poll . multiple {
let count = poll . votersCount ? . intValue ? ? 0
if count > 1 {
return L10n . Common . Controls . Status . Poll . VoterCount . single ( count )
} else {
return L10n . Common . Controls . Status . Poll . VoterCount . multiple ( count )
}
} else {
let count = poll . votesCount . intValue
if count > 1 {
return L10n . Common . Controls . Status . Poll . VoteCount . single ( count )
} else {
return L10n . Common . Controls . Status . Poll . VoteCount . multiple ( count )
}
}
} ( )
if poll . expired {
cell . pollCountdownSubscription = nil
cell . statusView . pollCountdownLabel . text = L10n . Common . Controls . Status . Poll . closed
} else if let expiresAt = poll . expiresAt {
cell . statusView . pollCountdownLabel . text = L10n . Common . Controls . Status . Poll . timeLeft ( expiresAt . shortTimeAgoSinceNow )
cell . pollCountdownSubscription = timestampUpdatePublisher
. sink { _ in
cell . statusView . pollCountdownLabel . text = L10n . Common . Controls . Status . Poll . timeLeft ( expiresAt . shortTimeAgoSinceNow )
}
} else {
2021-03-12 12:25:28 +01:00
// a s s e r t i o n F a i l u r e ( )
2021-03-03 12:34:29 +01:00
cell . pollCountdownSubscription = nil
cell . statusView . pollCountdownLabel . text = " - "
}
cell . statusView . pollTableView . allowsSelection = ! poll . expired
2021-03-05 08:53:36 +01:00
let votedOptions = poll . options . filter { option in
2021-03-10 07:36:28 +01:00
( option . votedBy ? ? Set ( ) ) . map ( \ . id ) . contains ( requestUserID )
2021-03-05 08:53:36 +01:00
}
let didVotedLocal = ! votedOptions . isEmpty
2021-03-10 07:36:28 +01:00
let didVotedRemote = ( poll . votedBy ? ? Set ( ) ) . map ( \ . id ) . contains ( requestUserID )
2021-03-05 08:53:36 +01:00
cell . statusView . pollVoteButton . isEnabled = didVotedLocal
cell . statusView . pollVoteButton . isHidden = ! poll . multiple ? true : ( didVotedRemote || poll . expired )
2021-03-03 12:34:29 +01:00
cell . statusView . pollTableViewDataSource = PollSection . tableViewDiffableDataSource (
for : cell . statusView . pollTableView ,
managedObjectContext : managedObjectContext
)
var snapshot = NSDiffableDataSourceSnapshot < PollSection , PollItem > ( )
snapshot . appendSections ( [ . main ] )
2021-03-05 08:53:36 +01:00
2021-03-03 12:34:29 +01:00
let pollItems = poll . options
. sorted ( by : { $0 . index . intValue < $1 . index . intValue } )
. map { option -> PollItem in
let attribute : PollItem . Attribute = {
let selectState : PollItem . Attribute . SelectState = {
2021-03-05 08:53:36 +01:00
// c h e c k d i d V o t e d R e m o t e l a t e r t o m a k e t h e l o c a l c h a n g e p o s s i b l e
2021-03-04 11:53:29 +01:00
if ! votedOptions . isEmpty {
2021-03-03 12:34:29 +01:00
return votedOptions . contains ( option ) ? . on : . off
} else if poll . expired {
return . none
2021-03-05 08:53:36 +01:00
} else if didVotedRemote , votedOptions . isEmpty {
2021-03-04 11:53:29 +01:00
return . none
2021-03-03 12:34:29 +01:00
} else {
return . off
}
} ( )
let voteState : PollItem . Attribute . VoteState = {
2021-03-05 07:23:26 +01:00
var needsReveal : Bool
if poll . expired {
needsReveal = true
2021-03-05 08:53:36 +01:00
} else if didVotedRemote {
2021-03-05 07:23:26 +01:00
needsReveal = true
} else {
needsReveal = false
}
guard needsReveal else { return . hidden }
2021-03-03 12:34:29 +01:00
let percentage : Double = {
guard poll . votesCount . intValue > 0 else { return 0.0 }
return Double ( option . votesCount ? . intValue ? ? 0 ) / Double ( poll . votesCount . intValue )
} ( )
let voted = votedOptions . isEmpty ? true : votedOptions . contains ( option )
2021-03-05 06:41:48 +01:00
return . reveal ( voted : voted , percentage : percentage , animated : updateProgressAnimated )
2021-03-03 12:34:29 +01:00
} ( )
return PollItem . Attribute ( selectState : selectState , voteState : voteState )
} ( )
let option = PollItem . opion ( objectID : option . objectID , attribute : attribute )
return option
}
snapshot . appendItems ( pollItems , toSection : . main )
cell . statusView . pollTableViewDataSource ? . apply ( snapshot , animatingDifferences : false , completion : nil )
}
2021-04-06 10:43:08 +02:00
static func configureEmptyStateHeader (
cell : TimelineHeaderTableViewCell ,
attribute : Item . EmptyStateHeaderAttribute
) {
cell . timelineHeaderView . iconImageView . image = attribute . reason . iconImage
cell . timelineHeaderView . messageLabel . text = attribute . reason . message
}
2021-01-28 09:10:30 +01:00
}
2021-02-24 09:11:48 +01:00
extension StatusSection {
2021-01-28 09:10:30 +01:00
private static func formattedNumberTitleForActionButton ( _ number : Int ? ) -> String {
guard let number = number , number > 0 else { return " " }
return String ( number )
}
2021-04-26 09:58:49 +02:00
private static func setupStatusMoreButtonMenu (
2021-04-28 13:56:30 +02:00
cell : StatusTableViewCell ,
indexPath : IndexPath ,
2021-04-26 09:58:49 +02:00
dependency : NeedsDependency ,
status : Status ) {
2021-04-28 13:56:30 +02:00
guard let userProvider = dependency as ? UserProvider else { fatalError ( ) }
2021-04-26 09:58:49 +02:00
guard let authenticationBox = dependency . context . authenticationService . activeMastodonAuthenticationBox . value else {
return
}
2021-04-29 04:50:10 +02:00
let author = status . authorForUserProvider
2021-04-28 13:56:30 +02:00
let canReport = authenticationBox . userID != author . id
2021-04-30 06:53:25 +02:00
let isInSameDomain = authenticationBox . domain = = author . domainFromAcct
2021-04-28 13:56:30 +02:00
let isMuting = ( author . mutingBy ? ? Set ( ) ) . map ( \ . id ) . contains ( authenticationBox . userID )
let isBlocking = ( author . blockingBy ? ? Set ( ) ) . map ( \ . id ) . contains ( authenticationBox . userID )
2021-04-30 08:55:02 +02:00
let isDomainBlocking = dependency . context . blockDomainService . blockedDomains . value . contains ( author . domainFromAcct )
2021-04-26 09:58:49 +02:00
cell . statusView . actionToolbarContainer . moreButton . showsMenuAsPrimaryAction = true
2021-04-30 08:55:02 +02:00
cell . statusView . actionToolbarContainer . moreButton . menu = UserProviderFacade . createProfileActionMenu (
for : author ,
isMuting : isMuting ,
isBlocking : isBlocking ,
canReport : canReport ,
isInSameDomain : isInSameDomain ,
isDomainBlocking : isDomainBlocking ,
provider : userProvider ,
cell : cell ,
indexPath : indexPath ,
sourceView : cell . statusView . actionToolbarContainer . moreButton ,
barButtonItem : nil ,
shareUser : nil ,
shareStatus : status
)
2021-04-26 09:58:49 +02:00
}
2021-01-28 09:10:30 +01:00
}