2021-03-03 09:12:48 +01:00
//
// S t a t u s P r o v i d e r + U I T a b l e V i e w D e l e g a t e . s w i f t
// M a s t o d o n
//
// C r e a t e d b y M a i n a s u K C i r n o o n 2 0 2 1 - 3 - 3 .
//
import Combine
import CoreDataStack
import MastodonSDK
2021-03-10 10:14:12 +01:00
import os . log
import UIKit
2021-03-03 09:12:48 +01:00
extension StatusTableViewCellDelegate where Self : StatusProvider {
func handleTableView ( _ tableView : UITableView , willDisplay cell : UITableViewCell , forRowAt indexPath : IndexPath ) {
2021-04-01 08:39:15 +02:00
// u p d a t e p o l l w h e n s t a t u s a p p e a r
2021-03-03 09:12:48 +01:00
let now = Date ( )
var pollID : Mastodon . Entity . Poll . ID ?
2021-04-01 08:39:15 +02:00
status ( for : cell , indexPath : indexPath )
. compactMap { [ weak self ] status -> AnyPublisher < Mastodon . Response . Content < Mastodon . Entity . Poll > , Error > ? in
2021-03-03 09:12:48 +01:00
guard let self = self else { return nil }
guard let authenticationBox = self . context . authenticationService . activeMastodonAuthenticationBox . value else { return nil }
2021-04-01 08:39:15 +02:00
guard let status = ( status ? . reblog ? ? status ) else { return nil }
guard let poll = status . poll else { return nil }
2021-03-03 09:12:48 +01:00
pollID = poll . id
// n o t e x p i r e d A N D l a s t u p d a t e > 6 0 s
guard ! poll . expired else {
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: poll %s expired. Skip for update " , ( #file as NSString ) . lastPathComponent , #line , #function , poll . id )
2021-03-03 09:12:48 +01:00
return nil
}
let timeIntervalSinceUpdate = now . timeIntervalSince ( poll . updatedAt )
2021-03-04 11:53:29 +01:00
#if DEBUG
2021-03-10 10:14:12 +01:00
let autoRefreshTimeInterval : TimeInterval = 3 // s p e e d u p t e s t i n g
2021-03-04 11:53:29 +01:00
#else
let autoRefreshTimeInterval : TimeInterval = 60
#endif
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: poll %s updated in the %.2fs. Skip for update " , ( #file as NSString ) . lastPathComponent , #line , #function , poll . id , timeIntervalSinceUpdate )
2021-03-03 09:12:48 +01:00
return nil
}
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: poll %s info update… " , ( #file as NSString ) . lastPathComponent , #line , #function , poll . id )
2021-03-03 09:12:48 +01:00
return self . context . apiService . poll (
2021-04-01 08:39:15 +02:00
domain : status . domain ,
2021-03-03 09:12:48 +01:00
pollID : poll . id ,
pollObjectID : poll . objectID ,
mastodonAuthenticationBox : authenticationBox
)
}
. setFailureType ( to : Error . self )
. switchToLatest ( )
. receive ( on : DispatchQueue . main )
. sink ( receiveCompletion : { completion in
switch completion {
case . failure ( let error ) :
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: poll %s info fail to update: %s " , ( #file as NSString ) . lastPathComponent , #line , #function , pollID ? ? " ? " , error . localizedDescription )
2021-03-03 09:12:48 +01:00
case . finished :
break
}
} , receiveValue : { response in
let poll = response . value
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: poll %s info updated " , ( #file as NSString ) . lastPathComponent , #line , #function , poll . id )
2021-03-03 09:12:48 +01:00
} )
. store ( in : & disposeBag )
2021-03-10 07:36:28 +01:00
2021-04-01 08:39:15 +02:00
status ( for : cell , indexPath : indexPath )
2021-06-23 14:47:49 +02:00
. receive ( on : RunLoop . main )
2021-04-01 08:39:15 +02:00
. sink { [ weak self ] status in
2021-03-10 07:36:28 +01:00
guard let self = self else { return }
2021-04-01 08:39:15 +02:00
let status = status ? . reblog ? ? status
guard let media = ( status ? . mediaAttachments ? ? Set ( ) ) . first else { return }
2021-03-10 07:36:28 +01:00
guard let videoPlayerViewModel = self . context . videoPlaybackService . dequeueVideoPlayerViewModel ( for : media ) else { return }
DispatchQueue . main . async {
videoPlayerViewModel . willDisplay ( )
}
}
. store ( in : & disposeBag )
2021-03-03 09:12:48 +01:00
}
2021-03-10 10:14:12 +01:00
func handleTableView ( _ tableView : UITableView , didEndDisplaying cell : UITableViewCell , forRowAt indexPath : IndexPath ) {
// o s _ l o g ( " % { p u b l i c } s [ % { p u b l i c } l d ] , % { p u b l i c } s : i n d e x P a t h % s " , ( ( # f i l e a s N S S t r i n g ) . l a s t P a t h C o m p o n e n t ) , # l i n e , # f u n c t i o n , i n d e x P a t h . d e b u g D e s c r i p t i o n )
2021-04-01 08:39:15 +02:00
status ( for : cell , indexPath : indexPath )
. sink { [ weak self ] status in
2021-03-10 10:14:12 +01:00
guard let self = self else { return }
2021-04-01 08:39:15 +02:00
guard let media = ( status ? . mediaAttachments ? ? Set ( ) ) . first else { return }
2021-03-10 10:14:12 +01:00
2021-03-11 06:11:13 +01:00
if let videoPlayerViewModel = self . context . videoPlaybackService . dequeueVideoPlayerViewModel ( for : media ) {
DispatchQueue . main . async {
videoPlayerViewModel . didEndDisplaying ( )
}
}
2021-04-08 08:11:35 +02:00
if let currentAudioAttachment = self . context . audioPlaybackService . attachment ,
status ? . mediaAttachments ? . contains ( currentAudioAttachment ) = = true {
2021-03-11 06:11:13 +01:00
self . context . audioPlaybackService . pause ( )
2021-03-10 10:14:12 +01:00
}
}
. store ( in : & disposeBag )
}
2021-04-13 13:46:42 +02:00
func handleTableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
StatusProviderFacade . coordinateToStatusThreadScene ( for : . primary , provider : self , indexPath : indexPath )
}
2021-03-03 09:12:48 +01:00
}
2021-03-10 10:14:12 +01:00
2021-04-30 13:28:06 +02:00
extension StatusTableViewCellDelegate where Self : StatusProvider {
private typealias ImagePreviewPresentableCell = UITableViewCell & DisposeBagCollectable & MosaicImageViewContainerPresentable
func handleTableView ( _ tableView : UITableView , contextMenuConfigurationForRowAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
guard let imagePreviewPresentableCell = tableView . cellForRow ( at : indexPath ) as ? ImagePreviewPresentableCell else { return nil }
guard imagePreviewPresentableCell . isRevealing else { return nil }
2021-05-06 12:03:58 +02:00
let status = self . status ( for : nil , indexPath : indexPath )
2021-04-30 13:28:06 +02:00
return contextMenuConfiguration ( tableView , status : status , imagePreviewPresentableCell : imagePreviewPresentableCell , contextMenuConfigurationForRowAt : indexPath , point : point )
}
private func contextMenuConfiguration (
_ tableView : UITableView ,
status : Future < Status ? , Never > ,
imagePreviewPresentableCell presentable : ImagePreviewPresentableCell ,
contextMenuConfigurationForRowAt indexPath : IndexPath ,
point : CGPoint
) -> UIContextMenuConfiguration ? {
let imageViews = presentable . mosaicImageViewContainer . imageViews
guard ! imageViews . isEmpty else { return nil }
for ( i , imageView ) in imageViews . enumerated ( ) {
let pointInImageView = imageView . convert ( point , from : tableView )
guard imageView . point ( inside : pointInImageView , with : nil ) else {
continue
}
guard let image = imageView . image , image . size != CGSize ( width : 1 , height : 1 ) else {
// n o t p r o v i d e p r e v i e w u n t i l i m a g e r e a d y
return nil
}
// s e t u p p r e v i e w
let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel ( aspectRatio : image . size , thumbnail : image )
status
. sink { status in
guard let status = ( status ? . reblog ? ? status ) ,
let media = status . mediaAttachments ? . sorted ( by : { $0 . index . compare ( $1 . index ) = = . orderedAscending } ) ,
i < media . count , let url = URL ( string : media [ i ] . url ) else {
return
}
contextMenuImagePreviewViewModel . url . value = url
}
. store ( in : & contextMenuImagePreviewViewModel . disposeBag )
// s e t u p c o n t e x t m e n u
let contextMenuConfiguration = TimelineTableViewCellContextMenuConfiguration ( identifier : nil ) { ( ) -> UIViewController ? in
// k n o w i s s u e : p r e v i e w s i z e l o o k s n o t a s l a r g e a s s y s t e m d e f a u l t p r e v i e w
let previewProvider = ContextMenuImagePreviewViewController ( )
previewProvider . viewModel = contextMenuImagePreviewViewModel
return previewProvider
} actionProvider : { _ -> UIMenu ? in
let savePhotoAction = UIAction (
title : L10n . Common . Controls . Actions . savePhoto , image : UIImage ( systemName : " square.and.arrow.down " ) ! , identifier : nil , discoverabilityTitle : nil , attributes : [ ] , state : . off
) { [ weak self ] _ in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: save photo " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
guard let self = self else { return }
self . attachment ( of : status , index : i )
. setFailureType ( to : Error . self )
. compactMap { attachment -> AnyPublisher < UIImage , Error > ? in
guard let attachment = attachment , let url = URL ( string : attachment . url ) else { return nil }
return self . context . photoLibraryService . saveImage ( url : url )
}
2021-05-06 09:05:24 +02:00
. switchToLatest ( )
. sink ( receiveCompletion : { [ weak self ] completion in
guard let self = self else { return }
switch completion {
case . failure ( let error ) :
guard let error = error as ? PhotoLibraryService . PhotoLibraryError ,
case . noPermission = error else { return }
let alertController = SettingService . openSettingsAlertController ( title : L10n . Common . Alerts . SavePhotoFailure . title , message : L10n . Common . Alerts . SavePhotoFailure . message )
self . coordinator . present ( scene : . alertController ( alertController : alertController ) , from : self , transition : . alertController ( animated : true , completion : nil ) )
case . finished :
break
}
2021-04-30 13:28:06 +02:00
} , receiveValue : { _ in
// d o n o t h i n g
} )
. store ( in : & self . context . disposeBag )
}
2021-07-06 13:53:47 +02:00
let copyPhotoAction = UIAction (
title : L10n . Common . Controls . Actions . copyPhoto ,
image : UIImage ( systemName : " doc.on.doc " ) , identifier : nil , discoverabilityTitle : nil , attributes : [ ] , state : . off
) { [ weak self ] _ in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: copy photo " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
guard let self = self else { return }
self . attachment ( of : status , index : i )
. setFailureType ( to : Error . self )
. compactMap { attachment -> AnyPublisher < UIImage , Error > ? in
guard let attachment = attachment , let url = URL ( string : attachment . url ) else { return nil }
return self . context . photoLibraryService . copyImage ( url : url )
}
. switchToLatest ( )
2021-07-06 14:02:30 +02:00
. sink ( receiveCompletion : { completion in
2021-07-06 13:53:47 +02:00
switch completion {
case . failure ( let error ) :
2021-07-06 14:02:30 +02:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: copy photo fail: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , error . localizedDescription )
2021-07-06 13:53:47 +02:00
case . finished :
break
}
} , receiveValue : { _ in
// d o n o t h i n g
} )
. store ( in : & self . context . disposeBag )
}
2021-04-30 13:28:06 +02:00
let shareAction = UIAction (
title : L10n . Common . Controls . Actions . share , image : UIImage ( systemName : " square.and.arrow.up " ) ! , identifier : nil , discoverabilityTitle : nil , attributes : [ ] , state : . off
) { [ weak self ] _ in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: share " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
guard let self = self else { return }
self . attachment ( of : status , index : i )
. sink ( receiveValue : { [ weak self ] attachment in
guard let self = self else { return }
guard let attachment = attachment , let url = URL ( string : attachment . url ) else { return }
let applicationActivities : [ UIActivity ] = [
SafariActivity ( sceneCoordinator : self . coordinator )
]
let activityViewController = UIActivityViewController (
activityItems : [ url ] ,
applicationActivities : applicationActivities
)
activityViewController . popoverPresentationController ? . sourceView = imageView
self . present ( activityViewController , animated : true , completion : nil )
} )
. store ( in : & self . context . disposeBag )
}
2021-07-06 13:53:47 +02:00
let children = [ savePhotoAction , copyPhotoAction , shareAction ]
2021-04-30 13:28:06 +02:00
return UIMenu ( title : " " , image : nil , children : children )
}
contextMenuConfiguration . indexPath = indexPath
contextMenuConfiguration . index = i
return contextMenuConfiguration
}
return nil
}
private func attachment ( of status : Future < Status ? , Never > , index : Int ) -> AnyPublisher < Attachment ? , Never > {
status
. map { status in
guard let status = status ? . reblog ? ? status else { return nil }
guard let media = status . mediaAttachments ? . sorted ( by : { $0 . index . compare ( $1 . index ) = = . orderedAscending } ) else { return nil }
guard index < media . count else { return nil }
return media [ index ]
}
. eraseToAnyPublisher ( )
}
func handleTableView ( _ tableView : UITableView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return _handleTableView ( tableView , configuration : configuration )
}
func handleTableView ( _ tableView : UITableView , previewForDismissingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return _handleTableView ( tableView , configuration : configuration )
}
private func _handleTableView ( _ tableView : UITableView , configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
guard let configuration = configuration as ? TimelineTableViewCellContextMenuConfiguration else { return nil }
guard let indexPath = configuration . indexPath , let index = configuration . index else { return nil }
guard let cell = tableView . cellForRow ( at : indexPath ) as ? ImagePreviewPresentableCell else {
return nil
}
let imageViews = cell . mosaicImageViewContainer . imageViews
guard index < imageViews . count else { return nil }
let imageView = imageViews [ index ]
return UITargetedPreview ( view : imageView , parameters : UIPreviewParameters ( ) )
}
func handleTableView ( _ tableView : UITableView , willPerformPreviewActionForMenuWith configuration : UIContextMenuConfiguration , animator : UIContextMenuInteractionCommitAnimating ) {
guard let previewableViewController = self as ? MediaPreviewableViewController else { return }
guard let configuration = configuration as ? TimelineTableViewCellContextMenuConfiguration else { return }
guard let indexPath = configuration . indexPath , let index = configuration . index else { return }
guard let cell = tableView . cellForRow ( at : indexPath ) as ? ImagePreviewPresentableCell else { return }
let imageViews = cell . mosaicImageViewContainer . imageViews
guard index < imageViews . count else { return }
let imageView = imageViews [ index ]
2021-05-06 12:03:58 +02:00
let status = self . status ( for : nil , indexPath : indexPath )
2021-04-30 13:28:06 +02:00
let initialFrame : CGRect ? = {
guard let previewViewController = animator . previewViewController else { return nil }
return UIView . findContextMenuPreviewFrameInWindow ( previewController : previewViewController )
} ( )
animator . preferredCommitStyle = . pop
animator . addCompletion { [ weak self ] in
guard let self = self else { return }
status
// . d e l a y ( f o r : . m i l l i s e c o n d s ( 5 0 0 ) , s c h e d u l e r : D i s p a t c h Q u e u e . m a i n )
. sink { [ weak self ] status in
guard let self = self else { return }
guard let status = ( status ? . reblog ? ? status ) else { return }
let meta = MediaPreviewViewModel . StatusImagePreviewMeta (
statusObjectID : status . objectID ,
initialIndex : index ,
preloadThumbnailImages : cell . mosaicImageViewContainer . thumbnails ( )
)
let pushTransitionItem = MediaPreviewTransitionItem (
source : . mosaic ( cell . mosaicImageViewContainer ) ,
previewableViewController : previewableViewController
)
pushTransitionItem . aspectRatio = {
if let image = imageView . image {
return image . size
}
guard let media = status . mediaAttachments ? . sorted ( by : { $0 . index . compare ( $1 . index ) = = . orderedAscending } ) else { return nil }
guard index < media . count else { return nil }
let meta = media [ index ] . meta
guard let width = meta ? . original ? . width , let height = meta ? . original ? . height else { return nil }
return CGSize ( width : width , height : height )
} ( )
pushTransitionItem . sourceImageView = imageView
pushTransitionItem . initialFrame = {
if let initialFrame = initialFrame {
return initialFrame
}
return imageView . superview ! . convert ( imageView . frame , to : nil )
} ( )
pushTransitionItem . image = {
if let image = imageView . image {
return image
}
if index < cell . mosaicImageViewContainer . blurhashOverlayImageViews . count {
return cell . mosaicImageViewContainer . blurhashOverlayImageViews [ index ] . image
}
return nil
} ( )
let mediaPreviewViewModel = MediaPreviewViewModel (
context : self . context ,
meta : meta ,
pushTransitionItem : pushTransitionItem
)
DispatchQueue . main . async {
self . coordinator . present ( scene : . mediaPreview ( viewModel : mediaPreviewViewModel ) , from : self , transition : . custom ( transitioningDelegate : previewableViewController . mediaPreviewTransitionController ) )
}
}
. store ( in : & cell . disposeBag )
}
}
}
extension UIView {
// h a c k t o r e t r i e v e p r e v i e w v i e w f r a m e i n w i n d o w
fileprivate static func findContextMenuPreviewFrameInWindow (
previewController : UIViewController
) -> CGRect ? {
guard let window = previewController . view . window else { return nil }
let targetViews = window . subviews
. map { $0 . findSameSize ( view : previewController . view ) }
. flatMap { $0 }
for targetView in targetViews {
guard let targetViewSuperview = targetView . superview else { continue }
let frame = targetViewSuperview . convert ( targetView . frame , to : nil )
guard frame . origin . x > 0 , frame . origin . y > 0 else { continue }
return frame
}
return nil
}
private func findSameSize ( view : UIView ) -> [ UIView ] {
var views : [ UIView ] = [ ]
if view . bounds . size = = bounds . size {
views . append ( self )
}
for subview in subviews {
let targetViews = subview . findSameSize ( view : view )
views . append ( contentsOf : targetViews )
}
return views
}
}