2022-10-06 03:12:03 +00:00
//
// 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 / 5 / 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
import Pachyderm
2022-10-07 02:36:55 +00:00
import Combine
2022-10-06 03:12:03 +00:00
@ MainActor
protocol StatusCollectionViewCellDelegate : AnyObject , TuskerNavigationDelegate , MenuActionProvider {
func statusCellNeedsReconfigure ( _ cell : StatusCollectionViewCell , animated : Bool )
}
@ MainActor
protocol StatusCollectionViewCell : UICollectionViewCell {
// MARK: S u b v i e w s
var avatarImageView : CachedImageView { get }
var displayNameLabel : EmojiLabel { get }
var usernameLabel : UILabel { get }
var contentWarningLabel : EmojiLabel { get }
var collapseButton : UIButton { get }
var contentContainer : StatusContentContainer { get }
var replyButton : UIButton { get }
var favoriteButton : UIButton { get }
var reblogButton : UIButton { get }
var moreButton : UIButton { get }
// TODO: w h y i s o n e o f t h e s e ! a n d t h e o t h e r ?
var mastodonController : MastodonController ! { get }
var delegate : StatusCollectionViewCellDelegate ? { get }
var showStatusAutomatically : Bool { get }
var showReplyIndicator : Bool { get }
var statusID : String ! { get set }
var statusState : StatusState ! { get set }
var accountID : String ! { get set }
var isGrayscale : Bool { get set }
2022-10-07 02:36:55 +00:00
var cancellables : Set < AnyCancellable > { get set }
2022-10-06 03:12:03 +00:00
func updateUIForPreferences ( status : StatusMO )
}
// MARK: U I C o n f i g u r a t i o n
extension StatusCollectionViewCell {
static var avatarImageViewSize : CGFloat { 50 }
2022-10-07 02:36:55 +00:00
func baseCreateObservers ( ) {
mastodonController . persistentContainer . statusSubject
. receive ( on : DispatchQueue . main )
. filter { [ unowned self ] in $0 = = self . statusID }
. sink { [ unowned self ] in
if let mastodonController = self . mastodonController ,
let status = mastodonController . persistentContainer . status ( for : $0 ) {
self . updateStatusState ( status : status )
}
}
. store ( in : & cancellables )
mastodonController . persistentContainer . accountSubject
. receive ( on : DispatchQueue . main )
. filter { [ unowned self ] in $0 = = self . accountID }
. sink { [ unowned self ] in
if let mastodonController = self . mastodonController ,
let account = mastodonController . persistentContainer . account ( for : $0 ) {
self . updateAccountUI ( account : account )
}
}
. store ( in : & cancellables )
}
2022-10-06 03:12:03 +00:00
func doUpdateUI ( status : StatusMO ) {
statusID = status . id
accountID = status . account . id
2022-10-07 02:36:55 +00:00
updateAccountUI ( account : status . account )
2022-10-06 03:12:03 +00:00
updateUIForPreferences ( status : status )
2022-10-07 02:36:55 +00:00
contentContainer . contentTextView . setTextFrom ( status : status )
2022-10-06 03:12:03 +00:00
contentContainer . cardView . card = status . card
contentContainer . cardView . isHidden = status . card = = nil
contentContainer . cardView . navigationDelegate = delegate
contentContainer . cardView . actionProvider = delegate
contentContainer . 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 && mastodonController . loggedIn && accountID != mastodonController . account . id )
} else {
reblogDisabled = status . visibility = = . direct || status . visibility = = . private
}
reblogButton . isEnabled = ! reblogDisabled && mastodonController . loggedIn
replyButton . isEnabled = mastodonController . loggedIn
favoriteButton . isEnabled = mastodonController . loggedIn
if statusState . unknown {
statusState . resolveFor ( status : status , text : contentContainer . contentTextView . text )
if statusState . collapsible ! && showStatusAutomatically {
statusState . collapsed = false
}
}
contentContainer . setCollapsed ( statusState . collapsed ! )
if statusState . collapsed ! {
contentContainer . alpha = 0
// TODO: i s t h i s a c c e s s i n g t h e i m a g e v i e w b e f o r e t h e b u t t o n ' s b e e n l a i d o u t ?
collapseButton . imageView ! . transform = CGAffineTransform ( rotationAngle : . pi )
collapseButton . accessibilityLabel = NSLocalizedString ( " Expand Status " , comment : " expand status button accessibility label " )
} else {
contentContainer . alpha = 1
collapseButton . imageView ! . transform = CGAffineTransform ( rotationAngle : 0 )
collapseButton . accessibilityLabel = NSLocalizedString ( " Collapse Status " , comment : " collapse status button accessibility label " )
}
}
2022-10-07 02:36:55 +00:00
func updateAccountUI ( account : AccountMO ) {
avatarImageView . update ( for : account . avatar )
displayNameLabel . updateForAccountDisplayName ( account : account )
usernameLabel . text = " @ \( account . acct ) "
}
2022-10-06 03:12:03 +00:00
func baseUpdateUIForPreferences ( status : StatusMO ) {
avatarImageView . layer . cornerRadius = Preferences . shared . avatarStyle . cornerRadiusFraction * Self . avatarImageViewSize
contentContainer . attachmentsView . contentHidden = Preferences . shared . blurAllMedia || status . sensitive
}
// o n l y c a l l e d w h e n i s G r a y s c a l e d o e s n o t m a t c h t h e p r e f
func updateGrayscaleableUI ( status : StatusMO ) {
isGrayscale = Preferences . shared . grayscaleImages
if contentContainer . contentTextView . hasEmojis {
contentContainer . contentTextView . setTextFrom ( status : status )
}
displayNameLabel . updateForAccountDisplayName ( account : status . 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 ) ? ? [ ] )
contentContainer . pollView . isHidden = status . poll = = nil
contentContainer . pollView . mastodonController = mastodonController
contentContainer . pollView . toastableViewController = delegate ? . toastableViewController
contentContainer . pollView . updateUI ( status : status , poll : status . poll )
}
}
// MARK: I n t e r a c t i o n
extension StatusCollectionViewCell {
func toggleCollapse ( ) {
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
delegate ? . statusCellNeedsReconfigure ( self , animated : true )
}
func toggleFavorite ( ) {
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 )
}
}
}
func toggleReblog ( ) {
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 )
// 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 . 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 StatusCollectionViewCell {
func contextMenuConfigurationForAccount ( sourceView : UIView ) -> UIContextMenuConfiguration ? {
return UIContextMenuConfiguration ( ) {
ProfileViewController ( accountID : self . accountID , mastodonController : self . mastodonController )
} actionProvider : { _ in
return UIMenu ( children : self . delegate ? . actionsForProfile ( accountID : self . accountID , sourceView : sourceView ) ? ? [ ] )
}
}
}
extension StatusCollectionViewCell {
func dragItemsForAccount ( ) -> [ 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 ) ]
}
}