2022-10-03 01:34:06 +00:00
//
// T i m e l i n e S t a t u s C o l l e c t i o n V i e w C e l l . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 1 0 / 1 / 2 2 .
// C o p y r i g h t © 2 0 2 2 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import UIKit
2022-10-04 04:02:41 +00:00
import Pachyderm
@ MainActor
2022-10-05 21:40:00 +00:00
protocol TimelineStatusCollectionViewCellDelegate : AnyObject , TuskerNavigationDelegate , MenuActionProvider {
2022-10-06 02:28:10 +00:00
func statusCellNeedsReconfigure ( _ cell : TimelineStatusCollectionViewCell , animated : Bool )
2022-10-04 04:02:41 +00:00
}
2022-10-03 01:34:06 +00:00
class TimelineStatusCollectionViewCell : UICollectionViewListCell {
2022-10-05 21:40:00 +00:00
// MARK: S u b v i e w s
2022-10-03 01:34:06 +00:00
2022-10-06 02:28:10 +00:00
private lazy var reblogLabel = EmojiLabel ( ) . configure {
2022-10-03 01:34:06 +00:00
$0 . textColor = . secondaryLabel
2022-10-04 04:02:41 +00:00
// t h i s n e e d s t o h a v e a h i g h e r p r i o r t y t h a n t h e c o n t e n t c o n t a i n e r ' s z e r o h e i g h t c o n s t r a i n t
$0 . setContentHuggingPriority ( . defaultHigh , for : . vertical )
2022-10-06 02:28:10 +00:00
$0 . addGestureRecognizer ( UITapGestureRecognizer ( target : self , action : #selector ( reblogLabelPressed ) ) )
2022-10-03 01:34:06 +00:00
}
private lazy var mainContainer = UIView ( ) . configure {
avatarImageView . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( avatarImageView )
contentVStack . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( contentVStack )
metaIndicatorsView . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( metaIndicatorsView )
NSLayoutConstraint . activate ( [
avatarImageView . leadingAnchor . constraint ( equalTo : $0 . leadingAnchor ) ,
avatarImageView . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
contentVStack . leadingAnchor . constraint ( equalTo : avatarImageView . trailingAnchor , constant : 8 ) ,
contentVStack . trailingAnchor . constraint ( equalTo : $0 . trailingAnchor ) ,
contentVStack . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
contentVStack . bottomAnchor . constraint ( equalTo : $0 . bottomAnchor ) ,
metaIndicatorsView . leadingAnchor . constraint ( greaterThanOrEqualTo : $0 . leadingAnchor ) ,
metaIndicatorsView . trailingAnchor . constraint ( equalTo : avatarImageView . trailingAnchor ) ,
metaIndicatorsView . topAnchor . constraint ( equalTo : avatarImageView . bottomAnchor , constant : 4 ) ,
] )
}
2022-10-05 21:40:00 +00:00
private static let avatarImageViewSize : CGFloat = 50
2022-10-06 02:28:10 +00:00
private lazy var avatarImageView = CachedImageView ( cache : . avatars ) . configure {
2022-10-03 01:34:06 +00:00
$0 . layer . masksToBounds = true
NSLayoutConstraint . activate ( [
2022-10-05 21:40:00 +00:00
$0 . heightAnchor . constraint ( equalToConstant : TimelineStatusCollectionViewCell . avatarImageViewSize ) ,
$0 . widthAnchor . constraint ( equalToConstant : TimelineStatusCollectionViewCell . avatarImageViewSize ) ,
2022-10-03 01:34:06 +00:00
] )
2022-10-06 02:28:10 +00:00
$0 . isUserInteractionEnabled = true
$0 . addInteraction ( UIContextMenuInteraction ( delegate : self ) )
$0 . addInteraction ( UIDragInteraction ( delegate : self ) )
$0 . addGestureRecognizer ( UITapGestureRecognizer ( target : self , action : #selector ( accountPressed ) ) )
2022-10-03 01:34:06 +00:00
}
private let metaIndicatorsView = StatusMetaIndicatorsView ( )
private lazy var contentVStack = UIStackView ( arrangedSubviews : [
nameHStack ,
contentWarningLabel ,
collapseButton ,
2022-10-04 04:02:41 +00:00
contentContainer ,
2022-10-03 01:34:06 +00:00
] ) . configure {
$0 . axis = . vertical
$0 . spacing = 4
$0 . alignment = . fill
}
private lazy var nameHStack = UIStackView ( arrangedSubviews : [
displayNameLabel ,
usernameLabel ,
pinImageView ,
timestampLabel ,
] ) . configure {
$0 . axis = . horizontal
$0 . spacing = 4
2022-10-06 02:28:10 +00:00
$0 . addGestureRecognizer ( UITapGestureRecognizer ( target : self , action : #selector ( accountPressed ) ) )
2022-10-03 01:34:06 +00:00
}
private let displayNameLabel = EmojiLabel ( ) . configure {
$0 . font = UIFont ( descriptor : . preferredFontDescriptor ( withTextStyle : . body ) . addingAttributes ( [
. traits : [
UIFontDescriptor . TraitKey . weight : UIFont . Weight . semibold . rawValue ,
]
] ) , size : 0 )
$0 . setContentHuggingPriority ( . init ( 251 ) , for : . horizontal )
$0 . setContentCompressionResistancePriority ( . init ( 749 ) , for : . horizontal )
}
private let usernameLabel = UILabel ( ) . configure {
$0 . textColor = . secondaryLabel
$0 . font = UIFont ( descriptor : . preferredFontDescriptor ( withTextStyle : . body ) . addingAttributes ( [
. traits : [
UIFontDescriptor . TraitKey . weight : UIFont . Weight . light . rawValue ,
]
] ) , size : 0 )
$0 . setContentHuggingPriority ( . init ( 249 ) , for : . horizontal )
$0 . setContentCompressionResistancePriority ( . init ( 748 ) , for : . horizontal )
}
private let pinImageView = UIImageView ( image : UIImage ( systemName : " pin.fill " ) ) . configure {
$0 . tintColor = . secondaryLabel
$0 . setContentHuggingPriority ( . init ( 251 ) , for : . horizontal )
}
private let timestampLabel = UILabel ( ) . configure {
$0 . textColor = . secondaryLabel
$0 . font = UIFont ( descriptor : . preferredFontDescriptor ( withTextStyle : . body ) . addingAttributes ( [
. traits : [
UIFontDescriptor . TraitKey . weight : UIFont . Weight . light . rawValue ,
]
] ) , size : 0 )
}
2022-10-05 21:40:00 +00:00
private lazy var contentWarningLabel = EmojiLabel ( ) . configure {
2022-10-03 01:34:06 +00:00
$0 . textColor = . secondaryLabel
$0 . font = UIFont ( descriptor : . preferredFontDescriptor ( withTextStyle : . body ) . addingAttributes ( [
. traits : [
UIFontDescriptor . TraitKey . weight : UIFont . Weight . bold . rawValue ,
]
] ) , size : 0 )
2022-10-04 04:02:41 +00:00
// t h i s n e e d s t o h a v e a h i g h e r p r i o r t y t h a n t h e c o n t e n t c o n t a i n e r ' s z e r o h e i g h t c o n s t r a i n t
$0 . setContentHuggingPriority ( . defaultHigh , for : . vertical )
$0 . addGestureRecognizer ( UITapGestureRecognizer ( target : self , action : #selector ( collapseButtonPressed ) ) )
2022-10-03 01:34:06 +00:00
}
2022-10-04 04:02:41 +00:00
private lazy var collapseButton = UIButton ( configuration : {
2022-10-03 01:34:06 +00:00
var config = UIButton . Configuration . filled ( )
config . image = UIImage ( systemName : " chevron.down " )
return config
2022-10-04 04:02:41 +00:00
} ( ) ) . configure {
$0 . setContentHuggingPriority ( . defaultHigh , for : . vertical )
$0 . addTarget ( self , action : #selector ( collapseButtonPressed ) , for : . touchUpInside )
2022-10-03 01:34:06 +00:00
}
2022-10-04 04:02:41 +00:00
private let contentContainer = StatusContentContainer ( ) . configure {
$0 . setContentHuggingPriority ( . defaultLow , for : . vertical )
2022-10-03 01:34:06 +00:00
}
2022-10-04 04:02:41 +00:00
private var contentTextView : StatusContentTextView {
contentContainer . contentTextView
2022-10-03 01:34:06 +00:00
}
2022-10-04 04:02:41 +00:00
private var cardView : StatusCardView {
contentContainer . cardView
}
private var attachmentsView : AttachmentsContainerView {
contentContainer . attachmentsView
}
private var pollView : StatusPollView {
contentContainer . pollView
2022-10-03 01:34:06 +00:00
}
private var placeholderReplyButtonLeadingConstraint : NSLayoutConstraint !
private lazy var actionsContainer = UIView ( ) . configure {
replyButton . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( replyButton )
favoriteButton . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( favoriteButton )
reblogButton . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( reblogButton )
moreButton . translatesAutoresizingMaskIntoConstraints = false
$0 . addSubview ( moreButton )
placeholderReplyButtonLeadingConstraint = replyButton . leadingAnchor . constraint ( equalTo : $0 . leadingAnchor )
NSLayoutConstraint . activate ( [
favoriteButton . widthAnchor . constraint ( equalTo : replyButton . widthAnchor ) ,
reblogButton . widthAnchor . constraint ( equalTo : replyButton . widthAnchor ) ,
moreButton . widthAnchor . constraint ( equalTo : replyButton . widthAnchor ) ,
// TODO: g a h
placeholderReplyButtonLeadingConstraint ,
replyButton . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
replyButton . bottomAnchor . constraint ( equalTo : $0 . bottomAnchor ) ,
favoriteButton . leadingAnchor . constraint ( equalTo : replyButton . trailingAnchor ) ,
favoriteButton . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
favoriteButton . bottomAnchor . constraint ( equalTo : $0 . bottomAnchor ) ,
reblogButton . leadingAnchor . constraint ( equalTo : favoriteButton . trailingAnchor ) ,
reblogButton . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
reblogButton . bottomAnchor . constraint ( equalTo : $0 . bottomAnchor ) ,
moreButton . leadingAnchor . constraint ( equalTo : reblogButton . trailingAnchor ) ,
moreButton . trailingAnchor . constraint ( equalTo : $0 . trailingAnchor ) ,
moreButton . topAnchor . constraint ( equalTo : $0 . topAnchor ) ,
moreButton . bottomAnchor . constraint ( equalTo : $0 . bottomAnchor ) ,
] )
}
2022-10-06 02:28:10 +00:00
private lazy var replyButton = UIButton ( ) . configure {
2022-10-03 01:34:06 +00:00
$0 . setImage ( UIImage ( systemName : " arrowshape.turn.up.left.fill " ) , for : . normal )
2022-10-06 02:28:10 +00:00
$0 . addTarget ( self , action : #selector ( replyPressed ) , for : . touchUpInside )
2022-10-03 01:34:06 +00:00
}
2022-10-06 02:28:10 +00:00
private lazy var favoriteButton = UIButton ( ) . configure {
2022-10-03 01:34:06 +00:00
$0 . setImage ( UIImage ( systemName : " star.fill " ) , for : . normal )
2022-10-06 02:28:10 +00:00
$0 . addTarget ( self , action : #selector ( favoritePressed ) , for : . touchUpInside )
2022-10-03 01:34:06 +00:00
}
2022-10-06 02:28:10 +00:00
private lazy var reblogButton = UIButton ( ) . configure {
2022-10-03 01:34:06 +00:00
$0 . setImage ( UIImage ( systemName : " repeat " ) , for : . normal )
2022-10-06 02:28:10 +00:00
$0 . addTarget ( self , action : #selector ( reblogPressed ) , for : . touchUpInside )
2022-10-03 01:34:06 +00:00
}
private let moreButton = UIButton ( ) . configure {
$0 . setImage ( UIImage ( systemName : " ellipsis " ) , for : . normal )
$0 . showsMenuAsPrimaryAction = true
}
2022-10-05 21:40:00 +00:00
// MARK: C e l l s t a t e
private var mainContainerTopToReblogLabelConstraint : NSLayoutConstraint !
private var mainContainerTopToSelfConstraint : NSLayoutConstraint !
private var mainContainerBottomToActionsConstraint : NSLayoutConstraint !
private var mainContainerBottomToSelfConstraint : NSLayoutConstraint !
2022-10-04 04:02:41 +00:00
weak var mastodonController : MastodonController !
weak var delegate : TimelineStatusCollectionViewCellDelegate ?
2022-10-05 21:40:00 +00:00
var showStatusAutomatically : Bool {
// TODO: n e e d e d o n c e c o n v e r s a t i o n c o n t r o l l e r r e f a c t o r e d
false
}
var showReplyIndicator : Bool {
// TODO: n e e d e d o n c e c o n v e r s a t i o n c o n t r o l l e r r e f a c t o r e d
false
}
var showPinned : Bool {
// TODO: n e e d e d o n c e p r o f i l e c o n t r o l l e r r e f a c t o r e d
false
}
2022-10-04 04:02:41 +00:00
2022-10-05 21:40:00 +00:00
private ( set ) var statusID : String !
2022-10-04 04:02:41 +00:00
private ( set ) var statusState = StatusState . unknown
2022-10-05 21:40:00 +00:00
private var accountID : String !
private var reblogStatusID : String ?
private var rebloggerID : String ?
2022-10-04 04:02:41 +00:00
2022-10-03 01:34:06 +00:00
private var firstLayout = true
2022-10-05 21:40:00 +00:00
private var isGrayscale = false
2022-10-06 02:28:10 +00:00
private var updateTimestampWorkItem : DispatchWorkItem ?
2022-10-03 01:34:06 +00:00
override init ( frame : CGRect ) {
super . init ( frame : frame )
2022-10-05 21:40:00 +00:00
for subview in [ reblogLabel , mainContainer , actionsContainer ] {
subview . translatesAutoresizingMaskIntoConstraints = false
contentView . addSubview ( subview )
}
mainContainerTopToReblogLabelConstraint = mainContainer . topAnchor . constraint ( equalTo : reblogLabel . bottomAnchor , constant : 4 )
mainContainerTopToSelfConstraint = mainContainer . topAnchor . constraint ( equalTo : contentView . topAnchor , constant : 8 )
mainContainerBottomToActionsConstraint = mainContainer . bottomAnchor . constraint ( equalTo : actionsContainer . topAnchor )
mainContainerBottomToSelfConstraint = mainContainer . bottomAnchor . constraint ( equalTo : contentView . bottomAnchor , constant : - 6 )
let metaIndicatorsBottomConstraint = metaIndicatorsView . bottomAnchor . constraint ( lessThanOrEqualTo : contentView . bottomAnchor , constant : - 6 )
// s o m e t i m e s d u r i n g i n t e r m e d i a t e l a y o u t s , t h e r e a r e c o n f l i c t i n g c o n s t r a i n t s , s o l e t t h i s o n e g e t b r o k e n t e m p o r a r i l y , t o a v o i d a b u n c h o f p r i n t i n g
metaIndicatorsBottomConstraint . priority = . init ( 999 )
2022-10-03 01:34:06 +00:00
NSLayoutConstraint . activate ( [
2022-10-05 21:40:00 +00:00
// w h y i s t h i s 4 b u t t h e m a i n C o n t a i n e r T o p S e l f C o n s t r a i n t c o n s t a n t 8 ? b e c a u s e t h i s l o o k s m o r e b a l a n c e d
reblogLabel . topAnchor . constraint ( equalTo : contentView . topAnchor , constant : 4 ) ,
reblogLabel . leadingAnchor . constraint ( equalTo : contentView . leadingAnchor , constant : 16 ) ,
reblogLabel . trailingAnchor . constraint ( equalTo : contentView . trailingAnchor , constant : - 16 ) ,
mainContainer . leadingAnchor . constraint ( equalTo : contentView . leadingAnchor , constant : 16 ) ,
mainContainer . trailingAnchor . constraint ( equalTo : contentView . trailingAnchor , constant : - 16 ) ,
actionsContainer . leadingAnchor . constraint ( equalTo : contentView . leadingAnchor , constant : 16 ) ,
actionsContainer . trailingAnchor . constraint ( equalTo : contentView . trailingAnchor , constant : - 16 ) ,
// y e s , t h i s i s d e l i b e r a t e l y 6 . 4 l o o k s t o c r a m p e d , 8 l o o k s u n e v e n
actionsContainer . bottomAnchor . constraint ( equalTo : contentView . bottomAnchor , constant : - 6 ) ,
metaIndicatorsBottomConstraint ,
2022-10-03 01:34:06 +00:00
] )
2022-10-05 21:40:00 +00:00
updateActionsVisibility ( )
NotificationCenter . default . addObserver ( self , selector : #selector ( preferencesChanged ) , name : . preferencesChanged , object : nil )
2022-10-03 01:34:06 +00:00
// T E M P
reblogLabel . text = " Reblogged by Person "
avatarImageView . backgroundColor = . red
displayNameLabel . text = " Display name "
usernameLabel . text = " @username "
timestampLabel . text = " 2m "
contentWarningLabel . text = " Content Warning "
contentTextView . setTextFromHtml ( " <p>Weeeeeeeee</p> " )
attachmentsView . backgroundColor = . red
metaIndicatorsView . placeholder ( )
// c o n t e n t W a r n i n g L a b e l . i s H i d d e n = t r u e
// c o l l a p s e B u t t o n . i s H i d d e n = t r u e
// c a r d V i e w . i s H i d d e n = t r u e
// p o l l V i e w . i s H i d d e n = t r u e
// a t t a c h m e n t s V i e w . i s H i d d e n = t r u e
// a c t i o n s C o n t a i n e r . i s H i d d e n = t r u e
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func layoutSubviews ( ) {
super . layoutSubviews ( )
if firstLayout {
firstLayout = false
2022-10-05 21:40:00 +00:00
// t h e b u t t o n ' s i m a g e v i e w d o e s n ' t e x i s t u n t i l a f t e r t h e f i r s t l a y o u t
// a c c e s s i n g i t b e f o r e t h a t c a u s e t h e b u t t o n t o l a y o u t I f N e e d e d w h i c h g e n e r a t e s a b r o k e n , i n t e r m e d i a t e l a y o u t a n d p r i n t s a b u n c h o f u n h e l p f u l a u t o l a y o u t w a r n i n g s
// s o w e w a i t u n t i l a f t e r t h e f i r s t l a y o u t p a s s t o s e t u p t h e r e p l y b u t t o n ' s r e a l c o n s t r a i n t
2022-10-03 01:34:06 +00:00
placeholderReplyButtonLeadingConstraint . isActive = false
replyButton . imageView ! . leadingAnchor . constraint ( equalTo : contentTextView . leadingAnchor ) . isActive = true
}
}
2022-10-06 02:28:10 +00:00
// MARK: A c c e s s i b i l i t y
override var accessibilityLabel : String ? {
get {
guard let status = mastodonController . persistentContainer . status ( for : statusID ) else {
return nil
}
var str = " \( status . account . displayOrUserName ) , \( contentTextView . text ? ? " " ) "
if status . attachments . count > 0 {
// TODO: l o c a l i z e m e
str += " , \( status . attachments . count ) attachment \( status . attachments . count > 1 ? " s " : " " ) "
}
if status . poll != nil {
str += " , poll "
}
str += " , \( status . createdAt . formatted ( . relative ( presentation : . numeric ) ) ) "
if let rebloggerID ,
let reblogger = mastodonController . persistentContainer . account ( for : rebloggerID ) {
str += " , reblogged by \( reblogger . displayOrUserName ) "
}
return str
}
set { }
}
override func accessibilityActivate ( ) -> Bool {
delegate ? . selected ( status : statusID , state : statusState . copy ( ) )
return true
}
// MARK: C o n f i g u r e U I
2022-10-04 04:02:41 +00:00
func updateUI ( statusID : String , state : StatusState ) {
2022-10-05 21:40:00 +00:00
guard var status = mastodonController . persistentContainer . status ( for : statusID ) else {
2022-10-04 04:02:41 +00:00
fatalError ( )
}
2022-10-05 21:40:00 +00:00
2022-10-04 04:02:41 +00:00
self . statusState = state
2022-10-05 21:40:00 +00:00
if let rebloggedStatus = status . reblog {
reblogStatusID = statusID
rebloggerID = status . account . id
reblogLabel . isHidden = false
mainContainerTopToReblogLabelConstraint . isActive = true
mainContainerTopToSelfConstraint . isActive = false
updateRebloggerLabel ( reblogger : status . account )
status = rebloggedStatus
} else {
reblogStatusID = nil
rebloggerID = nil
reblogLabel . isHidden = true
mainContainerTopToReblogLabelConstraint . isActive = false
mainContainerTopToSelfConstraint . isActive = true
}
doUpdateUI ( status : status )
}
private func doUpdateUI ( status : StatusMO ) {
self . statusID = status . id
self . accountID = status . account . id
let account = status . account
usernameLabel . text = " @ \( account . acct ) "
contentTextView . setTextFrom ( status : status )
updateGrayscaleableUI ( account : account , status : status )
updateUIForPreferences ( account : account , status : status )
2022-10-06 02:28:10 +00:00
doUpdateTimestamp ( status : status )
timestampLabel . isHidden = showPinned
pinImageView . isHidden = ! showPinned
2022-10-05 21:40:00 +00:00
cardView . card = status . card
cardView . isHidden = status . card = = nil
cardView . navigationDelegate = delegate
cardView . actionProvider = delegate
attachmentsView . updateUI ( status : status )
updateStatusState ( status : status )
contentWarningLabel . text = status . spoilerText
contentWarningLabel . isHidden = status . spoilerText . isEmpty
if ! contentWarningLabel . isHidden {
contentWarningLabel . setEmojis ( status . emojis , identifier : statusID )
}
let reblogDisabled : Bool
if mastodonController . instanceFeatures . boostToOriginalAudience {
reblogDisabled = status . visibility = = . direct || ( status . visibility = = . private && status . account . id != mastodonController . account . id )
} else {
reblogDisabled = status . visibility = = . private || status . visibility = = . direct
}
reblogButton . isEnabled = ! reblogDisabled && mastodonController . loggedIn
favoriteButton . isEnabled = mastodonController . loggedIn
replyButton . isEnabled = mastodonController . loggedIn
if statusState . unknown {
statusState . resolveFor ( status : status , text : contentTextView . text )
if statusState . collapsible ! && showStatusAutomatically {
statusState . collapsed = false
}
}
contentContainer . setCollapsed ( statusState . collapsed ! )
contentContainer . alpha = statusState . collapsed ! ? 0 : 1
collapseButton . imageView ! . transform = CGAffineTransform ( rotationAngle : statusState . collapsed ! ? . pi : 0 )
if statusState . collapsed ! {
collapseButton . accessibilityLabel = NSLocalizedString ( " Expand Status " , comment : " expand status button accessibility label " )
} else {
collapseButton . accessibilityLabel = NSLocalizedString ( " Collapse Status " , comment : " collapse status button accessibility label " )
}
}
2022-10-06 02:28:10 +00:00
private func updateUIForPreferences ( account : AccountMO , status : StatusMO ) {
2022-10-05 21:40:00 +00:00
avatarImageView . layer . cornerRadius = Preferences . shared . avatarStyle . cornerRadiusFraction * TimelineStatusCollectionViewCell . avatarImageViewSize
attachmentsView . contentHidden = Preferences . shared . blurAllMedia || status . sensitive
if showReplyIndicator {
metaIndicatorsView . allowedIndicators = . all
} else {
metaIndicatorsView . allowedIndicators = . all . subtracting ( . reply )
}
if isGrayscale != Preferences . shared . grayscaleImages {
updateGrayscaleableUI ( account : account , status : status )
}
if let rebloggerID ,
let reblogger = mastodonController . persistentContainer . account ( for : rebloggerID ) {
updateRebloggerLabel ( reblogger : reblogger )
}
}
2022-10-06 02:28:10 +00:00
private func updateTimestamp ( ) {
guard let mastodonController ,
let status = mastodonController . persistentContainer . status ( for : statusID ) else {
return
}
doUpdateTimestamp ( status : status )
}
private func doUpdateTimestamp ( status : StatusMO ) {
timestampLabel . text = status . createdAt . timeAgoString ( )
let delay : DispatchTimeInterval ?
switch status . createdAt . timeAgo ( ) . 1 {
case . second :
delay = . seconds ( 10 )
case . minute :
delay = . seconds ( 60 )
default :
delay = nil
}
if let delay {
if updateTimestampWorkItem = = nil {
updateTimestampWorkItem = DispatchWorkItem { [ weak self ] in
self ? . updateTimestamp ( )
}
}
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay , execute : updateTimestampWorkItem ! )
} else {
updateTimestampWorkItem = nil
}
}
2022-10-05 21:40:00 +00:00
private func updateRebloggerLabel ( reblogger : AccountMO ) {
if Preferences . shared . hideCustomEmojiInUsernames {
reblogLabel . text = " Reblogged by \( reblogger . displayNameWithoutCustomEmoji ) "
reblogLabel . removeEmojis ( )
} else {
reblogLabel . text = " Reblogged by \( reblogger . displayOrUserName ) "
reblogLabel . setEmojis ( reblogger . emojis , identifier : reblogger . id )
}
}
func updateGrayscaleableUI ( account : AccountMO , status : StatusMO ) {
isGrayscale = Preferences . shared . grayscaleImages
avatarImageView . update ( for : account . avatar )
if contentTextView . hasEmojis {
contentTextView . setTextFrom ( status : status )
}
displayNameLabel . updateForAccountDisplayName ( account : account )
}
func updateStatusState ( status : StatusMO ) {
if status . favourited {
favoriteButton . tintColor = UIColor ( displayP3Red : 1 , green : 0.8 , blue : 0 , alpha : 1 )
favoriteButton . accessibilityLabel = NSLocalizedString ( " Undo Favorite " , comment : " undo favorite button accessibility label " )
} else {
favoriteButton . tintColor = nil
favoriteButton . accessibilityLabel = NSLocalizedString ( " Favorite " , comment : " favorite button accessibility label " )
}
if status . reblogged {
reblogButton . tintColor = UIColor ( displayP3Red : 1 , green : 0.8 , blue : 0 , alpha : 1 )
reblogButton . accessibilityLabel = NSLocalizedString ( " Undo Reblog " , comment : " undo reblog button accessibility label " )
} else {
reblogButton . tintColor = nil
reblogButton . accessibilityLabel = NSLocalizedString ( " Reblog " , comment : " reblog button accessibility label " )
}
// k e e p m e n u i n s y n c w i t h c h a n g e d s t a t e s e . g . b o o k m a r k e d , m u t e d
// d o n o t i n c l u d e r e p l y a c t i o n h e r e , b e c a u s e t h e c e l l a l r e a d y c o n t a i n s a b u t t o n f o r i t
moreButton . menu = UIMenu ( title : " " , image : nil , identifier : nil , options : [ ] , children : delegate ? . actionsForStatus ( status , sourceView : moreButton , includeReply : false ) ? ? [ ] )
pollView . isHidden = status . poll = = nil
pollView . mastodonController = mastodonController
pollView . toastableViewController = delegate ? . toastableViewController
pollView . updateUI ( status : status , poll : status . poll )
}
private func updateActionsVisibility ( ) {
if Preferences . shared . hideActionsInTimeline {
actionsContainer . isHidden = true
mainContainerBottomToSelfConstraint . isActive = true
mainContainerBottomToActionsConstraint . isActive = false
} else {
actionsContainer . isHidden = false
mainContainerBottomToSelfConstraint . isActive = false
mainContainerBottomToActionsConstraint . isActive = true
}
}
@objc private func preferencesChanged ( ) {
guard let mastodonController ,
let status = mastodonController . persistentContainer . status ( for : statusID ) else {
return
}
updateUIForPreferences ( account : status . account , status : status )
// o n l y n e e d s t o h a p p e n w h e n p r e f s c h a n g e , r a t h e r t h a n i n u p d a t e U I F o r P r e f s b / c t h i s i s s e t u p c o r r e c t l y d u r i n g i n i t
let oldState = actionsContainer . isHidden
if oldState != Preferences . shared . hideActionsInTimeline {
updateActionsVisibility ( )
2022-10-06 02:28:10 +00:00
delegate ? . statusCellNeedsReconfigure ( self , animated : true )
2022-10-05 21:40:00 +00:00
}
2022-10-03 01:34:06 +00:00
}
2022-10-04 04:02:41 +00:00
// MARK: I n t e r a c t i o n
2022-10-06 02:28:10 +00:00
@objc private func reblogLabelPressed ( ) {
guard let rebloggerID else {
return
}
delegate ? . selected ( account : rebloggerID )
}
@objc private func accountPressed ( ) {
delegate ? . selected ( account : accountID )
}
@objc private func collapseButtonPressed ( ) {
2022-10-04 04:02:41 +00:00
statusState . collapsed ! . toggle ( )
// t h i s d e l e g a t e c a l l c a u s e s t h e c o l l e c t i o n v i e w t o r e c o n f i g u r e t h i s c e l l , a t w h i c h p o i n t ( a n d i n s i d e o f t h e c o l l e c t i o n v i e w ' s a n i m a t i o n h a n d l i n g ) w e ' l l u p d a t e t h e c o n t e n t C o n t a i n e r
2022-10-06 02:28:10 +00:00
delegate ? . statusCellNeedsReconfigure ( self , animated : true )
2022-10-04 04:02:41 +00:00
}
2022-10-06 02:28:10 +00:00
@objc private func replyPressed ( ) {
fatalError ( )
}
@objc private func favoritePressed ( ) {
guard let status = mastodonController . persistentContainer . status ( for : statusID ) else {
fatalError ( )
}
let oldValue = status . favourited
status . favourited . toggle ( )
// u p d a t e u i b e f o r e n e t w o r k r e q u e s t t o m a k e t h i n g s a p p e a r s p e e d y
updateStatusState ( status : status )
let request = ( status . favourited ? Status . favourite : Status . unfavourite ) ( statusID )
Task {
do {
let ( newStatus , _ ) = try await mastodonController . run ( request )
mastodonController . persistentContainer . addOrUpdate ( status : newStatus )
// TODO: s h o u l d t h i s b e f o r e t h e n e t w o r k r e q u e s t
UIImpactFeedbackGenerator ( style : . light ) . impactOccurred ( )
} catch {
status . favourited = oldValue
// TODO: d i s p l a y e r r o r m e s s a g e
UINotificationFeedbackGenerator ( ) . notificationOccurred ( . error )
}
}
}
@objc private func reblogPressed ( ) {
guard let status = mastodonController . persistentContainer . status ( for : statusID ) else {
fatalError ( )
}
if ! status . reblogged ,
Preferences . shared . confirmBeforeReblog {
let image : UIImage ?
let reblogVisibilityActions : [ CustomAlertController . MenuAction ] ?
if mastodonController . instanceFeatures . reblogVisibility {
image = UIImage ( systemName : Status . Visibility . public . unfilledImageName )
reblogVisibilityActions = [ Status . Visibility . unlisted , . private ] . map { visibility in
CustomAlertController . MenuAction ( title : " Reblog as \( visibility . displayName ) " , subtitle : visibility . subtitle , image : UIImage ( systemName : visibility . unfilledImageName ) ) { [ unowned self ] in
self . doReblog ( status : status , visibility : visibility )
}
}
} else {
image = nil
reblogVisibilityActions = [ ]
}
let preview = ConfirmReblogStatusPreviewView ( status : status )
var config = CustomAlertController . Configuration ( title : " Are you sure you want to reblog this post? " , content : preview , actions : [
CustomAlertController . Action ( title : " Cancel " , style : . cancel , handler : nil ) ,
CustomAlertController . Action ( title : " Reblog " , image : image , style : . default , handler : { [ unowned self ] in
self . doReblog ( status : status , visibility : nil )
} )
] )
if let reblogVisibilityActions {
var menuAction = CustomAlertController . Action ( title : nil , image : UIImage ( systemName : " chevron.down " ) , style : . menu ( reblogVisibilityActions ) , handler : nil )
menuAction . isSecondaryMenu = true
config . actions . append ( menuAction )
}
let alert = CustomAlertController ( config : config )
delegate ? . present ( alert , animated : true )
} else {
doReblog ( status : status , visibility : nil )
}
}
private func doReblog ( status : StatusMO , visibility : Status . Visibility ? ) {
let oldValue = status . reblogged
status . reblogged . toggle ( )
updateStatusState ( status : status )
let request : Request < Status >
if status . reblogged {
request = Status . reblog ( statusID , visibility : visibility )
} else {
request = Status . unreblog ( statusID )
}
Task {
do {
let ( newStatus , _ ) = try await mastodonController . run ( request )
mastodonController . persistentContainer . addOrUpdate ( status : newStatus )
UIImpactFeedbackGenerator ( style : . light ) . impactOccurred ( )
} catch {
status . reblogged = oldValue
// TODO: d i s p l a y e r r o r m e s s a g e
UINotificationFeedbackGenerator ( ) . notificationOccurred ( . error )
}
}
}
}
extension TimelineStatusCollectionViewCell : UIContextMenuInteractionDelegate {
func contextMenuInteraction ( _ interaction : UIContextMenuInteraction , configurationForMenuAtLocation location : CGPoint ) -> UIContextMenuConfiguration ? {
return UIContextMenuConfiguration ( ) {
ProfileViewController ( accountID : self . accountID , mastodonController : self . mastodonController )
} actionProvider : { _ in
return UIMenu ( children : self . delegate ? . actionsForProfile ( accountID : self . accountID , sourceView : interaction . view ! ) ? ? [ ] )
}
}
func contextMenuInteraction ( _ interaction : UIContextMenuInteraction , willPerformPreviewActionForMenuWith configuration : UIContextMenuConfiguration , animator : UIContextMenuInteractionCommitAnimating ) {
if let viewController = animator . previewViewController ,
let delegate {
animator . preferredCommitStyle = . pop
animator . addCompletion {
if let customPresenting = viewController as ? CustomPreviewPresenting {
customPresenting . presentFromPreview ( presenter : delegate )
} else {
delegate . show ( viewController )
}
}
}
}
}
extension TimelineStatusCollectionViewCell : UIDragInteractionDelegate {
func dragInteraction ( _ interaction : UIDragInteraction , itemsForBeginning session : UIDragSession ) -> [ UIDragItem ] {
guard let currentAccountID = mastodonController . accountInfo ? . id ,
let account = mastodonController . persistentContainer . account ( for : accountID ) else {
return [ ]
}
let provider = NSItemProvider ( object : account . url as NSURL )
let activity = UserActivityManager . showProfileActivity ( id : accountID , accountID : currentAccountID )
activity . displaysAuxiliaryScene = true
provider . registerObject ( activity , visibility : . all )
return [ UIDragItem ( itemProvider : provider ) ]
}
2022-10-03 01:34:06 +00:00
}