Merge branch /develop into feature/compose
# Conflicts: # Mastodon.xcodeproj/project.pbxproj # Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift # Mastodon/State/AppContext.swift # MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift
This commit is contained in:
commit
c8c296d1ba
|
@ -163,7 +163,7 @@ public extension Toot {
|
||||||
func update(liked: Bool, mastodonUser: MastodonUser) {
|
func update(liked: Bool, mastodonUser: MastodonUser) {
|
||||||
if liked {
|
if liked {
|
||||||
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).addObjects(from: [mastodonUser])
|
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
@ -174,7 +174,7 @@ public extension Toot {
|
||||||
func update(reblogged: Bool, mastodonUser: MastodonUser) {
|
func update(reblogged: Bool, mastodonUser: MastodonUser) {
|
||||||
if reblogged {
|
if reblogged {
|
||||||
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).addObjects(from: [mastodonUser])
|
self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
@ -186,7 +186,7 @@ public extension Toot {
|
||||||
func update(muted: Bool, mastodonUser: MastodonUser) {
|
func update(muted: Bool, mastodonUser: MastodonUser) {
|
||||||
if muted {
|
if muted {
|
||||||
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
|
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).addObjects(from: [mastodonUser])
|
self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.mutedBy ?? Set()).contains(mastodonUser) {
|
if (self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
@ -198,7 +198,7 @@ public extension Toot {
|
||||||
func update(bookmarked: Bool, mastodonUser: MastodonUser) {
|
func update(bookmarked: Bool, mastodonUser: MastodonUser) {
|
||||||
if bookmarked {
|
if bookmarked {
|
||||||
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).addObjects(from: [mastodonUser])
|
self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
||||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||||
2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlayer.swift */; };
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
||||||
|
@ -60,6 +60,7 @@
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; };
|
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; };
|
||||||
|
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; };
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; };
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; };
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; };
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; };
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; };
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; };
|
||||||
|
@ -93,7 +94,7 @@
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
||||||
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */; };
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; };
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */; };
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */; };
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; };
|
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; };
|
||||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||||
|
@ -141,6 +142,8 @@
|
||||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
||||||
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
|
||||||
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
||||||
|
@ -164,6 +167,7 @@
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||||
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
||||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||||
|
@ -210,6 +214,7 @@
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
||||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
||||||
|
@ -286,7 +291,7 @@
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
||||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||||
2D206B8B25F6015000143C56 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
|
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
|
||||||
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
||||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -318,6 +323,7 @@
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
||||||
|
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = "<group>"; };
|
||||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = "<group>"; };
|
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = "<group>"; };
|
||||||
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = "<group>"; };
|
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -354,7 +360,7 @@
|
||||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
||||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
||||||
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicPlayerView.swift; sourceTree = "<group>"; };
|
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = "<group>"; };
|
||||||
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = "<group>"; };
|
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = "<group>"; };
|
||||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
||||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
@ -411,6 +417,8 @@
|
||||||
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
|
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
||||||
|
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
||||||
|
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = "<group>"; };
|
||||||
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
||||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -431,6 +439,7 @@
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||||
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -479,6 +488,7 @@
|
||||||
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
|
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -608,6 +618,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
||||||
|
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
||||||
);
|
);
|
||||||
path = Content;
|
path = Content;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -693,7 +704,7 @@
|
||||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||||
2D206B8B25F6015000143C56 /* AudioPlayer.swift */,
|
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
||||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||||
DB49A61925FF327D00B98345 /* EmojiService */,
|
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||||
|
@ -969,6 +980,7 @@
|
||||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */,
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */,
|
||||||
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */,
|
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */,
|
||||||
|
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */,
|
||||||
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
||||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
||||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||||
|
@ -1204,6 +1216,7 @@
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */,
|
2D206B8525F5FB0900143C56 /* Double.swift */,
|
||||||
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
||||||
|
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
|
||||||
);
|
);
|
||||||
path = Extension;
|
path = Extension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1256,7 +1269,8 @@
|
||||||
children = (
|
children = (
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
||||||
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */,
|
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */,
|
||||||
|
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */,
|
||||||
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */,
|
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */,
|
||||||
);
|
);
|
||||||
path = Container;
|
path = Container;
|
||||||
|
@ -1275,6 +1289,7 @@
|
||||||
DBA9B90325F1D4420012E7B6 /* Control */ = {
|
DBA9B90325F1D4420012E7B6 /* Control */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
|
||||||
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
|
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
|
||||||
);
|
);
|
||||||
path = Control;
|
path = Control;
|
||||||
|
@ -1649,7 +1664,7 @@
|
||||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */,
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
|
@ -1720,6 +1735,7 @@
|
||||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||||
|
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||||
|
@ -1727,6 +1743,7 @@
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||||
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */,
|
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */,
|
||||||
|
@ -1750,6 +1767,7 @@
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||||
|
@ -1792,6 +1810,7 @@
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
|
@ -1800,8 +1819,9 @@
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */,
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -37,7 +37,11 @@ extension StatusSection {
|
||||||
StatusSection.configure(
|
StatusSection.configure(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
toot: timelineIndex.toot,
|
||||||
|
requestUserID: timelineIndex.userID,
|
||||||
|
statusItemAttribute: attribute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cell.delegate = statusTableViewCellDelegate
|
cell.delegate = statusTableViewCellDelegate
|
||||||
|
@ -52,7 +56,11 @@ extension StatusSection {
|
||||||
StatusSection.configure(
|
StatusSection.configure(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
toot: toot,
|
||||||
|
requestUserID: requestUserID,
|
||||||
|
statusItemAttribute: attribute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cell.delegate = statusTableViewCellDelegate
|
cell.delegate = statusTableViewCellDelegate
|
||||||
|
@ -94,13 +102,23 @@ extension StatusSection {
|
||||||
return L10n.Common.Controls.Status.userReblogged(name)
|
return L10n.Common.Controls.Status.userReblogged(name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// set name username avatar
|
// set name username
|
||||||
cell.statusView.nameLabel.text = {
|
cell.statusView.nameLabel.text = {
|
||||||
let author = (toot.reblog ?? toot).author
|
let author = (toot.reblog ?? toot).author
|
||||||
return author.displayName.isEmpty ? author.username : author.displayName
|
return author.displayName.isEmpty ? author.username : author.displayName
|
||||||
}()
|
}()
|
||||||
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL()))
|
// set avatar
|
||||||
|
if let reblog = toot.reblog {
|
||||||
|
cell.statusView.avatarButton.isHidden = true
|
||||||
|
cell.statusView.avatarStackedContainerButton.isHidden = false
|
||||||
|
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
|
||||||
|
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
||||||
|
} else {
|
||||||
|
cell.statusView.avatarButton.isHidden = false
|
||||||
|
cell.statusView.avatarStackedContainerButton.isHidden = true
|
||||||
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
||||||
|
}
|
||||||
|
|
||||||
// set text
|
// set text
|
||||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
||||||
|
@ -162,13 +180,14 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||||
let isStatusSensitive = statusItemAttribute.isStatusSensitive
|
let isStatusSensitive = statusItemAttribute.isStatusSensitive
|
||||||
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
|
||||||
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||||
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
|
||||||
|
|
||||||
// set audio
|
// set audio
|
||||||
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
|
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
|
||||||
cell.statusView.audioView.isHidden = false
|
cell.statusView.audioView.isHidden = false
|
||||||
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, videoPlaybackService: dependency.context.videoPlaybackService)
|
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: dependency.context.audioPlaybackService)
|
||||||
} else {
|
} else {
|
||||||
cell.statusView.audioView.isHidden = true
|
cell.statusView.audioView.isHidden = true
|
||||||
}
|
}
|
||||||
|
@ -185,12 +204,16 @@ extension StatusSection {
|
||||||
return CGSize(width: maxWidth, height: maxWidth * scale)
|
return CGSize(width: maxWidth, height: maxWidth * scale)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
|
||||||
|
cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||||
|
cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
|
||||||
|
|
||||||
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
||||||
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
||||||
{
|
{
|
||||||
let parent = cell.delegate?.parent()
|
let parent = cell.delegate?.parent()
|
||||||
let mosaicPlayerView = cell.statusView.mosaicPlayerView
|
let playerContainerView = cell.statusView.playerContainerView
|
||||||
let playerViewController = mosaicPlayerView.setupPlayer(
|
let playerViewController = playerContainerView.setupPlayer(
|
||||||
aspectRatio: videoPlayerViewModel.videoSize,
|
aspectRatio: videoPlayerViewModel.videoSize,
|
||||||
maxSize: playerViewMaxSize,
|
maxSize: playerViewMaxSize,
|
||||||
parent: parent
|
parent: parent
|
||||||
|
@ -198,17 +221,33 @@ extension StatusSection {
|
||||||
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate
|
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate
|
||||||
playerViewController.player = videoPlayerViewModel.player
|
playerViewController.player = videoPlayerViewModel.player
|
||||||
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
||||||
|
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
|
||||||
mosaicPlayerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif
|
if videoPlayerViewModel.videoKind == .gif {
|
||||||
mosaicPlayerView.isHidden = false
|
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)
|
||||||
|
}
|
||||||
|
playerContainerView.isHidden = false
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
cell.statusView.mosaicPlayerView.playerViewController.player?.pause()
|
cell.statusView.playerContainerView.playerViewController.player?.pause()
|
||||||
cell.statusView.mosaicPlayerView.playerViewController.player = nil
|
cell.statusView.playerContainerView.playerViewController.player = nil
|
||||||
}
|
}
|
||||||
// set poll
|
// set poll
|
||||||
let poll = (toot.reblog ?? toot).poll
|
let poll = (toot.reblog ?? toot).poll
|
||||||
StatusSection.configure(
|
StatusSection.configurePoll(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
requestUserID: requestUserID,
|
requestUserID: requestUserID,
|
||||||
|
@ -222,7 +261,7 @@ extension StatusSection {
|
||||||
} receiveValue: { change in
|
} receiveValue: { change in
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let newPoll = object as? Poll else { return }
|
let newPoll = object as? Poll else { return }
|
||||||
StatusSection.configure(
|
StatusSection.configurePoll(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
poll: newPoll,
|
poll: newPoll,
|
||||||
requestUserID: requestUserID,
|
requestUserID: requestUserID,
|
||||||
|
@ -234,19 +273,7 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolbar
|
// toolbar
|
||||||
let replyCountTitle: String = {
|
StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
|
||||||
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
|
||||||
return StatusSection.formattedNumberTitleForActionButton(count)
|
|
||||||
}()
|
|
||||||
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
|
||||||
|
|
||||||
let isLike = (toot.reblog ?? toot).favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
|
||||||
let favoriteCountTitle: String = {
|
|
||||||
let count = (toot.reblog ?? toot).favouritesCount.intValue
|
|
||||||
return StatusSection.formattedNumberTitleForActionButton(count)
|
|
||||||
}()
|
|
||||||
cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal)
|
|
||||||
cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike
|
|
||||||
|
|
||||||
// set date
|
// set date
|
||||||
let createdAt = (toot.reblog ?? toot).createdAt
|
let createdAt = (toot.reblog ?? toot).createdAt
|
||||||
|
@ -264,20 +291,47 @@ extension StatusSection {
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { change in
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let newToot = object as? Toot else { return }
|
let toot = object as? Toot else { return }
|
||||||
let targetToot = newToot.reblog ?? newToot
|
StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
|
||||||
|
|
||||||
let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue)
|
||||||
let favoriteCount = targetToot.favouritesCount.intValue
|
os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.favouritesCount.intValue)
|
||||||
let favoriteCountTitle = StatusSection.formattedNumberTitleForActionButton(favoriteCount)
|
|
||||||
cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal)
|
|
||||||
cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike
|
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, targetToot.id, favoriteCount)
|
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configure(
|
static func configureActionToolBar(
|
||||||
|
cell: StatusTableViewCell,
|
||||||
|
toot: Toot,
|
||||||
|
requestUserID: String
|
||||||
|
) {
|
||||||
|
let toot = toot.reblog ?? toot
|
||||||
|
|
||||||
|
// set reply
|
||||||
|
let replyCountTitle: String = {
|
||||||
|
let count = toot.repliesCount?.intValue ?? 0
|
||||||
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
|
}()
|
||||||
|
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
||||||
|
// set reblog
|
||||||
|
let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||||
|
let reblogCountTitle: String = {
|
||||||
|
let count = toot.reblogsCount.intValue
|
||||||
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
|
}()
|
||||||
|
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
||||||
|
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
|
||||||
|
// set like
|
||||||
|
let isLike = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||||
|
let favoriteCountTitle: String = {
|
||||||
|
let count = toot.favouritesCount.intValue
|
||||||
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
|
}()
|
||||||
|
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||||
|
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
|
||||||
|
}
|
||||||
|
|
||||||
|
static func configurePoll(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
poll: Poll?,
|
poll: Poll?,
|
||||||
requestUserID: String,
|
requestUserID: String,
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// NSManagedObjectContext.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension NSManagedObjectContext {
|
||||||
|
func safeFetch<T>(_ request: NSFetchRequest<T>) -> [T] where T : NSFetchRequestResult {
|
||||||
|
do {
|
||||||
|
return try fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ internal enum Asset {
|
||||||
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
||||||
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
||||||
}
|
}
|
||||||
|
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/mediaTypeIndicotor")
|
||||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||||
|
@ -78,6 +79,7 @@ internal enum Asset {
|
||||||
internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen")
|
internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen")
|
||||||
internal static let lightWhite = ColorAsset(name: "Colors/lightWhite")
|
internal static let lightWhite = ColorAsset(name: "Colors/lightWhite")
|
||||||
internal static let plusCircleFill = ImageAsset(name: "Colors/plus.circle.fill")
|
internal static let plusCircleFill = ImageAsset(name: "Colors/plus.circle.fill")
|
||||||
|
internal static let systemGreen = ColorAsset(name: "Colors/system.green")
|
||||||
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||||
}
|
}
|
||||||
internal enum Welcome {
|
internal enum Welcome {
|
||||||
|
|
|
@ -23,7 +23,13 @@ extension AvatarConfigurableView {
|
||||||
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
||||||
let placeholderImage: UIImage = {
|
let placeholderImage: UIImage = {
|
||||||
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
||||||
return placeholderImage.af.imageRoundedIntoCircle()
|
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
|
||||||
|
return placeholderImage
|
||||||
|
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
|
||||||
|
.af.imageRounded(withCornerRadius: 4, divideRadiusByImageScale: true)
|
||||||
|
} else {
|
||||||
|
return placeholderImage.af.imageRoundedIntoCircle()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// cancel previous task
|
// cancel previous task
|
||||||
|
@ -65,7 +71,8 @@ extension AvatarConfigurableView {
|
||||||
)
|
)
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||||
avatarImageView.layer.cornerCurve = .circular
|
avatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
|
||||||
|
|
||||||
default:
|
default:
|
||||||
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
||||||
avatarImageView.af.setImage(
|
avatarImageView.af.setImage(
|
||||||
|
@ -92,7 +99,7 @@ extension AvatarConfigurableView {
|
||||||
)
|
)
|
||||||
avatarButton.layer.masksToBounds = true
|
avatarButton.layer.masksToBounds = true
|
||||||
avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||||
avatarButton.layer.cornerCurve = .continuous
|
avatarButton.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous : .circular
|
||||||
default:
|
default:
|
||||||
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
||||||
avatarButton.af.setImage(
|
avatarButton.af.setImage(
|
||||||
|
|
|
@ -16,6 +16,10 @@ import ActiveLabel
|
||||||
// MARK: - ActionToolbarContainerDelegate
|
// MARK: - ActionToolbarContainerDelegate
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
|
||||||
|
StatusProviderFacade.responseToStatusReblogAction(provider: self, cell: cell)
|
||||||
|
}
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) {
|
||||||
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell)
|
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +50,16 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
contentWarningOverlayView.isUserInteractionEnabled = false
|
||||||
|
statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||||
guard let item = item(for: cell, indexPath: nil) else { return }
|
guard let item = item(for: cell, indexPath: nil) else { return }
|
||||||
|
|
||||||
|
@ -58,12 +71,12 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
contentWarningOverlayView.isUserInteractionEnabled = false
|
||||||
var snapshot = diffableDataSource.snapshot()
|
var snapshot = diffableDataSource.snapshot()
|
||||||
snapshot.reloadItems([item])
|
snapshot.reloadItems([item])
|
||||||
UIView.animate(withDuration: 0.33) {
|
UIView.animate(withDuration: 0.33) {
|
||||||
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil
|
contentWarningOverlayView.blurVisualEffectView.effect = nil
|
||||||
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0
|
contentWarningOverlayView.vibrancyVisualEffectView.alpha = 0.0
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
// update poll when toot appear
|
||||||
let now = Date()
|
let now = Date()
|
||||||
var pollID: Mastodon.Entity.Poll.ID?
|
var pollID: Mastodon.Entity.Poll.ID?
|
||||||
toot(for: cell, indexPath: indexPath)
|
toot(for: cell, indexPath: indexPath)
|
||||||
|
@ -70,6 +71,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
toot(for: cell, indexPath: indexPath)
|
toot(for: cell, indexPath: indexPath)
|
||||||
.sink { [weak self] toot in
|
.sink { [weak self] toot in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
let toot = toot?.reblog ?? toot
|
||||||
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
||||||
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
||||||
|
|
||||||
|
@ -87,10 +89,14 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
.sink { [weak self] toot in
|
.sink { [weak self] toot in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
||||||
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) {
|
||||||
videoPlayerViewModel.didEndDisplaying()
|
DispatchQueue.main.async {
|
||||||
|
videoPlayerViewModel.didEndDisplaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = toot?.mediaAttachments?.contains(currentAudioAttachment) {
|
||||||
|
self.context.audioPlaybackService.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import ActiveLabel
|
||||||
enum StatusProviderFacade {
|
enum StatusProviderFacade {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusProviderFacade {
|
extension StatusProviderFacade {
|
||||||
|
|
||||||
static func responseToStatusLikeAction(provider: StatusProvider) {
|
static func responseToStatusLikeAction(provider: StatusProvider) {
|
||||||
|
@ -56,10 +57,9 @@ extension StatusProviderFacade {
|
||||||
|
|
||||||
toot
|
toot
|
||||||
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
|
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
|
||||||
guard let toot = toot else { return nil }
|
guard let toot = toot?.reblog ?? toot else { return nil }
|
||||||
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
|
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
|
||||||
let targetToot = (toot.reblog ?? toot)
|
let isLiked = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
||||||
let isLiked = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
|
||||||
return isLiked ? .destroy : .create
|
return isLiked ? .destroy : .create
|
||||||
}()
|
}()
|
||||||
return (toot.objectID, favoriteKind)
|
return (toot.objectID, favoriteKind)
|
||||||
|
@ -120,6 +120,115 @@ extension StatusProviderFacade {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusProviderFacade {
|
||||||
|
|
||||||
|
|
||||||
|
static func responseToStatusReblogAction(provider: StatusProvider) {
|
||||||
|
_responseToStatusReblogAction(
|
||||||
|
provider: provider,
|
||||||
|
toot: provider.toot()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||||
|
_responseToStatusReblogAction(
|
||||||
|
provider: provider,
|
||||||
|
toot: provider.toot(for: cell, indexPath: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func _responseToStatusReblogAction(provider: StatusProvider, toot: Future<Toot?, Never>) {
|
||||||
|
// prepare authentication
|
||||||
|
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare current user infos
|
||||||
|
guard let _currentMastodonUser = provider.context.authenticationService.activeMastodonAuthentication.value?.user else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mastodonUserID = activeMastodonAuthenticationBox.userID
|
||||||
|
assert(_currentMastodonUser.id == mastodonUserID)
|
||||||
|
let mastodonUserObjectID = _currentMastodonUser.objectID
|
||||||
|
|
||||||
|
guard let context = provider.context else { return }
|
||||||
|
|
||||||
|
// haptic feedback generator
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
|
toot
|
||||||
|
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
|
||||||
|
guard let toot = toot?.reblog ?? toot else { return nil }
|
||||||
|
let reblogKind: Mastodon.API.Reblog.ReblogKind = {
|
||||||
|
let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
||||||
|
return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil))
|
||||||
|
}()
|
||||||
|
return (toot.objectID, reblogKind)
|
||||||
|
}
|
||||||
|
.map { tootObjectID, reblogKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.ReblogKind), Error> in
|
||||||
|
return context.apiService.reblog(
|
||||||
|
tootObjectID: tootObjectID,
|
||||||
|
mastodonUserObjectID: mastodonUserObjectID,
|
||||||
|
reblogKind: reblogKind
|
||||||
|
)
|
||||||
|
.map { tootID in (tootID, reblogKind) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
.switchToLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.handleEvents { _ in
|
||||||
|
generator.prepare()
|
||||||
|
responseFeedbackGenerator.prepare()
|
||||||
|
} receiveOutput: { _, reblogKind in
|
||||||
|
generator.impactOccurred()
|
||||||
|
switch reblogKind {
|
||||||
|
case .reblog:
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
|
||||||
|
case .undoReblog:
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
|
||||||
|
}
|
||||||
|
} receiveCompletion: { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure:
|
||||||
|
// TODO: handle error
|
||||||
|
break
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { tootID, reblogKind in
|
||||||
|
return context.apiService.reblog(
|
||||||
|
statusID: tootID,
|
||||||
|
reblogKind: reblogKind,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak provider] completion in
|
||||||
|
guard let provider = provider else { return }
|
||||||
|
if provider.view.window != nil {
|
||||||
|
responseFeedbackGenerator.impactOccurred()
|
||||||
|
}
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.store(in: &provider.disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusProviderFacade {
|
extension StatusProviderFacade {
|
||||||
enum Target {
|
enum Target {
|
||||||
case toot
|
case toot
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.600",
|
||||||
|
"blue" : "0",
|
||||||
|
"green" : "0",
|
||||||
|
"red" : "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.604",
|
||||||
|
"green" : "0.741",
|
||||||
|
"red" : "0.475"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,4 +99,4 @@ any server.";
|
||||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||||
"Scene.Welcome.Slogan" = "Social networking
|
"Scene.Welcome.Slogan" = "Social networking
|
||||||
back in your hands.";
|
back in your hands.";
|
||||||
|
|
|
@ -45,10 +45,18 @@ extension HomeTimelineViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToTopGapAction(action)
|
self.moveToTopGapAction(action)
|
||||||
}),
|
}),
|
||||||
|
UIAction(title: "First Reblog Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.moveToFirstReblogToot(action)
|
||||||
|
}),
|
||||||
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
|
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToFirstPollToot(action)
|
self.moveToFirstPollToot(action)
|
||||||
}),
|
}),
|
||||||
|
UIAction(title: "First Audio Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.moveToFirstAudioToot(action)
|
||||||
|
}),
|
||||||
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
|
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
// guard let self = self else { return }
|
// guard let self = self else { return }
|
||||||
// self.moveToFirstReplyToot(action)
|
// self.moveToFirstReplyToot(action)
|
||||||
|
@ -101,6 +109,26 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func moveToFirstReblogToot(_ sender: UIAction) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
|
return homeTimelineIndex.toot.reblog != nil
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||||
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
|
} else {
|
||||||
|
print("Not found reblog toot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func moveToFirstPollToot(_ sender: UIAction) {
|
@objc private func moveToFirstPollToot(_ sender: UIAction) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
@ -118,7 +146,28 @@ extension HomeTimelineViewController {
|
||||||
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
} else {
|
} else {
|
||||||
print("Not found status contains poll")
|
print("Not found poll status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func moveToFirstAudioToot(_ sender: UIAction) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
|
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
||||||
|
return toot.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||||
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
|
} else {
|
||||||
|
print("Not found audio toot")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,8 @@ extension HomeTimelineViewController {
|
||||||
|
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
|
context.videoPlaybackService.viewDidDisappear(from: self)
|
||||||
|
context.audioPlaybackService.viewDidDisappear(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
@ -237,7 +238,10 @@ extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
|
|
@ -81,6 +81,11 @@ extension PublicTimelineViewController {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
context.videoPlaybackService.viewDidDisappear(from: self)
|
||||||
|
context.audioPlaybackService.viewDidDisappear(from: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIScrollViewDelegate
|
// MARK: - UIScrollViewDelegate
|
||||||
|
@ -114,8 +119,11 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
||||||
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||||
|
}
|
||||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,12 @@ protocol MosaicImageViewContainerPresentable: class {
|
||||||
|
|
||||||
protocol MosaicImageViewContainerDelegate: class {
|
protocol MosaicImageViewContainerDelegate: class {
|
||||||
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||||
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
|
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class MosaicImageViewContainer: UIView {
|
final class MosaicImageViewContainer: UIView {
|
||||||
|
|
||||||
static let cornerRadius: CGFloat = 4
|
|
||||||
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
|
||||||
|
|
||||||
weak var delegate: MosaicImageViewContainerDelegate?
|
weak var delegate: MosaicImageViewContainerDelegate?
|
||||||
|
|
||||||
let container = UIStackView()
|
let container = UIStackView()
|
||||||
|
@ -37,14 +34,10 @@ final class MosaicImageViewContainer: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let blurVisualEffectView = UIVisualEffectView(effect: MosaicImageViewContainer.blurVisualEffect)
|
|
||||||
let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicImageViewContainer.blurVisualEffect))
|
let contentWarningOverlayView: ContentWarningOverlayView = {
|
||||||
let contentWarningLabel: UILabel = {
|
let contentWarningOverlayView = ContentWarningOverlayView()
|
||||||
let label = UILabel()
|
return contentWarningOverlayView
|
||||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
|
||||||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
|
||||||
label.textAlignment = .center
|
|
||||||
return label
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
@ -61,6 +54,12 @@ final class MosaicImageViewContainer: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate {
|
||||||
|
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
self.delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension MosaicImageViewContainer {
|
extension MosaicImageViewContainer {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
|
@ -77,32 +76,7 @@ extension MosaicImageViewContainer {
|
||||||
containerHeightLayoutConstraint
|
containerHeightLayoutConstraint
|
||||||
])
|
])
|
||||||
|
|
||||||
// add blur visual effect view in the setup method
|
contentWarningOverlayView.delegate = self
|
||||||
blurVisualEffectView.layer.masksToBounds = true
|
|
||||||
blurVisualEffectView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
|
|
||||||
blurVisualEffectView.layer.cornerCurve = .continuous
|
|
||||||
|
|
||||||
vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor),
|
|
||||||
vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor),
|
|
||||||
vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor),
|
|
||||||
vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor),
|
|
||||||
contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor),
|
|
||||||
contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
blurVisualEffectView.isUserInteractionEnabled = true
|
|
||||||
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
||||||
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.visualEffectViewTapGestureRecognizerHandler(_:)))
|
|
||||||
blurVisualEffectView.addGestureRecognizer(tapGesture)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -117,9 +91,10 @@ extension MosaicImageViewContainer {
|
||||||
container.subviews.forEach { subview in
|
container.subviews.forEach { subview in
|
||||||
subview.removeFromSuperview()
|
subview.removeFromSuperview()
|
||||||
}
|
}
|
||||||
blurVisualEffectView.removeFromSuperview()
|
contentWarningOverlayView.removeFromSuperview()
|
||||||
blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect
|
contentWarningOverlayView.blurVisualEffectView.effect = ContentWarningOverlayView.blurVisualEffect
|
||||||
vibrancyVisualEffectView.alpha = 1.0
|
contentWarningOverlayView.vibrancyVisualEffectView.alpha = 1.0
|
||||||
|
contentWarningOverlayView.isUserInteractionEnabled = true
|
||||||
imageViews = []
|
imageViews = []
|
||||||
|
|
||||||
container.spacing = 1
|
container.spacing = 1
|
||||||
|
@ -140,7 +115,7 @@ extension MosaicImageViewContainer {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
imageViews.append(imageView)
|
imageViews.append(imageView)
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
|
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
||||||
imageView.layer.cornerCurve = .continuous
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
@ -155,13 +130,12 @@ extension MosaicImageViewContainer {
|
||||||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||||
containerHeightLayoutConstraint.isActive = true
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
addSubview(contentWarningOverlayView)
|
||||||
addSubview(blurVisualEffectView)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
blurVisualEffectView.topAnchor.constraint(equalTo: imageView.topAnchor),
|
contentWarningOverlayView.topAnchor.constraint(equalTo: imageView.topAnchor),
|
||||||
blurVisualEffectView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
|
contentWarningOverlayView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
|
||||||
blurVisualEffectView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
contentWarningOverlayView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||||
blurVisualEffectView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
return imageView
|
return imageView
|
||||||
|
@ -193,7 +167,7 @@ extension MosaicImageViewContainer {
|
||||||
self.imageViews.append(contentsOf: imageViews)
|
self.imageViews.append(contentsOf: imageViews)
|
||||||
imageViews.forEach { imageView in
|
imageViews.forEach { imageView in
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
|
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
||||||
imageView.layer.cornerCurve = .continuous
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
}
|
}
|
||||||
|
@ -242,13 +216,12 @@ extension MosaicImageViewContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
addSubview(contentWarningOverlayView)
|
||||||
addSubview(blurVisualEffectView)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
blurVisualEffectView.topAnchor.constraint(equalTo: container.topAnchor),
|
contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor),
|
||||||
blurVisualEffectView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
blurVisualEffectView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
blurVisualEffectView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
return imageViews
|
return imageViews
|
||||||
|
@ -260,7 +233,7 @@ extension MosaicImageViewContainer {
|
||||||
|
|
||||||
@objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
@objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
delegate?.mosaicImageViewContainer(self, didTapContentWarningVisualEffectView: blurVisualEffectView)
|
delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
//
|
||||||
|
// PlayerContainerView+MediaTypeIndicotorView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension PlayerContainerView {
|
||||||
|
|
||||||
|
final class MediaTypeIndicotorView: UIView {
|
||||||
|
|
||||||
|
static let indicatorViewSize = CGSize(width: 47, height: 25)
|
||||||
|
|
||||||
|
let maskLayer = CAShapeLayer()
|
||||||
|
|
||||||
|
let label: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = .white
|
||||||
|
label.textAlignment = .right
|
||||||
|
label.adjustsFontSizeToFitWidth = true
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let path = UIBezierPath()
|
||||||
|
path.move(to: CGPoint(x: bounds.width, y: bounds.height))
|
||||||
|
path.addLine(to: CGPoint(x: bounds.width, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: bounds.width * 0.5, y: 0))
|
||||||
|
path.addCurve(
|
||||||
|
to: CGPoint(x: 0, y: bounds.height),
|
||||||
|
controlPoint1: CGPoint(x: bounds.width * 0.2, y: 0),
|
||||||
|
controlPoint2: CGPoint(x: 0, y: bounds.height * 0.3)
|
||||||
|
)
|
||||||
|
path.close()
|
||||||
|
|
||||||
|
maskLayer.frame = bounds
|
||||||
|
maskLayer.path = path.cgPath
|
||||||
|
layer.mask = maskLayer
|
||||||
|
|
||||||
|
layer.cornerRadius = PlayerContainerView.cornerRadius
|
||||||
|
layer.maskedCorners = [.layerMaxXMaxYCorner]
|
||||||
|
layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlayerContainerView.MediaTypeIndicotorView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color
|
||||||
|
layoutMargins = UIEdgeInsets(top: 3, left: 13, bottom: 0, right: 6)
|
||||||
|
|
||||||
|
addSubview(label)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func roundedFont(weight: UIFont.Weight,fontSize: CGFloat) -> UIFont {
|
||||||
|
let systemFont = UIFont.systemFont(ofSize: fontSize, weight: weight)
|
||||||
|
guard let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont }
|
||||||
|
let roundedFont = UIFont(descriptor: descriptor, size: fontSize)
|
||||||
|
return roundedFont
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
||||||
|
let fontSize: CGFloat = 18
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case .gif:
|
||||||
|
label.font = PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .heavy, fontSize: fontSize)
|
||||||
|
label.text = "GIF"
|
||||||
|
case .video:
|
||||||
|
let configuration = UIImage.SymbolConfiguration(font: PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .regular, fontSize: fontSize))
|
||||||
|
let image = UIImage(systemName: "video.fill", withConfiguration: configuration)!
|
||||||
|
let attachment = NSTextAttachment()
|
||||||
|
attachment.image = image.withTintColor(.white)
|
||||||
|
label.attributedText = NSAttributedString(attachment: attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 47) {
|
||||||
|
let view = PlayerContainerView.MediaTypeIndicotorView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 25),
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 47),
|
||||||
|
])
|
||||||
|
view.setMediaKind(kind: .gif)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 47, height: 25))
|
||||||
|
UIViewPreview(width: 47) {
|
||||||
|
let view = PlayerContainerView.MediaTypeIndicotorView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 25),
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 47),
|
||||||
|
])
|
||||||
|
view.setMediaKind(kind: .video)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 47, height: 25))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// MosaicPlayerView.swift
|
// PlayerContainerView.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by xiaojian sun on 2021/3/10.
|
// Created by xiaojian sun on 2021/3/10.
|
||||||
|
@ -8,22 +8,28 @@
|
||||||
import AVKit
|
import AVKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class MosaicPlayerView: UIView {
|
protocol PlayerContainerViewDelegate: class {
|
||||||
|
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PlayerContainerView: UIView {
|
||||||
static let cornerRadius: CGFloat = 8
|
static let cornerRadius: CGFloat = 8
|
||||||
|
|
||||||
private let container = UIView()
|
private let container = UIView()
|
||||||
private let touchBlockingView = TouchBlockingView()
|
private let touchBlockingView = TouchBlockingView()
|
||||||
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
let contentWarningOverlayView: ContentWarningOverlayView = {
|
||||||
|
let contentWarningOverlayView = ContentWarningOverlayView()
|
||||||
|
return contentWarningOverlayView
|
||||||
|
}()
|
||||||
|
|
||||||
let playerViewController = AVPlayerViewController()
|
let playerViewController = AVPlayerViewController()
|
||||||
|
|
||||||
let gifIndicatorLabel: UILabel = {
|
let mediaTypeIndicotorView = MediaTypeIndicotorView()
|
||||||
let label = UILabel()
|
let mediaTypeIndicotorViewInContentWarningOverlay = MediaTypeIndicotorView()
|
||||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
|
||||||
label.text = "GIF"
|
weak var delegate: PlayerContainerViewDelegate?
|
||||||
label.textColor = .white
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
@ -36,7 +42,7 @@ final class MosaicPlayerView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MosaicPlayerView {
|
extension PlayerContainerView {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(container)
|
addSubview(container)
|
||||||
|
@ -49,27 +55,52 @@ extension MosaicPlayerView {
|
||||||
containerHeightLayoutConstraint,
|
containerHeightLayoutConstraint,
|
||||||
])
|
])
|
||||||
|
|
||||||
addSubview(gifIndicatorLabel)
|
|
||||||
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
gifIndicatorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 4),
|
|
||||||
gifIndicatorLabel.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
// will not influence full-screen playback
|
// will not influence full-screen playback
|
||||||
playerViewController.view.layer.masksToBounds = true
|
playerViewController.view.layer.masksToBounds = true
|
||||||
playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius
|
playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius
|
||||||
playerViewController.view.layer.cornerCurve = .continuous
|
playerViewController.view.layer.cornerCurve = .continuous
|
||||||
|
|
||||||
|
// mediaType
|
||||||
|
mediaTypeIndicotorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playerViewController.contentOverlayView!.addSubview(mediaTypeIndicotorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
||||||
|
mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
||||||
|
mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh),
|
||||||
|
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
addSubview(contentWarningOverlayView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
contentWarningOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
contentWarningOverlayView.delegate = self
|
||||||
|
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor),
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor),
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh),
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MosaicPlayerView {
|
// MARK: - ContentWarningOverlayViewDelegate
|
||||||
|
extension PlayerContainerView: ContentWarningOverlayViewDelegate {
|
||||||
|
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
delegate?.playerContainerView(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlayerContainerView {
|
||||||
func reset() {
|
func reset() {
|
||||||
// note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing
|
// note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing
|
||||||
|
|
||||||
gifIndicatorLabel.removeFromSuperview()
|
|
||||||
|
|
||||||
playerViewController.willMove(toParent: nil)
|
playerViewController.willMove(toParent: nil)
|
||||||
playerViewController.view.removeFromSuperview()
|
playerViewController.view.removeFromSuperview()
|
||||||
playerViewController.removeFromParent()
|
playerViewController.removeFromParent()
|
||||||
|
@ -109,13 +140,19 @@ extension MosaicPlayerView {
|
||||||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||||
containerHeightLayoutConstraint.isActive = true
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
bringSubviewToFront(mediaTypeIndicotorView)
|
||||||
touchBlockingView.addSubview(gifIndicatorLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
touchBlockingView.trailingAnchor.constraint(equalTo: gifIndicatorLabel.trailingAnchor, constant: 8),
|
|
||||||
touchBlockingView.bottomAnchor.constraint(equalTo: gifIndicatorLabel.bottomAnchor, constant: 8),
|
|
||||||
])
|
|
||||||
|
|
||||||
return playerViewController
|
return playerViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
||||||
|
mediaTypeIndicotorView.setMediaKind(kind: kind)
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.setMediaKind(kind: kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMediaIndicator(isHidden: Bool) {
|
||||||
|
mediaTypeIndicotorView.alpha = isHidden ? 0 : 1
|
||||||
|
mediaTypeIndicotorViewInContentWarningOverlay.alpha = isHidden ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
//
|
||||||
|
// ContentWarningOverlayView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/3/11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ContentWarningOverlayViewDelegate: class {
|
||||||
|
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentWarningOverlayView: UIView {
|
||||||
|
|
||||||
|
static let cornerRadius: CGFloat = 4
|
||||||
|
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||||
|
let blurVisualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect)
|
||||||
|
let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect))
|
||||||
|
|
||||||
|
let contentWarningLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
||||||
|
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
|
weak var delegate: ContentWarningOverlayViewDelegate?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ContentWarningOverlayView {
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
// add blur visual effect view in the setup method
|
||||||
|
blurVisualEffectView.layer.masksToBounds = true
|
||||||
|
blurVisualEffectView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
||||||
|
blurVisualEffectView.layer.cornerCurve = .continuous
|
||||||
|
|
||||||
|
vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor),
|
||||||
|
vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor),
|
||||||
|
vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor),
|
||||||
|
vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(blurVisualEffectView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurVisualEffectView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
blurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:)))
|
||||||
|
addGestureRecognizer(tapGestureRecognizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ContentWarningOverlayView {
|
||||||
|
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.contentWarningOverlayViewDidPressed(self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import AlamofireImage
|
||||||
|
|
||||||
protocol StatusViewDelegate: class {
|
protocol StatusViewDelegate: class {
|
||||||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ final class StatusView: UIView {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
|
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
|
||||||
label.textColor = Asset.Colors.Label.secondary.color
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
label.text = "Bob boosted"
|
label.text = "Bob reblogged"
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ final class StatusView: UIView {
|
||||||
button.setImage(placeholderImage, for: .normal)
|
button.setImage(placeholderImage, for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
|
||||||
let nameLabel: UILabel = {
|
let nameLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
@ -156,7 +158,7 @@ final class StatusView: UIView {
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let mosaicPlayerView = MosaicPlayerView()
|
let playerContainerView = PlayerContainerView()
|
||||||
|
|
||||||
let audioView: AudioContainerView = {
|
let audioView: AudioContainerView = {
|
||||||
let audioView = AudioContainerView()
|
let audioView = AudioContainerView()
|
||||||
|
@ -240,6 +242,14 @@ extension StatusView {
|
||||||
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarView.addSubview(avatarStackedContainerButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackedContainerButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
||||||
|
avatarStackedContainerButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
||||||
|
avatarStackedContainerButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
|
avatarStackedContainerButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
// author meta container: [title container | subtitle container]
|
// author meta container: [title container | subtitle container]
|
||||||
let authorMetaContainerStackView = UIStackView()
|
let authorMetaContainerStackView = UIStackView()
|
||||||
|
@ -353,7 +363,7 @@ extension StatusView {
|
||||||
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
|
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
|
||||||
])
|
])
|
||||||
// video gif
|
// video gif
|
||||||
statusContainerStackView.addArrangedSubview(mosaicPlayerView)
|
statusContainerStackView.addArrangedSubview(playerContainerView)
|
||||||
|
|
||||||
// action toolbar container
|
// action toolbar container
|
||||||
containerStackView.addArrangedSubview(actionToolbarContainer)
|
containerStackView.addArrangedSubview(actionToolbarContainer)
|
||||||
|
@ -364,12 +374,15 @@ extension StatusView {
|
||||||
pollTableView.isHidden = true
|
pollTableView.isHidden = true
|
||||||
pollStatusStackView.isHidden = true
|
pollStatusStackView.isHidden = true
|
||||||
audioView.isHidden = true
|
audioView.isHidden = true
|
||||||
mosaicPlayerView.isHidden = true
|
playerContainerView.isHidden = true
|
||||||
|
|
||||||
|
avatarStackedContainerButton.isHidden = true
|
||||||
contentWarningBlurContentImageView.isHidden = true
|
contentWarningBlurContentImageView.isHidden = true
|
||||||
statusContentWarningContainerStackView.isHidden = true
|
statusContentWarningContainerStackView.isHidden = true
|
||||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||||
|
|
||||||
|
playerContainerView.delegate = self
|
||||||
|
|
||||||
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
|
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
|
||||||
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
@ -420,6 +433,13 @@ extension StatusView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - PlayerContainerViewDelegate
|
||||||
|
extension StatusView: PlayerContainerViewDelegate {
|
||||||
|
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
delegate?.statusView(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - AvatarConfigurableView
|
// MARK: - AvatarConfigurableView
|
||||||
extension StatusView: AvatarConfigurableView {
|
extension StatusView: AvatarConfigurableView {
|
||||||
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
|
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
|
||||||
|
@ -435,6 +455,7 @@ import SwiftUI
|
||||||
struct StatusView_Previews: PreviewProvider {
|
struct StatusView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static let avatarFlora = UIImage(named: "tiraya-adam")
|
static let avatarFlora = UIImage(named: "tiraya-adam")
|
||||||
|
static let avatarMarkus = UIImage(named: "markus-spiske")
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
|
@ -449,6 +470,49 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
return statusView
|
return statusView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("Normal")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let statusView = StatusView()
|
||||||
|
statusView.headerContainerStackView.isHidden = false
|
||||||
|
statusView.avatarButton.isHidden = true
|
||||||
|
statusView.avatarStackedContainerButton.isHidden = false
|
||||||
|
statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarFlora
|
||||||
|
)
|
||||||
|
)
|
||||||
|
statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarMarkus
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return statusView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("Reblog")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
||||||
|
statusView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarFlora
|
||||||
|
)
|
||||||
|
)
|
||||||
|
statusView.headerContainerStackView.isHidden = false
|
||||||
|
let images = MosaicImageView_Previews.images
|
||||||
|
let imageViews = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
imageView.image = images[i]
|
||||||
|
}
|
||||||
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||||
|
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
|
||||||
|
statusView.isStatusTextSensitive = false
|
||||||
|
return statusView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 380))
|
||||||
|
.previewDisplayName("Image Meida")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
||||||
statusView.configure(
|
statusView.configure(
|
||||||
|
@ -472,6 +536,7 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
return statusView
|
return statusView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 380))
|
.previewLayout(.fixed(width: 375, height: 380))
|
||||||
|
.previewDisplayName("Content Sensitive")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
//
|
||||||
|
// AvatarStackContainerButton.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
final class AvatarStackedImageView: UIImageView { }
|
||||||
|
|
||||||
|
// MARK: - AvatarConfigurableView
|
||||||
|
extension AvatarStackedImageView: AvatarConfigurableView {
|
||||||
|
static var configurableAvatarImageSize: CGSize { CGSize(width: 28, height: 28) }
|
||||||
|
static var configurableAvatarImageCornerRadius: CGFloat { 4 }
|
||||||
|
var configurableAvatarImageView: UIImageView? { self }
|
||||||
|
var configurableAvatarButton: UIButton? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AvatarStackContainerButton: UIControl {
|
||||||
|
|
||||||
|
static let containerSize = CGSize(width: 42, height: 42)
|
||||||
|
static let maskOffset: CGFloat = 2
|
||||||
|
|
||||||
|
// UIControl.Event - Application: 0x0F000000
|
||||||
|
static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000
|
||||||
|
var primaryActionState: UIControl.State = .normal
|
||||||
|
|
||||||
|
let topLeadingAvatarStackedImageView = AvatarStackedImageView()
|
||||||
|
let bottomTrailingAvatarStackedImageView = AvatarStackedImageView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AvatarStackContainerButton {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(topLeadingAvatarStackedImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topLeadingAvatarStackedImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
topLeadingAvatarStackedImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
topLeadingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh),
|
||||||
|
topLeadingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
bottomTrailingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(bottomTrailingAvatarStackedImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomTrailingAvatarStackedImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
bottomTrailingAvatarStackedImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
bottomTrailingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh),
|
||||||
|
bottomTrailingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
// mask topLeadingAvatarStackedImageView
|
||||||
|
let offset: CGFloat = 2
|
||||||
|
let path: CGPath = {
|
||||||
|
let path = CGMutablePath()
|
||||||
|
path.addRect(CGRect(origin: .zero, size: AvatarStackedImageView.configurableAvatarImageSize))
|
||||||
|
let mirrorScale: CGFloat = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? -1 : 1
|
||||||
|
path.addPath(UIBezierPath(
|
||||||
|
roundedRect: CGRect(
|
||||||
|
x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset),
|
||||||
|
y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset,
|
||||||
|
width: AvatarStackedImageView.configurableAvatarImageSize.width,
|
||||||
|
height: AvatarStackedImageView.configurableAvatarImageSize.height
|
||||||
|
),
|
||||||
|
cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius
|
||||||
|
).cgPath)
|
||||||
|
return path
|
||||||
|
}()
|
||||||
|
let maskShapeLayer = CAShapeLayer()
|
||||||
|
maskShapeLayer.backgroundColor = UIColor.black.cgColor
|
||||||
|
maskShapeLayer.fillRule = .evenOdd
|
||||||
|
maskShapeLayer.path = path
|
||||||
|
topLeadingAvatarStackedImageView.layer.mask = maskShapeLayer
|
||||||
|
|
||||||
|
topLeadingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
|
||||||
|
bottomTrailingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return AvatarStackContainerButton.containerSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
updateState(touch: touch, event: event)
|
||||||
|
return super.beginTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
updateState(touch: touch, event: event)
|
||||||
|
return super.continueTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
resetState()
|
||||||
|
|
||||||
|
if let touch = touch {
|
||||||
|
if AvatarStackContainerButton.isTouching(touch, view: self, event: event) {
|
||||||
|
sendActions(for: AvatarStackContainerButton.primaryAction)
|
||||||
|
} else {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.endTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cancelTracking(with event: UIEvent?) {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
resetState()
|
||||||
|
super.cancelTracking(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AvatarStackContainerButton {
|
||||||
|
|
||||||
|
private func updateAppearance() {
|
||||||
|
topLeadingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
|
||||||
|
bottomTrailingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isTouching(_ touch: UITouch, view: UIView, event: UIEvent?) -> Bool {
|
||||||
|
let location = touch.location(in: view)
|
||||||
|
return view.point(inside: location, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetState() {
|
||||||
|
primaryActionState = .normal
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateState(touch: UITouch, event: UIEvent?) {
|
||||||
|
primaryActionState = AvatarStackContainerButton.isTouching(touch, view: self, event: event) ? .highlighted : .normal
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AvatarStackContainerButton_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 42) {
|
||||||
|
let avatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
avatarStackContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackContainerButton.widthAnchor.constraint(equalToConstant: 42),
|
||||||
|
avatarStackContainerButton.heightAnchor.constraint(equalToConstant: 42),
|
||||||
|
])
|
||||||
|
return avatarStackContainerButton
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 42, height: 42))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -21,8 +21,10 @@ protocol StatusTableViewCellDelegate: class {
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||||
|
@ -53,8 +55,8 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
statusView.isStatusTextSensitive = false
|
statusView.isStatusTextSensitive = false
|
||||||
statusView.cleanUpContentWarning()
|
statusView.cleanUpContentWarning()
|
||||||
statusView.pollTableView.dataSource = nil
|
statusView.pollTableView.dataSource = nil
|
||||||
statusView.mosaicPlayerView.reset()
|
statusView.playerContainerView.reset()
|
||||||
statusView.mosaicPlayerView.isHidden = true
|
statusView.playerContainerView.isHidden = true
|
||||||
disposeBag.removeAll()
|
disposeBag.removeAll()
|
||||||
observations.removeAll()
|
observations.removeAll()
|
||||||
}
|
}
|
||||||
|
@ -196,6 +198,10 @@ extension StatusTableViewCell: StatusViewDelegate {
|
||||||
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
|
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
delegate?.statusTableViewCell(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
|
}
|
||||||
|
|
||||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||||
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
|
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
|
||||||
}
|
}
|
||||||
|
@ -209,8 +215,8 @@ extension StatusTableViewCell: MosaicImageViewContainerDelegate {
|
||||||
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
|
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapContentWarningVisualEffectView: visualEffectView)
|
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -220,8 +226,8 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton) {
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
|
||||||
|
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, reblogButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) {
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) {
|
||||||
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender)
|
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
|
|
||||||
protocol ActionToolbarContainerDelegate: class {
|
protocol ActionToolbarContainerDelegate: class {
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton)
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton)
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton)
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton)
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton)
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton)
|
||||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton)
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton)
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,16 @@ protocol ActionToolbarContainerDelegate: class {
|
||||||
final class ActionToolbarContainer: UIView {
|
final class ActionToolbarContainer: UIView {
|
||||||
|
|
||||||
let replyButton = HitTestExpandedButton()
|
let replyButton = HitTestExpandedButton()
|
||||||
let retootButton = HitTestExpandedButton()
|
let reblogButton = HitTestExpandedButton()
|
||||||
let starButton = HitTestExpandedButton()
|
let favoriteButton = HitTestExpandedButton()
|
||||||
let moreButton = HitTestExpandedButton()
|
let moreButton = HitTestExpandedButton()
|
||||||
|
|
||||||
var isStarButtonHighlight: Bool = false {
|
var isReblogButtonHighlight: Bool = false {
|
||||||
didSet { isStarButtonHighlightStateDidChange(to: isStarButtonHighlight) }
|
didSet { isReblogButtonHighlightStateDidChange(to: isReblogButtonHighlight) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFavoriteButtonHighlight: Bool = false {
|
||||||
|
didSet { isFavoriteButtonHighlightStateDidChange(to: isFavoriteButtonHighlight) }
|
||||||
}
|
}
|
||||||
|
|
||||||
weak var delegate: ActionToolbarContainerDelegate?
|
weak var delegate: ActionToolbarContainerDelegate?
|
||||||
|
@ -57,8 +61,8 @@ extension ActionToolbarContainer {
|
||||||
])
|
])
|
||||||
|
|
||||||
replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside)
|
replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
retootButton.addTarget(self, action: #selector(ActionToolbarContainer.retootButtonDidPressed(_:)), for: .touchUpInside)
|
reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.reblogButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
starButton.addTarget(self, action: #selector(ActionToolbarContainer.starButtonDidPressed(_:)), for: .touchUpInside)
|
favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.favoriteButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside)
|
moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +93,7 @@ extension ActionToolbarContainer {
|
||||||
subview.removeFromSuperview()
|
subview.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttons = [replyButton, retootButton, starButton, moreButton]
|
let buttons = [replyButton, reblogButton, favoriteButton, moreButton]
|
||||||
buttons.forEach { button in
|
buttons.forEach { button in
|
||||||
button.tintColor = Asset.Colors.Button.actionToolbar.color
|
button.tintColor = Asset.Colors.Button.actionToolbar.color
|
||||||
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||||
|
@ -109,28 +113,28 @@ extension ActionToolbarContainer {
|
||||||
button.contentHorizontalAlignment = .leading
|
button.contentHorizontalAlignment = .leading
|
||||||
}
|
}
|
||||||
replyButton.setImage(replyImage, for: .normal)
|
replyButton.setImage(replyImage, for: .normal)
|
||||||
retootButton.setImage(reblogImage, for: .normal)
|
reblogButton.setImage(reblogImage, for: .normal)
|
||||||
starButton.setImage(starImage, for: .normal)
|
favoriteButton.setImage(starImage, for: .normal)
|
||||||
moreButton.setImage(moreImage, for: .normal)
|
moreButton.setImage(moreImage, for: .normal)
|
||||||
|
|
||||||
container.axis = .horizontal
|
container.axis = .horizontal
|
||||||
container.distribution = .fill
|
container.distribution = .fill
|
||||||
|
|
||||||
replyButton.translatesAutoresizingMaskIntoConstraints = false
|
replyButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
retootButton.translatesAutoresizingMaskIntoConstraints = false
|
reblogButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
starButton.translatesAutoresizingMaskIntoConstraints = false
|
favoriteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
moreButton.translatesAutoresizingMaskIntoConstraints = false
|
moreButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
container.addArrangedSubview(replyButton)
|
container.addArrangedSubview(replyButton)
|
||||||
container.addArrangedSubview(retootButton)
|
container.addArrangedSubview(reblogButton)
|
||||||
container.addArrangedSubview(starButton)
|
container.addArrangedSubview(favoriteButton)
|
||||||
container.addArrangedSubview(moreButton)
|
container.addArrangedSubview(moreButton)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
replyButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
replyButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||||
replyButton.heightAnchor.constraint(equalTo: retootButton.heightAnchor).priority(.defaultHigh),
|
replyButton.heightAnchor.constraint(equalTo: reblogButton.heightAnchor).priority(.defaultHigh),
|
||||||
replyButton.heightAnchor.constraint(equalTo: starButton.heightAnchor).priority(.defaultHigh),
|
replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh),
|
||||||
replyButton.heightAnchor.constraint(equalTo: moreButton.heightAnchor).priority(.defaultHigh),
|
replyButton.heightAnchor.constraint(equalTo: moreButton.heightAnchor).priority(.defaultHigh),
|
||||||
replyButton.widthAnchor.constraint(equalTo: retootButton.widthAnchor).priority(.defaultHigh),
|
replyButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor).priority(.defaultHigh),
|
||||||
replyButton.widthAnchor.constraint(equalTo: starButton.widthAnchor).priority(.defaultHigh),
|
replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
moreButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
moreButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
moreButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
moreButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||||
|
@ -140,16 +144,16 @@ extension ActionToolbarContainer {
|
||||||
button.contentHorizontalAlignment = .center
|
button.contentHorizontalAlignment = .center
|
||||||
}
|
}
|
||||||
replyButton.setImage(replyImage, for: .normal)
|
replyButton.setImage(replyImage, for: .normal)
|
||||||
retootButton.setImage(reblogImage, for: .normal)
|
reblogButton.setImage(reblogImage, for: .normal)
|
||||||
starButton.setImage(starImage, for: .normal)
|
favoriteButton.setImage(starImage, for: .normal)
|
||||||
|
|
||||||
container.axis = .horizontal
|
container.axis = .horizontal
|
||||||
container.spacing = 8
|
container.spacing = 8
|
||||||
container.distribution = .fillEqually
|
container.distribution = .fillEqually
|
||||||
|
|
||||||
container.addArrangedSubview(replyButton)
|
container.addArrangedSubview(replyButton)
|
||||||
container.addArrangedSubview(retootButton)
|
container.addArrangedSubview(reblogButton)
|
||||||
container.addArrangedSubview(starButton)
|
container.addArrangedSubview(favoriteButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,11 +162,18 @@ extension ActionToolbarContainer {
|
||||||
return oldStyle != style
|
return oldStyle != style
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isStarButtonHighlightStateDidChange(to isHighlight: Bool) {
|
private func isReblogButtonHighlightStateDidChange(to isHighlight: Bool) {
|
||||||
|
let tintColor = isHighlight ? Asset.Colors.systemGreen.color : Asset.Colors.Button.actionToolbar.color
|
||||||
|
reblogButton.tintColor = tintColor
|
||||||
|
reblogButton.setTitleColor(tintColor, for: .normal)
|
||||||
|
reblogButton.setTitleColor(tintColor, for: .highlighted)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isFavoriteButtonHighlightStateDidChange(to isHighlight: Bool) {
|
||||||
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Button.actionToolbar.color
|
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Button.actionToolbar.color
|
||||||
starButton.tintColor = tintColor
|
favoriteButton.tintColor = tintColor
|
||||||
starButton.setTitleColor(tintColor, for: .normal)
|
favoriteButton.setTitleColor(tintColor, for: .normal)
|
||||||
starButton.setTitleColor(tintColor, for: .highlighted)
|
favoriteButton.setTitleColor(tintColor, for: .highlighted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,12 +184,12 @@ extension ActionToolbarContainer {
|
||||||
delegate?.actionToolbarContainer(self, replayButtonDidPressed: sender)
|
delegate?.actionToolbarContainer(self, replayButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func retootButtonDidPressed(_ sender: UIButton) {
|
@objc private func reblogButtonDidPressed(_ sender: UIButton) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
delegate?.actionToolbarContainer(self, retootButtonDidPressed: sender)
|
delegate?.actionToolbarContainer(self, reblogButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func starButtonDidPressed(_ sender: UIButton) {
|
@objc private func favoriteButtonDidPressed(_ sender: UIButton) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
delegate?.actionToolbarContainer(self, starButtonDidPressed: sender)
|
delegate?.actionToolbarContainer(self, starButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,57 +13,57 @@ class AudioContainerViewModel {
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
audioAttachment: Attachment,
|
audioAttachment: Attachment,
|
||||||
videoPlaybackService: VideoPlaybackService
|
audioService: AudioPlaybackService
|
||||||
) {
|
) {
|
||||||
guard let duration = audioAttachment.meta?.original?.duration else { return }
|
guard let duration = audioAttachment.meta?.original?.duration else { return }
|
||||||
let audioView = cell.statusView.audioView
|
let audioView = cell.statusView.audioView
|
||||||
audioView.timeLabel.text = duration.asString(style: .positional)
|
audioView.timeLabel.text = duration.asString(style: .positional)
|
||||||
|
|
||||||
audioView.playButton.publisher(for: .touchUpInside)
|
audioView.playButton.publisher(for: .touchUpInside)
|
||||||
.sink { _ in
|
.sink { [weak audioService] _ in
|
||||||
if audioAttachment === AudioPlayer.shared.attachment {
|
guard let audioService = audioService else { return }
|
||||||
if AudioPlayer.shared.isPlaying() {
|
if audioAttachment === audioService.attachment {
|
||||||
AudioPlayer.shared.pause()
|
if audioService.isPlaying() {
|
||||||
|
audioService.pause()
|
||||||
} else {
|
} else {
|
||||||
AudioPlayer.shared.resume()
|
audioService.resume()
|
||||||
videoPlaybackService.pauseWhenPlayAudio()
|
|
||||||
}
|
}
|
||||||
if AudioPlayer.shared.currentTimeSubject.value == 0 {
|
if audioService.currentTimeSubject.value == 0 {
|
||||||
AudioPlayer.shared.playAudio(audioAttachment: audioAttachment)
|
audioService.playAudio(audioAttachment: audioAttachment)
|
||||||
videoPlaybackService.pauseWhenPlayAudio()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AudioPlayer.shared.playAudio(audioAttachment: audioAttachment)
|
audioService.playAudio(audioAttachment: audioAttachment)
|
||||||
videoPlaybackService.pauseWhenPlayAudio()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
audioView.slider.publisher(for: .valueChanged)
|
audioView.slider.publisher(for: .valueChanged)
|
||||||
.sink { slider in
|
.sink { [weak audioService] slider in
|
||||||
|
guard let audioService = audioService else { return }
|
||||||
let slider = slider as! UISlider
|
let slider = slider as! UISlider
|
||||||
let time = Double(slider.value) * duration
|
let time = Double(slider.value) * duration
|
||||||
AudioPlayer.shared.seekToTime(time: time)
|
audioService.seekToTime(time: time)
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
observePlayer(cell: cell, audioAttachment: audioAttachment)
|
observePlayer(cell: cell, audioAttachment: audioAttachment, audioService: audioService)
|
||||||
if audioAttachment != AudioPlayer.shared.attachment {
|
if audioAttachment != audioService.attachment {
|
||||||
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func observePlayer(
|
static func observePlayer(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
audioAttachment: Attachment
|
audioAttachment: Attachment,
|
||||||
|
audioService: AudioPlaybackService
|
||||||
) {
|
) {
|
||||||
let audioView = cell.statusView.audioView
|
let audioView = cell.statusView.audioView
|
||||||
var lastCurrentTimeSubject: TimeInterval?
|
var lastCurrentTimeSubject: TimeInterval?
|
||||||
AudioPlayer.shared.currentTimeSubject
|
audioService.currentTimeSubject
|
||||||
.throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true)
|
||||||
.compactMap { time -> (TimeInterval, Float)? in
|
.compactMap { [weak audioService] time -> (TimeInterval, Float)? in
|
||||||
defer {
|
defer {
|
||||||
lastCurrentTimeSubject = time
|
lastCurrentTimeSubject = time
|
||||||
}
|
}
|
||||||
guard audioAttachment === AudioPlayer.shared.attachment else { return nil }
|
guard audioAttachment === audioService?.attachment else { return nil }
|
||||||
guard let duration = audioAttachment.meta?.original?.duration else { return nil }
|
guard let duration = audioAttachment.meta?.original?.duration else { return nil }
|
||||||
|
|
||||||
if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
|
if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
|
||||||
|
@ -78,10 +78,10 @@ class AudioContainerViewModel {
|
||||||
audioView.slider.setValue(progress, animated: true)
|
audioView.slider.setValue(progress, animated: true)
|
||||||
})
|
})
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
AudioPlayer.shared.playbackState
|
audioService.playbackState
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveValue: { playbackState in
|
.sink(receiveValue: { playbackState in
|
||||||
if audioAttachment === AudioPlayer.shared.attachment {
|
if audioAttachment === audioService.attachment {
|
||||||
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: playbackState)
|
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: playbackState)
|
||||||
} else {
|
} else {
|
||||||
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import UIKit
|
||||||
final class VideoPlayerViewModel {
|
final class VideoPlayerViewModel {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo")
|
||||||
// input
|
// input
|
||||||
let previewImageURL: URL?
|
let previewImageURL: URL?
|
||||||
let videoURL: URL
|
let videoURL: URL
|
||||||
|
@ -63,7 +64,7 @@ final class VideoPlayerViewModel {
|
||||||
.sink { [weak self] timeControlStatus in
|
.sink { [weak self] timeControlStatus in
|
||||||
guard let _ = self else { return }
|
guard let _ = self else { return }
|
||||||
guard timeControlStatus == .playing else { return }
|
guard timeControlStatus == .playing else { return }
|
||||||
AudioPlayer.shared.pauseIfNeed()
|
NotificationCenter.default.post(name: VideoPlayerViewModel.appWillPlayVideoNotification, object: nil)
|
||||||
switch videoKind {
|
switch videoKind {
|
||||||
case .gif:
|
case .gif:
|
||||||
break
|
break
|
||||||
|
|
|
@ -78,7 +78,8 @@ extension APIService {
|
||||||
}()
|
}()
|
||||||
let _oldToot: Toot? = {
|
let _oldToot: Toot? = {
|
||||||
let request = Toot.sortedFetchRequest
|
let request = Toot.sortedFetchRequest
|
||||||
request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: entity.id)
|
request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: statusID)
|
||||||
|
request.fetchLimit = 1
|
||||||
request.returnsObjectsAsFaults = false
|
request.returnsObjectsAsFaults = false
|
||||||
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
||||||
do {
|
do {
|
||||||
|
@ -95,6 +96,9 @@ extension APIService {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
||||||
|
if favoriteKind == .destroy {
|
||||||
|
oldToot.update(favouritesCount: NSNumber(value: max(0, oldToot.favouritesCount.intValue - 1)))
|
||||||
|
}
|
||||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
|
@ -112,7 +116,8 @@ extension APIService {
|
||||||
.handleEvents(receiveCompletion: { completion in
|
.handleEvents(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print(error)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: error:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
debugPrint(error)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
//
|
||||||
|
// APIService+Reblog.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
// make local state change only
|
||||||
|
func reblog(
|
||||||
|
tootObjectID: NSManagedObjectID,
|
||||||
|
mastodonUserObjectID: NSManagedObjectID,
|
||||||
|
reblogKind: Mastodon.API.Reblog.ReblogKind
|
||||||
|
) -> AnyPublisher<Toot.ID, Error> {
|
||||||
|
var _targetTootID: Toot.ID?
|
||||||
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
let toot = managedObjectContext.object(with: tootObjectID) as! Toot
|
||||||
|
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||||
|
let targetToot = toot.reblog ?? toot
|
||||||
|
let targetTootID = targetToot.id
|
||||||
|
_targetTootID = targetTootID
|
||||||
|
|
||||||
|
switch reblogKind {
|
||||||
|
case .reblog:
|
||||||
|
targetToot.update(reblogged: true, mastodonUser: mastodonUser)
|
||||||
|
case .undoReblog:
|
||||||
|
targetToot.update(reblogged: false, mastodonUser: mastodonUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.tryMap { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
guard let targetTootID = _targetTootID else {
|
||||||
|
throw APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
return targetTootID
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send reblog request to remote
|
||||||
|
func reblog(
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
reblogKind: Mastodon.API.Reblog.ReblogKind,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let domain = mastodonAuthenticationBox.domain
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||||
|
return Mastodon.API.Reblog.reblog(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
statusID: statusID,
|
||||||
|
reblogKind: reblogKind,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.map { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
let entity = response.value
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
guard let requestMastodonUser: MastodonUser = {
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
return managedObjectContext.safeFetch(request).first
|
||||||
|
}() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let oldToot: Toot = {
|
||||||
|
let request = Toot.sortedFetchRequest
|
||||||
|
request.predicate = Toot.predicate(domain: domain, id: statusID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
||||||
|
return managedObjectContext.safeFetch(request).first
|
||||||
|
}() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
||||||
|
switch reblogKind {
|
||||||
|
case .undoReblog:
|
||||||
|
oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1)))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "<nil>", entity.reblogsCount )
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.handleEvents(receiveCompletion: { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: error:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
debugPrint(error)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,8 +10,12 @@ import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
final class AudioPlayer: NSObject {
|
final class AudioPlaybackService: NSObject {
|
||||||
|
|
||||||
|
static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.audio-playback-service.appWillPlayAudio")
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
var player = AVPlayer()
|
var player = AVPlayer()
|
||||||
|
@ -21,19 +25,16 @@ final class AudioPlayer: NSObject {
|
||||||
|
|
||||||
let session = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
let playbackState = CurrentValueSubject<PlaybackState, Never>(PlaybackState.unknown)
|
let playbackState = CurrentValueSubject<PlaybackState, Never>(PlaybackState.unknown)
|
||||||
|
|
||||||
// MARK: - singleton
|
|
||||||
public static let shared = AudioPlayer()
|
|
||||||
|
|
||||||
let currentTimeSubject = CurrentValueSubject<TimeInterval, Never>(0)
|
let currentTimeSubject = CurrentValueSubject<TimeInterval, Never>(0)
|
||||||
|
|
||||||
private override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
addObserver()
|
addObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AudioPlayer {
|
extension AudioPlaybackService {
|
||||||
func playAudio(audioAttachment: Attachment) {
|
func playAudio(audioAttachment: Attachment) {
|
||||||
guard let url = URL(string: audioAttachment.url) else {
|
guard let url = URL(string: audioAttachment.url) else {
|
||||||
return
|
return
|
||||||
|
@ -45,6 +46,7 @@ extension AudioPlayer {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyWillPlayAudioNotification()
|
||||||
if audioAttachment == attachment {
|
if audioAttachment == attachment {
|
||||||
if self.playbackState.value == .stopped {
|
if self.playbackState.value == .stopped {
|
||||||
self.seekToTime(time: .zero)
|
self.seekToTime(time: .zero)
|
||||||
|
@ -83,6 +85,12 @@ extension AudioPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
NotificationCenter.default.publisher(for: VideoPlayerViewModel.appWillPlayVideoNotification)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.pauseIfNeed()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] time in
|
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] time in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -119,10 +127,14 @@ extension AudioPlayer {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyWillPlayAudioNotification() {
|
||||||
|
NotificationCenter.default.post(name: AudioPlaybackService.appWillPlayAudioNotification, object: nil)
|
||||||
|
}
|
||||||
func isPlaying() -> Bool {
|
func isPlaying() -> Bool {
|
||||||
return self.playbackState.value == .readyToPlay || self.playbackState.value == .playing
|
return playbackState.value == .readyToPlay || playbackState.value == .playing
|
||||||
}
|
}
|
||||||
func resume() {
|
func resume() {
|
||||||
|
notifyWillPlayAudioNotification()
|
||||||
player.play()
|
player.play()
|
||||||
playbackState.value = .playing
|
playbackState.value = .playing
|
||||||
}
|
}
|
||||||
|
@ -140,3 +152,10 @@ extension AudioPlayer {
|
||||||
player.seek(to: CMTimeMake(value:Int64(time), timescale: 1))
|
player.seek(to: CMTimeMake(value:Int64(time), timescale: 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AudioPlaybackService {
|
||||||
|
func viewDidDisappear(from viewController: UIViewController?) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,6 +90,13 @@ extension VideoPlaybackService {
|
||||||
self.playerViewModel(viewModel, didUpdateTimeControlStatus: timeControlStatus)
|
self.playerViewModel(viewModel, didUpdateTimeControlStatus: timeControlStatus)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: AudioPlaybackService.appWillPlayAudioNotification)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.pauseWhenPlayAudio()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class AppContext: ObservableObject {
|
||||||
let apiService: APIService
|
let apiService: APIService
|
||||||
let authenticationService: AuthenticationService
|
let authenticationService: AuthenticationService
|
||||||
let emojiService: EmojiService
|
let emojiService: EmojiService
|
||||||
|
let audioPlaybackService = AudioPlaybackService()
|
||||||
let videoPlaybackService = VideoPlaybackService()
|
let videoPlaybackService = VideoPlaybackService()
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
|
|
|
@ -114,14 +114,14 @@ extension Mastodon.API.Favorites {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Mastodon.API.Favorites {
|
extension Mastodon.API.Favorites {
|
||||||
|
|
||||||
enum FavoriteKind {
|
public enum FavoriteKind {
|
||||||
case create
|
case create
|
||||||
case destroy
|
case destroy
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ListQuery: GetQuery,TimelineQueryType {
|
public struct ListQuery: GetQuery,TimelineQueryType {
|
||||||
|
|
||||||
public var limit: Int?
|
public var limit: Int?
|
||||||
public var minID: String?
|
public var minID: String?
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Status+Reblog.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.Reblog {
|
||||||
|
|
||||||
|
static func rebloggedByEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||||
|
let pathComponent = "statuses/" + statusID + "/reblogged_by"
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boosted by
|
||||||
|
///
|
||||||
|
/// View who boosted a given status.
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/15
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - statusID: id for status
|
||||||
|
/// - authorization: User token. Could be nil if status is public
|
||||||
|
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||||
|
public static func rebloggedBy(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let request = Mastodon.API.get(
|
||||||
|
url: rebloggedByEndpointURL(domain: domain, statusID: statusID),
|
||||||
|
query: nil,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Reblog {
|
||||||
|
|
||||||
|
static func reblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||||
|
let pathComponent = "statuses/" + statusID + "/reblog"
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boost
|
||||||
|
///
|
||||||
|
/// Reshare a status.
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/15
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - statusID: id for status
|
||||||
|
/// - authorization: User token.
|
||||||
|
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||||
|
public static func reblog(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
query: ReblogQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let request = Mastodon.API.post(
|
||||||
|
url: reblogEndpointURL(domain: domain, statusID: statusID),
|
||||||
|
query: nil,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias Visibility = Mastodon.Entity.Source.Privacy
|
||||||
|
|
||||||
|
public struct ReblogQuery: Codable, PostQuery {
|
||||||
|
public let visibility: Visibility?
|
||||||
|
|
||||||
|
public init(visibility: Visibility?) {
|
||||||
|
self.visibility = visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Reblog {
|
||||||
|
|
||||||
|
static func unreblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||||
|
let pathComponent = "statuses/" + statusID + "/unreblog"
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undo reblog
|
||||||
|
///
|
||||||
|
/// Undo a reshare of a status.
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/9
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - statusID: id for status
|
||||||
|
/// - authorization: User token.
|
||||||
|
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||||
|
public static func undoReblog(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let request = Mastodon.API.post(
|
||||||
|
url: unreblogEndpointURL(domain: domain, statusID: statusID),
|
||||||
|
query: nil,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Reblog {
|
||||||
|
|
||||||
|
public enum ReblogKind {
|
||||||
|
case reblog(query: ReblogQuery)
|
||||||
|
case undoReblog
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func reblog(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
reblogKind: ReblogKind,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
switch reblogKind {
|
||||||
|
case .reblog(let query):
|
||||||
|
return reblog(session: session, domain: domain, statusID: statusID, query: query, authorization: authorization)
|
||||||
|
case .undoReblog:
|
||||||
|
return undoReblog(session: session, domain: domain, statusID: statusID, authorization: authorization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -98,6 +98,7 @@ extension Mastodon.API {
|
||||||
public enum Onboarding { }
|
public enum Onboarding { }
|
||||||
public enum Polls { }
|
public enum Polls { }
|
||||||
public enum Statuses { }
|
public enum Statuses { }
|
||||||
|
public enum Reblog { }
|
||||||
public enum Timeline { }
|
public enum Timeline { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue