From 8e010c7fa5359eecb5f60fecaf98fcb064f6a4e5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 7 May 2023 14:59:56 -0400 Subject: [PATCH] Remove unused notifications and status table view code --- Tusker.xcodeproj/project.pbxproj | 72 --- .../UIViewController+Delegates.swift | 2 +- .../Explore/TrendingLinkTableViewCell.swift | 177 ------ .../NotificationsTableViewController.swift | 376 ------------- ...fableTimelineLikeTableViewController.swift | 370 ------------- ...ActionNotificationGroupTableViewCell.swift | 297 ---------- .../ActionNotificationGroupTableViewCell.xib | 93 ---- ...FollowNotificationGroupTableViewCell.swift | 259 --------- .../FollowNotificationGroupTableViewCell.xib | 84 --- ...llowRequestNotificationTableViewCell.swift | 247 --------- ...FollowRequestNotificationTableViewCell.xib | 118 ---- .../PollFinishedTableViewCell.swift | 144 ----- .../PollFinishedTableViewCell.xib | 96 ---- ...atusUpdatedNotificationTableViewCell.swift | 133 ----- ...StatusUpdatedNotificationTableViewCell.xib | 88 --- .../Status/BaseStatusTableViewCell.swift | 512 ------------------ .../Status/TimelineStatusTableViewCell.swift | 457 ---------------- .../Status/TimelineStatusTableViewCell.xib | 302 ----------- 18 files changed, 1 insertion(+), 3826 deletions(-) delete mode 100644 Tusker/Screens/Explore/TrendingLinkTableViewCell.swift delete mode 100644 Tusker/Screens/Notifications/NotificationsTableViewController.swift delete mode 100644 Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift delete mode 100644 Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift delete mode 100644 Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib delete mode 100644 Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift delete mode 100644 Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.xib delete mode 100644 Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift delete mode 100644 Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.xib delete mode 100644 Tusker/Views/Notifications/PollFinishedTableViewCell.swift delete mode 100644 Tusker/Views/Notifications/PollFinishedTableViewCell.xib delete mode 100644 Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.swift delete mode 100644 Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.xib delete mode 100644 Tusker/Views/Status/BaseStatusTableViewCell.swift delete mode 100644 Tusker/Views/Status/TimelineStatusTableViewCell.swift delete mode 100644 Tusker/Views/Status/TimelineStatusTableViewCell.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 2a8f2a18..744e9cbb 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -32,14 +32,12 @@ D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; - D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; }; D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; }; D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; }; - D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; }; D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; }; D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; }; D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; }; @@ -55,8 +53,6 @@ D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84C28F500D200B82C6E /* ProfileViewController.swift */; }; D61F75882932DB6000C0B37F /* StatusSwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75872932DB6000C0B37F /* StatusSwipeActions.swift */; }; D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */; }; - D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */; }; - D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; }; D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; }; D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; }; D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; }; @@ -130,16 +126,12 @@ D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; - D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; }; - D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; }; D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; }; - D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; }; D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; - D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */; }; D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; productRef = D6552366289870790048A653 /* ScreenCorners */; }; D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; }; D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; }; @@ -157,12 +149,9 @@ D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; - D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */; }; - D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */; }; D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; }; - D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; }; D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; @@ -222,10 +211,6 @@ D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; }; D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; }; D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; }; - D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; }; - D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */; }; - D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7E2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift */; }; - D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */; }; D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; }; D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; }; D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4531529EF64BA00032932 /* ShareViewController.swift */; }; @@ -279,7 +264,6 @@ D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; }; D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; }; D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; }; - D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; }; D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; }; @@ -435,14 +419,12 @@ D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestedProfileCardCollectionViewCell.xib; sourceTree = ""; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = ""; }; D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = ""; }; - D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = ""; }; D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = ""; }; D60E2F252442372B005F8713 /* AccountMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMO.swift; sourceTree = ""; }; D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazilyDecoding.swift; sourceTree = ""; }; D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = ""; }; D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = ""; }; D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = ""; }; - D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = ""; }; D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = ""; }; D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = ""; }; D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = ""; }; @@ -457,8 +439,6 @@ D61DC84C28F500D200B82C6E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; D61F75872932DB6000C0B37F /* StatusSwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSwipeActions.swift; sourceTree = ""; }; D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsPrefsView.swift; sourceTree = ""; }; - D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationTableViewCell.swift; sourceTree = ""; }; - D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; sourceTree = ""; }; D61F758F29353B4300C0B37F /* FileManager+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = ""; }; D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = ""; }; @@ -531,16 +511,12 @@ D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; - D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = ""; }; - D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = ""; }; D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = ""; }; - D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; }; D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = ""; }; D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = ""; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = ""; }; - D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = ""; }; D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = ""; }; D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = ""; }; D65B4B552971F98300DABDFB /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; @@ -560,11 +536,8 @@ D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = ""; }; - D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedTableViewCell.swift; sourceTree = ""; }; - D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollFinishedTableViewCell.xib; sourceTree = ""; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = ""; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = ""; }; - D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = ""; }; D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = ""; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = ""; }; @@ -626,10 +599,6 @@ D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = ""; }; D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = ""; }; D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = ""; }; - D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = ""; }; - D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActionNotificationGroupTableViewCell.xib; sourceTree = ""; }; - D6A3BC7E2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowNotificationGroupTableViewCell.swift; sourceTree = ""; }; - D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowNotificationGroupTableViewCell.xib; sourceTree = ""; }; D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = ""; }; D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = ""; }; D6A4531329EF64BA00032932 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -682,7 +651,6 @@ D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerComponents; path = Packages/TuskerComponents; sourceTree = ""; }; D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = ""; }; D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = ""; }; - D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = ""; }; D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = ""; }; D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; @@ -907,7 +875,6 @@ D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, - D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */, D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */, D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */, D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, @@ -1080,7 +1047,6 @@ isa = PBXGroup; children = ( D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */, - D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */, D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */, D646DCD32A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift */, D646DCD52A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift */, @@ -1137,9 +1103,6 @@ D641C78A213DD926004B4513 /* Status */ = { isa = PBXGroup; children = ( - D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */, - D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */, - D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */, D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */, D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */, D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */, @@ -1166,23 +1129,6 @@ path = "Profile Header"; sourceTree = ""; }; - D641C78C213DD937004B4513 /* Notifications */ = { - isa = PBXGroup; - children = ( - D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */, - D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */, - D6A3BC7E2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift */, - D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */, - D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */, - D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */, - D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */, - D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */, - D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */, - D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */, - ); - path = Notifications; - sourceTree = ""; - }; D646C954213B364600269FB5 /* Transitions */ = { isa = PBXGroup; children = ( @@ -1422,7 +1368,6 @@ D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */, D611C2CC232DC5FC00C86A49 /* Hashtag Cell */, D61AC1DA232EA43100C54D2D /* Instance Cell */, - D641C78C213DD937004B4513 /* Notifications */, D623A53B2635F4E20095BD04 /* Poll */, D641C78B213DD92F004B4513 /* Profile Header */, D641C78A213DD926004B4513 /* Status */, @@ -1436,7 +1381,6 @@ children = ( D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */, D6895DC128D65274006341DA /* CustomAlertController.swift */, - D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */, D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */, D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */, D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */, @@ -1859,7 +1803,6 @@ D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */, - D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */, D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */, D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */, @@ -1868,12 +1811,7 @@ D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */, - D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, - D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, - D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */, - D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */, - D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */, D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1968,7 +1906,6 @@ D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */, - D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */, D60E2F292442372B005F8713 /* AccountMO.swift in Sources */, D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */, @@ -1986,7 +1923,6 @@ D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, - D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, @@ -2021,7 +1957,6 @@ D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */, D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */, D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */, - D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */, D6D94955298963A900C59229 /* Colors.swift in Sources */, @@ -2030,7 +1965,6 @@ D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */, - D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */, D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */, D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */, D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */, @@ -2115,7 +2049,6 @@ D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */, D61F75BD293D099600C0B37F /* Lazy.swift in Sources */, - D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */, D659F36229541065002D944A /* TTTView.swift in Sources */, D65B4B6229771A3F00DABDFB /* FetchStatusService.swift in Sources */, @@ -2135,7 +2068,6 @@ D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */, - D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */, D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */, D620483423D3801D008A63EF /* LinkTextView.swift in Sources */, D61F75882932DB6000C0B37F /* StatusSwipeActions.swift in Sources */, @@ -2158,12 +2090,10 @@ D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */, - D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */, D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */, D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */, D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */, D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */, - D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */, D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, @@ -2184,7 +2114,6 @@ D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */, D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */, - D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D68A76F129539116001DA1B3 /* FlipView.swift in Sources */, @@ -2197,7 +2126,6 @@ D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */, D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, - D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */, D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */, D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */, diff --git a/Tusker/Extensions/UIViewController+Delegates.swift b/Tusker/Extensions/UIViewController+Delegates.swift index f3c61a4c..0bfa53e5 100644 --- a/Tusker/Extensions/UIViewController+Delegates.swift +++ b/Tusker/Extensions/UIViewController+Delegates.swift @@ -1,5 +1,5 @@ // -// UIViewController+StatusTableViewCellDelegate.swift +// UIViewController+Delegate.swift // Tusker // // Created by Shadowfacts on 8/27/18. diff --git a/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift b/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift deleted file mode 100644 index f51d3730..00000000 --- a/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// TrendingLinkTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 4/2/22. -// Copyright © 2022 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import WebURLFoundationExtras - -class TrendingLinkTableViewCell: UITableViewCell { - - private var card: Card? - private var isGrayscale = false - private var thumbnailRequest: ImageCache.Request? - - private let thumbnailView = UIImageView() - private let titleLabel = UILabel() - private let providerLabel = UILabel() - private let activityLabel = UILabel() - private let historyView = TrendHistoryView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - thumbnailView.contentMode = .scaleAspectFill - thumbnailView.clipsToBounds = true - - titleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline).withSymbolicTraits(.traitBold)!, size: 0) - titleLabel.numberOfLines = 2 - - providerLabel.font = .preferredFont(forTextStyle: .subheadline) - - activityLabel.font = .preferredFont(forTextStyle: .caption1) - - let vStack = UIStackView(arrangedSubviews: [ - titleLabel, - providerLabel, - activityLabel, - ]) - vStack.axis = .vertical - vStack.spacing = 4 - - let hStack = UIStackView(arrangedSubviews: [ - thumbnailView, - vStack, - historyView, - ]) - hStack.axis = .horizontal - hStack.spacing = 4 - hStack.alignment = .center - hStack.translatesAutoresizingMaskIntoConstraints = false - addSubview(hStack) - - NSLayoutConstraint.activate([ - thumbnailView.heightAnchor.constraint(equalToConstant: 75), - thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor), - - historyView.widthAnchor.constraint(equalToConstant: 75), - historyView.heightAnchor.constraint(equalToConstant: 44), - - hStack.leadingAnchor.constraint(equalToSystemSpacingAfter: safeAreaLayoutGuide.leadingAnchor, multiplier: 1), - safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: hStack.trailingAnchor, multiplier: 1), - hStack.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 1), - bottomAnchor.constraint(equalToSystemSpacingBelow: hStack.bottomAnchor, multiplier: 1), - ]) - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListGroupedCell(for: state) - } - - func updateUI(card: Card) { - self.card = card - self.thumbnailView.image = nil - - updateGrayscaleableUI(card: card) - updateUIForPreferences() - - let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) - titleLabel.text = title - titleLabel.isHidden = title.isEmpty - - let provider = card.providerName?.trimmingCharacters(in: .whitespacesAndNewlines) - providerLabel.text = provider - providerLabel.isHidden = provider?.isEmpty ?? true - - if let history = card.history { - let sorted = history.sorted(by: { $0.day < $1.day }) - let lastTwo = sorted[(sorted.count - 2)...] - let accounts = lastTwo.map(\.accounts).reduce(0, +) - let uses = lastTwo.map(\.uses).reduce(0, +) - - let format = NSLocalizedString("trending hashtag info", comment: "trending hashtag posts and people") - activityLabel.text = String.localizedStringWithFormat(format, accounts, uses) - activityLabel.isHidden = false - } else { - activityLabel.isHidden = true - } - - historyView.setHistory(card.history) - historyView.isHidden = card.history == nil || card.history!.count < 2 - } - - @objc private func updateUIForPreferences() { - if isGrayscale != Preferences.shared.grayscaleImages, - let card { - updateGrayscaleableUI(card: card) - } - } - - private func updateGrayscaleableUI(card: Card) { - isGrayscale = Preferences.shared.grayscaleImages - - if let imageURL = card.image, - let url = URL(imageURL) { - thumbnailRequest = ImageCache.attachments.get(url, completion: { _, image in - guard let image = image, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else { - return - } - DispatchQueue.main.async { - self.thumbnailView.image = transformedImage - } - }) - if thumbnailRequest != nil { - loadBlurHash(card: card) - } - } - } - - private func loadBlurHash(card: Card) { - guard let hash = card.blurhash else { - return - } - AttachmentView.queue.async { [weak self] in - let size: CGSize - if let width = card.width, let height = card.height { - let aspectRatio = CGFloat(width) / CGFloat(height) - if aspectRatio > 1 { - size = CGSize(width: 32, height: 32 / aspectRatio) - } else { - size = CGSize(width: 32 * aspectRatio, height: 32) - } - } else { - size = CGSize(width: 32, height: 32) - } - - guard let preview = UIImage(blurHash: hash, size: size) else { - return - } - DispatchQueue.main.async { [weak self] in - guard let self = self, - self.card?.url == card.url, - self.thumbnailView.image == nil else { - return - } - self.thumbnailView.image = preview - } - } - } - -} diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift deleted file mode 100644 index e8e0261e..00000000 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ /dev/null @@ -1,376 +0,0 @@ -// -// NotificationsTableViewController.swift -// Tusker -// -// Created by Shadowfacts on 9/2/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import Sentry - -class NotificationsTableViewController: DiffableTimelineLikeTableViewController { - - private let statusCell = "statusCell" - private let actionGroupCell = "actionGroupCell" - private let followGroupCell = "followGroupCell" - private let followRequestCell = "followRequestCell" - private let pollCell = "pollCell" - private let updatedCell = "updatedCell" - private let unknownCell = "unknownCell" - - weak var mastodonController: MastodonController! - - private let allowedTypes: [Pachyderm.Notification.Kind] - private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow] - - private var newer: RequestRange? - private var older: RequestRange? - - init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) { - self.allowedTypes = allowedTypes - self.mastodonController = mastodonController - - super.init() - - dragEnabled = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override class func refreshCommandTitle() -> String { - return NSLocalizedString("Refresh Notifications", comment: "refresh notifications command discoverability title") - } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) - tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell) - tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell) - tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell) - tableView.register(UINib(nibName: "PollFinishedTableViewCell", bundle: .main), forCellReuseIdentifier: pollCell) - tableView.register(UINib(nibName: "StatusUpdatedNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: updatedCell) - tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell) - - tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac - tableView.allowsFocus = true - tableView.backgroundColor = .appBackground - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) - } - - @objc private func handleStatusDeleted(_ notification: Foundation.Notification) { - guard let userInfo = notification.userInfo, - let accountID = mastodonController.accountInfo?.id, - userInfo["accountID"] as? String == accountID, - let statusIDs = userInfo["statusIDs"] as? [String] else { - return - } - var snapshot = self.dataSource.snapshot() - // this is not efficient, since the number of notifications is almost certainly greater than the number of deleted statuses - // but we can't just check if the status is in the data source, since we don't have the corresponding notification/group - let toDelete = snapshot.itemIdentifiers - .filter { item in - guard case .notificationGroup(let group) = item else { - return false - } - return group.kind == .mention && statusIDs.contains(group.notifications.first!.status!.id) - } - if !toDelete.isEmpty { - snapshot.deleteItems(toDelete) - self.dataSource.apply(snapshot, animatingDifferences: true) - } - } - - private func request(range: RequestRange) -> Request<[Pachyderm.Notification]> { - if mastodonController.instanceFeatures.notificationsAllowedTypes { - return Client.getNotifications(allowedTypes: allowedTypes, range: range) - } else { - var types = Set(Notification.Kind.allCases) - allowedTypes.forEach { types.remove($0) } - return Client.getNotifications(excludedTypes: Array(types), range: range) - } - } - - // MARK: - DiffableTimelineLikeTableViewController - - override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { - if case .loadingIndicator = item { - return self.loadingIndicatorCell(indexPath: indexPath) - } - let group = item.group! - - switch group.kind { - case .mention, .status: - guard let notification = group.notifications.first, - let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { - fatalError() - } - cell.delegate = self - guard let status = notification.status else { - let crumb = Breadcrumb(level: .fatal, category: "notifications") - crumb.data = [ - "id": notification.id, - "type": notification.kind.rawValue, - "created_at": notification.createdAt.formatted(.iso8601), - "account": notification.account.id, - ] - SentrySDK.addBreadcrumb(crumb) - fatalError("missing status for \(group.kind) notification") - } - cell.updateUI(statusID: status.id, state: group.statusState!) - return cell - - case .favourite, .reblog: - guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() } - cell.delegate = self - cell.updateUI(group: group) - return cell - - case .follow: - guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() } - cell.delegate = self - cell.updateUI(group: group) - return cell - - case .followRequest: - guard let notification = group.notifications.first, - let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() } - cell.delegate = self - cell.updateUI(notification: notification) - return cell - - case .poll: - guard let notification = group.notifications.first, - let cell = tableView.dequeueReusableCell(withIdentifier: pollCell, for: indexPath) as? PollFinishedTableViewCell else { fatalError() } - cell.delegate = self - cell.updateUI(notification: notification) - return cell - - case .update: - guard let notification = group.notifications.first, - let cell = tableView.dequeueReusableCell(withIdentifier: updatedCell, for: indexPath) as? StatusUpdatedNotificationTableViewCell else { fatalError() } - cell.delegate = self - cell.updateUI(notification: notification) - return cell - - case .unknown: - let cell = tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath) - cell.textLabel!.text = NSLocalizedString("Unknown Notification", comment: "unknown notification fallback cell text") - return cell - } - } - - private func validateNotifications(_ notifications: [Pachyderm.Notification]) -> [Pachyderm.Notification] { - return notifications.compactMap { notif in - if notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite) { - let crumb = Breadcrumb(level: .fatal, category: "notifications") - crumb.data = [ - "id": notif.id, - "type": notif.kind.rawValue, - "created_at": notif.createdAt.formatted(.iso8601), - "account": notif.account.id, - ] - SentrySDK.addBreadcrumb(crumb) - return nil - } else { - return notif - } - } - } - - override func loadInitialItems(completion: @escaping (LoadResult) -> Void) { - mastodonController.run(request(range: .default)) { (response) in - switch response { - case let .failure(error): - completion(.failure(.client(error))) - - case let .success(notifications, _): - let notifications = self.validateNotifications(notifications) - let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes) - - if !notifications.isEmpty { - self.newer = .after(id: notifications.first!.id, count: nil) - self.older = .before(id: notifications.last!.id, count: nil) - } - - self.mastodonController.persistentContainer.addAll(notifications: notifications) { - var snapshot = Snapshot() - snapshot.appendSections([.notifications]) - snapshot.appendItems(groups.map { .notificationGroup($0) }, toSection: .notifications) - completion(.success(snapshot)) - } - } - } - } - - override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { - guard let older = older else { - completion(.failure(.noOlder)) - return - } - - mastodonController.run(request(range: older)) { (response) in - switch response { - case let .failure(error): - completion(.failure(.client(error))) - - case let .success(newNotifications, _): - let newNotifications = self.validateNotifications(newNotifications) - if !newNotifications.isEmpty { - self.older = .before(id: newNotifications.last!.id, count: nil) - } - - let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) - - self.mastodonController.persistentContainer.addAll(notifications: newNotifications) { - let existingGroups = currentSnapshot().itemIdentifiers.compactMap(\.group) - let merged = NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes) - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.notifications]) - snapshot.appendItems(merged.map { .notificationGroup($0) }, toSection: .notifications) - completion(.success(snapshot)) - } - } - } - } - - override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { - guard let newer = newer else { - completion(.failure(.noNewer)) - return - } - - mastodonController.run(request(range: newer)) { (response) in - switch response { - case let .failure(error): - completion(.failure(.client(error))) - - case let .success(newNotifications, _): - let newNotifications = self.validateNotifications(newNotifications) - guard !newNotifications.isEmpty else { - completion(.failure(.allCaughtUp)) - return - } - - self.newer = .after(id: newNotifications.first!.id, count: nil) - - let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) - - self.mastodonController.persistentContainer.addAll(notifications: newNotifications) { - let existingGroups = currentSnapshot().itemIdentifiers.compactMap(\.group) - let merged = NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes) - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.notifications]) - snapshot.appendItems(merged.map { .notificationGroup($0) }, toSection: .notifications) - completion(.success(snapshot)) - } - } - } - } - - private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) { - guard let item = dataSource.itemIdentifier(for: indexPath), - let notifications = item.group?.notifications else { - return - } - let group = DispatchGroup() - notifications - .map { Pachyderm.Notification.dismiss(id: $0.id) } - .forEach { (request) in - group.enter() - mastodonController.run(request) { (_) in - group.leave() - } - } - group.notify(queue: .main) { - var snapshot = self.dataSource.snapshot() - snapshot.deleteItems([item]) - self.dataSource.apply(snapshot, completion: completion) - } - } - - // MARK: - UITableViewDelegate - - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let dismissAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Dismiss", comment: "dismiss notification swipe action title")) { (action, view, completion) in - self.dismissNotificationsInGroup(at: indexPath) { - completion(true) - } - } - dismissAction.accessibilityLabel = "Dismiss Notification" - dismissAction.image = UIImage(systemName: "clear.fill") - - let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() - - let config: UISwipeActionsConfiguration - if let cellConfiguration = cellConfiguration { - config = UISwipeActionsConfiguration(actions: cellConfiguration.actions + [dismissAction]) - config.performsFirstActionWithFullSwipe = cellConfiguration.performsFirstActionWithFullSwipe - } else { - config = UISwipeActionsConfiguration(actions: [dismissAction]) - config.performsFirstActionWithFullSwipe = false - } - return config - } - - override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] { - return [ - UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), handler: { (_) in - self.dismissNotificationsInGroup(at: indexPath) - }) - ] - } -} - -extension NotificationsTableViewController { - enum Section: DiffableTimelineLikeSection { - case loadingIndicator - case notifications - } - enum Item: DiffableTimelineLikeItem { - case loadingIndicator - case notificationGroup(NotificationGroup) - - var group: NotificationGroup? { - switch self { - case .loadingIndicator: - return nil - case .notificationGroup(let group): - return group - } - } - } -} - -extension NotificationsTableViewController: TuskerNavigationDelegate { - var apiController: MastodonController! { mastodonController } -} - -extension NotificationsTableViewController: MenuActionProvider { -} - -extension NotificationsTableViewController: StatusTableViewCellDelegate { - func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { - if #available(iOS 16.0, *) { - } else { - cellHeightChanged() - } - } -} - -extension NotificationsTableViewController: UITableViewDataSourcePrefetching { - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let group = dataSource.itemIdentifier(for: indexPath)?.group else { continue } - for notification in group.notifications { - guard let avatar = notification.account.avatar else { continue } - ImageCache.avatars.fetchIfNotCached(avatar) - } - } - } -} diff --git a/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift b/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift deleted file mode 100644 index 36706b8c..00000000 --- a/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift +++ /dev/null @@ -1,370 +0,0 @@ -// -// DiffableTimelineLikeTableViewController.swift -// Tusker -// -// Created by Shadowfacts on 6/18/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -protocol DiffableTimelineLikeSection: Hashable, CaseIterable { - static var loadingIndicator: Self { get } -} -protocol DiffableTimelineLikeItem: Hashable { - static var loadingIndicator: Self { get } -} - -class DiffableTimelineLikeTableViewController: EnhancedTableViewController, RefreshableViewController { - - typealias Snapshot = NSDiffableDataSourceSnapshot - typealias LoadResult = Result - - private let pageSize = 20 - - private(set) var state = State.unloaded - private var lastLastVisibleRow: IndexPath? - private var currentLoadingIndicatorWorkItem: DispatchWorkItem? - - private(set) var dataSource: UITableViewDiffableDataSource! - - init() { - super.init(style: .plain) - - addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle())) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - dataSource = UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView, indexPath, item) in - self.cellProvider(tableView, indexPath, item) - } - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 140 - tableView.register(LoadingTableViewCell.self, forCellReuseIdentifier: "loadingCell") - - #if !targetEnvironment(macCatalyst) - self.refreshControl = UIRefreshControl() - self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) - #endif - - if let prefetchSource = self as? UITableViewDataSourcePrefetching { - tableView.prefetchDataSource = prefetchSource - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - loadInitial() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - pruneOffscreenRows() - currentToast?.dismissToast(animated: false) - } - - class func refreshCommandTitle() -> String { - return "Refresh" - } - - private func pruneOffscreenRows() { - guard let lastVisibleRow = tableView.indexPathsForVisibleRows?.last else { - return - } - - var snapshot = dataSource.snapshot() - - let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section] - - let contentSections = snapshot.sectionIdentifiers.filter { timelineContentSections().contains($0) } - let contentSectionIndices = contentSections.compactMap(snapshot.indexOfSection(_:)) - guard let maxContentSectionIndex = contentSectionIndices.max() else { - return - } - - if lastVisibleRow.section < maxContentSectionIndex { - return - } else if lastVisibleRow.section == maxContentSectionIndex { - let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection) - - let numberOfPagesToPrune = (items.count - lastVisibleRow.row - 1) / pageSize - if numberOfPagesToPrune > 0 { - let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize)) - snapshot.deleteItems(itemsToRemove) - willRemoveItems(itemsToRemove) - } else { - return - } - } else { - // unreachable - return - } - - dataSource.apply(snapshot, animatingDifferences: false) - } - - private func showLoadingIndicatorDelayed() -> DispatchWorkItem { - currentLoadingIndicatorWorkItem?.cancel() - let workItem = DispatchWorkItem { [weak self] in - guard let self = self else { return } - var snapshot = self.dataSource.snapshot() - var changed = false - if !snapshot.sectionIdentifiers.contains(.loadingIndicator) { - snapshot.appendSections([.loadingIndicator]) - changed = true - } - if changed || !snapshot.itemIdentifiers(inSection: .loadingIndicator).contains(.loadingIndicator) { - snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator) - changed = true - } - if changed { - self.dataSource.apply(snapshot, animatingDifferences: false) - } - } - currentLoadingIndicatorWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250), execute: workItem) - return workItem - } - - private func loadInitial() { - guard state == .unloaded else { return } - // set loaded immediately so we don't trigger another request while the current one is running - state = .loadingInitial - - let showIndicator = showLoadingIndicatorDelayed() - - loadInitialItems() { result in - DispatchQueue.main.async { - showIndicator.cancel() - - switch result { - case .success(var snapshot): - if snapshot.sectionIdentifiers.contains(.loadingIndicator) { - snapshot.deleteSections([.loadingIndicator]) - } - self.dataSource.apply(snapshot, animatingDifferences: false) - self.state = .loaded - - case let .failure(.client(error)): - self.state = .unloaded - let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast) in - toast.dismissToast(animated: true) - self?.loadInitial() - } - self.showToast(configuration: config, animated: true) - - default: - self.state = .unloaded - } - } - } - } - - func reloadInitial() { - state = .unloaded - loadInitial() - } - - func loadOlder() { - guard state == .loaded else { return } - - state = .loadingOlder - - let showIndicator = showLoadingIndicatorDelayed() - - loadOlderItems(currentSnapshot: dataSource.snapshot) { result in - DispatchQueue.main.async { - self.state = .loaded - showIndicator.cancel() - - switch result { - case .success(var snapshot): - if snapshot.sectionIdentifiers.contains(.loadingIndicator) { - snapshot.deleteSections([.loadingIndicator]) - } - self.dataSource.apply(snapshot, animatingDifferences: false) - - case let .failure(.client(error)): - let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast) in - toast.dismissToast(animated: true) - self?.loadOlder() - } - self.showToast(configuration: config, animated: true) - - default: - break - } - } - } - } - - @available(iOS, deprecated: 16.0) - func cellHeightChanged() { - // causes the table view to recalculate the cell heights - tableView.beginUpdates() - tableView.endUpdates() - } - - // MARK: - UITableViewDelegate - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - // this assumes that indexPathsForVisibleRows is always in order - lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last - - let orderedContentSections = dataSource.snapshot().sectionIdentifiers.enumerated().filter { timelineContentSections().contains($0.element) } - if let lastContentSection = orderedContentSections.last, - indexPath.section == lastContentSection.offset, - indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - - loadOlder() - } - } - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() - } - - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() - } - - // MARK: - RefreshableViewController - - func refresh() { - // if we're unloaded, there's nothing "newer" to load - // if we're performing some other operation, we don't want to step on its toes - guard state == .loaded else { - self.refreshControl?.endRefreshing() - return - } - - state = .loadingNewer - - var firstItem: Item? = nil - let currentSnapshot: () -> Snapshot = { - let snapshot = self.dataSource.snapshot() - - for section in self.timelineContentSections() { - if snapshot.indexOfSection(section) != nil, - let first = snapshot.itemIdentifiers(inSection: section).first { - firstItem = first - break - } - } - - return snapshot - } - - loadNewerItems(currentSnapshot: currentSnapshot) { result in - DispatchQueue.main.async { - self.refreshControl?.endRefreshing() - self.state = .loaded - - switch result { - case let .success(snapshot): - self.dataSource.apply(snapshot, animatingDifferences: false) - if let firstItem = firstItem, - let indexPath = self.dataSource.indexPath(for: firstItem) { - // maintain the current position in the list (don't scroll to top) - self.tableView.scrollToRow(at: indexPath, at: .top, animated: false) - } - - case let .failure(.client(error)): - let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast) in - toast.dismissToast(animated: true) - self?.refresh() - } - self.showToast(configuration: config, animated: true) - - case .failure(.allCaughtUp): - var config = ToastConfiguration(title: "You're all caught up") - config.edge = .top - config.dismissAutomaticallyAfter = 2 - config.action = { (toast) in - toast.dismissToast(animated: true) - } - self.showToast(configuration: config, animated: true) - - default: - break - } - } - } - } - - // MARK: - Subclass Methods - - func loadingIndicatorCell(indexPath: IndexPath) -> UITableViewCell? { - let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingTableViewCell - cell.indicator.startAnimating() - return cell - } - - func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { - fatalError("cellProvider(_:_:_:) must be implemented by subclasses") - } - - func loadInitialItems(completion: @escaping (LoadResult) -> Void) { - fatalError("loadInitialItems(completion:) must be implemented by subclasses") - } - - func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { - fatalError("loadOlderItesm(completion:) must be implemented by subclasses") - } - - func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { - fatalError("loadNewerItems(completion:) must be implemented by subclasses") - } - - func timelineContentSections() -> Section.AllCases { - return Section.allCases - } - - func willRemoveItems(_ items: [Item]) { - } - -} - -extension DiffableTimelineLikeTableViewController { - enum State: Equatable { - case unloaded - case loadingInitial - case loaded - case loadingNewer - case loadingOlder - } -} - -extension DiffableTimelineLikeTableViewController { - enum LoadError: LocalizedError { - case noClient - case noOlder - case noNewer - case allCaughtUp - case client(Client.Error) - } -} - -extension DiffableTimelineLikeTableViewController: BackgroundableViewController { - func sceneDidEnterBackground() { - pruneOffscreenRows() - currentToast?.dismissToast(animated: false) - } -} - -extension DiffableTimelineLikeTableViewController: ToastableViewController { -} diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift deleted file mode 100644 index f998aa8c..00000000 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ /dev/null @@ -1,297 +0,0 @@ -// -// ActionNotificationGroupTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 9/5/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import SwiftSoup -import Sentry - -class ActionNotificationGroupTableViewCell: UITableViewCell { - - weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? - var mastodonController: MastodonController! { delegate?.apiController } - - @IBOutlet weak var actionImageView: UIImageView! - @IBOutlet weak var verticalStackView: UIStackView! - @IBOutlet weak var actionAvatarStackView: UIStackView! - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var actionLabel: MultiSourceEmojiLabel! - @IBOutlet weak var statusContentLabel: UILabel! - - var group: NotificationGroup! - var statusID: String! - - private var updateTimestampWorkItem: DispatchWorkItem? - private var isGrayscale = false - - deinit { - updateTimestampWorkItem?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - actionLabel.combiner = self.updateActionLabel - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - @objc func updateUIForPreferences() { - for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { - imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) - } - - if isGrayscale != Preferences.shared.grayscaleImages { - Task { - await updateGrayscaleableUI() - } - } - } - - func updateUI(group: NotificationGroup) { - guard group.kind == .favourite || group.kind == .reblog else { - fatalError("Invalid notification type \(group.kind) for ActionNotificationGroupTableViewCell") - } - self.group = group - - guard let firstNotification = group.notifications.first else { fatalError() } - guard let status = firstNotification.status else { - let crumb = Breadcrumb(level: .fatal, category: "notifications") - crumb.data = [ - "id": firstNotification.id, - "type": firstNotification.kind.rawValue, - "created_at": firstNotification.createdAt.formatted(.iso8601), - "account": firstNotification.account.id, - ] - SentrySDK.addBreadcrumb(crumb) - fatalError("missing status for favorite/reblog notification") - } - self.statusID = status.id - - updateUIForPreferences() - - switch group.kind { - case .favourite: - actionImageView.image = UIImage(systemName: "star.fill") - case .reblog: - actionImageView.image = UIImage(systemName: "repeat") - default: - fatalError() - } - - isGrayscale = Preferences.shared.grayscaleImages - - updateTimestamp() - let timestampLabelSize = timestampLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: timestampLabel.bounds.height)) - - let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } - - actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - var imageViews = [UIImageView]() - for _ in people { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 - actionAvatarStackView.addArrangedSubview(imageView) - imageViews.append(imageView) - - // don't add more avatars if they would overflow or squeeze the timestamp label - let avatarViewsWidth = 30 * CGFloat(imageViews.count) - let avatarMarginsWidth = 4 * CGFloat(max(0, imageViews.count - 1)) - // todo: when the cell is first created, verticalStackView.bounds.width is not correct - let maxAvatarStackWidth = verticalStackView.bounds.width - timestampLabelSize.width - 8 - let remainingWidth = maxAvatarStackWidth - avatarViewsWidth - avatarMarginsWidth - if remainingWidth < 34 { - break - } - } - NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) }) - - Task { - await updateGrayscaleableUI() - } - - actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id) - - let doc = try! SwiftSoup.parse(status.content) - statusContentLabel.text = try! doc.text() - } - - @MainActor - private func updateGrayscaleableUI() async { - let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } - let groupID = group.id - - for (index, account) in people.enumerated() { - guard actionAvatarStackView.arrangedSubviews.count > index, - let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView, - let avatarURL = account.avatar else { - continue - } - - Task { - let (_, image) = await ImageCache.avatars.get(avatarURL) - guard let image = image, - self.group.id == groupID, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { - return - } - imageView.image = transformedImage - } - } - } - - private func updateTimestamp() { - guard let notification = group.notifications.first else { - fatalError("Missing cached notification") - } - - timestampLabel.text = notification.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch notification.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } else { - updateTimestampWorkItem = nil - } - } - - func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString { - let verb: String - switch group.kind { - case .favourite: - verb = "Favorited" - case .reblog: - verb = "Reblogged" - default: - fatalError() - } - - // todo: figure out how to localize this - let str = NSMutableAttributedString(string: "\(verb) by ") - switch names.count { - case 1: - str.append(names.first!) - case 2: - str.append(names.first!) - str.append(NSAttributedString(string: " and ")) - str.append(names.last!) - default: - for (index, name) in names.enumerated() { - str.append(name) - if index < names.count - 2 { - str.append(NSAttributedString(string: ", ")) - } else if index == names.count - 2 { - str.append(NSAttributedString(string: ", and ")) - } - } - } - return str - } - - override func prepareForReuse() { - super.prepareForReuse() - - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - } - - // MARK: Accessibility - - override var accessibilityLabel: String? { - get { - let first = group.notifications.first! - var str = "" - switch group.kind { - case .favourite: - str += "Favorited by " - case .reblog: - str += "Reblogged by " - default: - return nil - } - str += first.account.displayNameWithoutCustomEmoji - if group.notifications.count > 1 { - str += " and \(group.notifications.count - 1) more" - } - str += ", \(first.createdAt.formatted(.relative(presentation: .numeric))), " - str += statusContentLabel.text ?? "" - return str - } - set {} - } - -} - -extension ActionNotificationGroupTableViewCell: SelectableTableViewCell { - func didSelectCell() { - guard let delegate = delegate else { return } - let notifications = group.notifications - let accountIDs = notifications.map { $0.account.id } - let action: StatusActionAccountListViewController.ActionType - switch notifications.first!.kind { - case .favourite: - action = .favorite - case .reblog: - action = .reblog - default: - fatalError() - } - let vc = StatusActionAccountListViewController(actionType: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController) - vc.showInaccurateCountWarning = false - delegate.show(vc) - } -} - -extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - return (content: { - let notifications = self.group.notifications - let accountIDs = notifications.map { $0.account.id } - let action: StatusActionAccountListViewController.ActionType - switch notifications.first!.kind { - case .favourite: - action = .favorite - case .reblog: - action = .reblog - default: - fatalError() - } - let vc = StatusActionAccountListViewController(actionType: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController) - vc.showInaccurateCountWarning = false - return vc - }, actions: { - return [] - }) - } -} diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib deleted file mode 100644 index 69110007..00000000 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift deleted file mode 100644 index 017a98bf..00000000 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ /dev/null @@ -1,259 +0,0 @@ -// -// FollowNotificationGroupTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 9/5/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class FollowNotificationGroupTableViewCell: UITableViewCell { - - weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? - var mastodonController: MastodonController! { delegate?.apiController } - - @IBOutlet weak var avatarStackView: UIStackView! - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var actionLabel: MultiSourceEmojiLabel! - - var group: NotificationGroup! - - private var avatarRequests = [String: ImageCache.Request]() - private var updateTimestampWorkItem: DispatchWorkItem? - private var isGrayscale = false - - deinit { - updateTimestampWorkItem?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - actionLabel.combiner = self.updateActionLabel - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - @objc func updateUIForPreferences() { - for case let imageView as UIImageView in avatarStackView.arrangedSubviews { - imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) - } - - if isGrayscale != Preferences.shared.grayscaleImages { - updateGrayscaleableUI() - } - } - - func updateUI(group: NotificationGroup) { - self.group = group - - let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } - - actionLabel.setEmojis(pairs: people.map { - ($0.displayOrUserName, $0.emojis) - }, identifier: group.id) - updateTimestamp() - - isGrayscale = Preferences.shared.grayscaleImages - - avatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - for account in people { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 - if let avatarURL = account.avatar { - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in - guard let self = self, - let image = image, - self.group.id == group.id, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { - return - } - - DispatchQueue.main.async { - self.avatarRequests.removeValue(forKey: account.id) - imageView.image = transformedImage - } - } - } - avatarStackView.addArrangedSubview(imageView) - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 30), - imageView.heightAnchor.constraint(equalToConstant: 30), - ]) - } - } - - private func updateGrayscaleableUI() { - isGrayscale = Preferences.shared.grayscaleImages - - let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } - let groupID = group.id - - for (index, account) in people.enumerated() { - guard avatarStackView.arrangedSubviews.count > index, - let imageView = avatarStackView.arrangedSubviews[index] as? UIImageView else { - continue - } - - if let avatarURL = account.avatar { - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in - guard let self = self else { return } - guard let image = image, - self.group.id == groupID, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { - DispatchQueue.main.async { - self.avatarRequests.removeValue(forKey: account.id) - } - return - } - - DispatchQueue.main.async { - self.avatarRequests.removeValue(forKey: account.id) - imageView.image = transformedImage - } - } - } - } - } - - func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString { - // todo: figure out how to localize this - let str = NSMutableAttributedString(string: "Followed by ") - switch names.count { - case 1: - str.append(names.first!) - case 2: - str.append(names.first!) - str.append(NSAttributedString(string: " and ")) - str.append(names.last!) - default: - for (index, name) in names.enumerated() { - str.append(name) - if index < names.count - 2 { - str.append(NSAttributedString(string: ", ")) - } else if index == names.count - 2 { - str.append(NSAttributedString(string: ", and ")) - } - } - } - return str - } - - func updateTimestamp() { - guard let notification = group.notifications.first else { - fatalError("Missing cached notification") - } - - timestampLabel.text = notification.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch notification.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } else { - updateTimestampWorkItem = nil - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - avatarRequests.values.forEach { $0.cancel() } - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - } - - // MARK: Accessibility - - override var accessibilityLabel: String? { - get { - let first = group.notifications.first! - var str = "Followed by " - str += first.account.displayNameWithoutCustomEmoji - if group.notifications.count > 1 { - str += " and \(group.notifications.count - 1) more" - } - str += ", \(first.createdAt.formatted(.relative(presentation: .numeric)))" - return str - } - set {} - } - -} - -extension FollowNotificationGroupTableViewCell: SelectableTableViewCell { - func didSelectCell() { - let accountIDs = group.notifications.map { $0.account.id } - switch accountIDs.count { - case 0: - return - case 1: - delegate?.selected(account: accountIDs.first!) - default: - let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: mastodonController) - vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title") - delegate?.show(vc) - } - } -} - -extension FollowNotificationGroupTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - guard let mastodonController = mastodonController else { return nil } - let accountIDs = self.group.notifications.map { $0.account.id } - return (content: { - if accountIDs.count == 1 { - return ProfileViewController(accountID: accountIDs.first!, mastodonController: mastodonController) - } else { - return AccountListViewController(accountIDs: accountIDs, mastodonController: mastodonController) - } - }, actions: { - if accountIDs.count == 1 { - return self.delegate?.actionsForProfile(accountID: accountIDs.first!, source: .view(self)) ?? [] - } else { - return [] - } - }) - } -} - -extension FollowNotificationGroupTableViewCell: DraggableTableViewCell { - func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { - guard group.notifications.count == 1 else { - return [] - } - let notification = group.notifications[0] - let provider = NSItemProvider(object: notification.account.url as NSURL) - let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id) - activity.displaysAuxiliaryScene = true - provider.registerObject(activity, visibility: .all) - return [UIDragItem(itemProvider: provider)] - } -} diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.xib b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.xib deleted file mode 100644 index 8bb2a224..00000000 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.xib +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift deleted file mode 100644 index c3734a7d..00000000 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// FollowRequestNotificationTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 1/4/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class FollowRequestNotificationTableViewCell: UITableViewCell { - - weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? - var mastodonController: MastodonController! { delegate?.apiController } - - @IBOutlet weak var stackView: UIStackView! - @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var actionLabel: EmojiLabel! - @IBOutlet weak var actionButtonsStackView: UIStackView! - @IBOutlet weak var acceptButton: UIButton! - @IBOutlet weak var rejectButton: UIButton! - - var notification: Pachyderm.Notification? - var account: Account! - - private var avatarRequest: ImageCache.Request? - private var updateTimestampWorkItem: DispatchWorkItem? - private var isGrayscale = false - - deinit { - updateTimestampWorkItem?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - avatarImageView.layer.masksToBounds = true - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - updateUIForPreferences() - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - @objc func updateUIForPreferences() { - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 - - if isGrayscale != Preferences.shared.grayscaleImages, - let account = self.account { - updateUI(account: account) - } - } - - func updateUI(notification: Pachyderm.Notification) { - self.notification = notification - updateUI(account: notification.account) - updateTimestamp() - } - - func updateUI(account: Account) { - // todo: update to use managed objects - self.account = account - if Preferences.shared.hideCustomEmojiInUsernames { - actionLabel.text = "Request to follow from \(account.displayName)" - actionLabel.removeEmojis() - } else { - actionLabel.text = "Request to follow from \(account.displayName)" - actionLabel.setEmojis(account.emojis, identifier: account.id) - } - - if let avatarURL = account.avatar { - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in - guard let self = self else { return } - self.avatarRequest = nil - - guard self.account == account, - let image = image, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { - return - } - - DispatchQueue.main.async { - self.avatarImageView.image = transformedImage - } - } - } - } - - private func updateTimestamp() { - guard let notification = notification else { return } - - timestampLabel.text = notification.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch notification.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } else { - updateTimestampWorkItem = nil - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - avatarRequest?.cancel() - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - } - - private func addLabel(_ text: String) { - let label = UILabel() - label.textAlignment = .center - label.font = .boldSystemFont(ofSize: 17) - label.text = text - self.stackView.addArrangedSubview(label) - } - - // MARK: Accessibility - - override var accessibilityLabel: String? { - get { - guard let notification else { return nil } - var str = "Follow requested by " - str += notification.account.displayNameWithoutCustomEmoji - str += ", \(notification.createdAt.formatted(.relative(presentation: .numeric)))" - return str - } - set {} - } - - override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { - get { - return [ - UIAccessibilityCustomAction(name: "Accept Request", target: self, selector: #selector(acceptButtonPressed)), - UIAccessibilityCustomAction(name: "Reject Request", target: self, selector: #selector(acceptButtonPressed)), - ] - } - set {} - } - - // MARK: - Interaction - - @IBAction func rejectButtonPressed() { - acceptButton.isEnabled = false - rejectButton.isEnabled = false - - Task { - let request = Account.rejectFollowRequest(account.id) - do { - _ = try await mastodonController.run(request) - - UIImpactFeedbackGenerator(style: .light).impactOccurred() - self.actionButtonsStackView.isHidden = true - self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label")) - } catch let error as Client.Error { - acceptButton.isEnabled = true - rejectButton.isEnabled = true - if let toastable = delegate?.toastableViewController { - let config = ToastConfiguration(from: error, with: "Rejecting Follow", in: toastable) { [weak self] toast in - toast.dismissToast(animated: true) - self?.rejectButtonPressed() - } - toastable.showToast(configuration: config, animated: true) - } - } - } - } - - @IBAction func acceptButtonPressed() { - acceptButton.isEnabled = false - rejectButton.isEnabled = false - - Task { - let request = Account.authorizeFollowRequest(account.id) - do { - _ = try await mastodonController.run(request) - - UIImpactFeedbackGenerator(style: .light).impactOccurred() - self.actionButtonsStackView.isHidden = true - self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label")) - } catch let error as Client.Error { - acceptButton.isEnabled = true - rejectButton.isEnabled = true - - if let toastable = delegate?.toastableViewController { - let config = ToastConfiguration(from: error, with: "Accepting Follow", in: toastable) { [weak self] toast in - toast.dismissToast(animated: true) - self?.acceptButtonPressed() - } - toastable.showToast(configuration: config, animated: true) - } - } - } - } - -} - -extension FollowRequestNotificationTableViewCell: SelectableTableViewCell { - func didSelectCell() { - delegate?.selected(account: account.id) - } -} - -extension FollowRequestNotificationTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - guard let mastodonController = mastodonController else { return nil } - return (content: { - return ProfileViewController(accountID: self.account.id, mastodonController: mastodonController) - }, actions: { - return [] - }) - } -} - -extension FollowRequestNotificationTableViewCell: DraggableTableViewCell { - func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { - let provider = NSItemProvider(object: account.url as NSURL) - let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id) - activity.displaysAuxiliaryScene = true - provider.registerObject(activity, visibility: .all) - return [UIDragItem(itemProvider: provider)] - } -} diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.xib b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.xib deleted file mode 100644 index ffe57b35..00000000 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.xib +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Notifications/PollFinishedTableViewCell.swift b/Tusker/Views/Notifications/PollFinishedTableViewCell.swift deleted file mode 100644 index d568237a..00000000 --- a/Tusker/Views/Notifications/PollFinishedTableViewCell.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// PollFinishedTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 4/28/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import SwiftSoup - -class PollFinishedTableViewCell: UITableViewCell { - - weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? - var mastodonController: MastodonController? { delegate?.apiController } - - @IBOutlet weak var displayNameLabel: EmojiLabel! - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var statusContentLabel: UILabel! - @IBOutlet weak var pollView: StatusPollView! - - var notification: Pachyderm.Notification? - - private var updateTimestampWorkItem: DispatchWorkItem? - - override func awakeFromNib() { - super.awakeFromNib() - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - displayNameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! - displayNameLabel.adjustsFontForContentSizeCategory = true - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - func updateUI(notification: Pachyderm.Notification) { - guard let statusID = notification.status?.id, - let status = delegate?.apiController.persistentContainer.status(for: statusID), - let poll = status.poll else { - return - } - - self.notification = notification - - updateTimestamp() - - displayNameLabel.text = notification.account.displayName - displayNameLabel.setEmojis(notification.account.emojis, identifier: notification.account.id) - - let doc = try! SwiftSoup.parseBodyFragment(status.content) - statusContentLabel.text = try! doc.text() - - pollView.mastodonController = mastodonController - pollView.toastableViewController = delegate - pollView.updateUI(status: status, poll: poll) - } - - private func updateTimestamp() { - guard let notification = notification else { return } - - timestampLabel.text = notification.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch notification.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } - } else { - updateTimestampWorkItem = nil - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - } - - // MARK: Accessibility - - override var accessibilityLabel: String? { - get { - guard let notification else { return nil } - var str = "Poll from " - str += notification.account.displayNameWithoutCustomEmoji - str += " finished " - str += notification.createdAt.formatted(.relative(presentation: .numeric)) - if let poll = notification.status?.poll, - poll.options.contains(where: { ($0.votesCount ?? 0) > 0 }) { - let winner = poll.options.max(by: { ($0.votesCount ?? 0) < ($1.votesCount ?? 0) })! - str += ", winning option: \(winner.title)" - } - return str - } - set {} - } - -} - -extension PollFinishedTableViewCell: SelectableTableViewCell { - func didSelectCell() { - guard let delegate = delegate, - let status = notification?.status else { - return - } - delegate.selected(status: status.id) - } -} - -extension PollFinishedTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - guard let delegate = delegate, - let statusID = notification?.status?.id, - let status = delegate.apiController.persistentContainer.status(for: statusID) else { - return nil - } - return (content: { - ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController) - }, actions: { - delegate.actionsForStatus(status, source: .view(self)) - }) - } -} diff --git a/Tusker/Views/Notifications/PollFinishedTableViewCell.xib b/Tusker/Views/Notifications/PollFinishedTableViewCell.xib deleted file mode 100644 index 34ba18bf..00000000 --- a/Tusker/Views/Notifications/PollFinishedTableViewCell.xib +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.swift b/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.swift deleted file mode 100644 index fb7df591..00000000 --- a/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// StatusUpdatedNotificationTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 11/27/22. -// Copyright © 2022 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import SwiftSoup - -class StatusUpdatedNotificationTableViewCell: UITableViewCell { - - weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? - - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var displayNameLabel: EmojiLabel! - @IBOutlet weak var contentLabel: UILabel! - - private var notification: Pachyderm.Notification? - private var updateTimestampWorkItem: DispatchWorkItem? - - override func awakeFromNib() { - super.awakeFromNib() - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - displayNameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! - displayNameLabel.adjustsFontForContentSizeCategory = true - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - func updateUI(notification: Pachyderm.Notification) { - guard notification.kind == .update, - let status = notification.status else { - return - } - - self.notification = notification - - updateTimestamp() - - displayNameLabel.text = notification.account.displayName - displayNameLabel.setEmojis(notification.account.emojis, identifier: notification.account.id) - - let doc = try! SwiftSoup.parseBodyFragment(status.content) - contentLabel.text = try! doc.text() - } - - private func updateTimestamp() { - guard let notification else { return } - - timestampLabel.text = notification.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch notification.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } - } else { - updateTimestampWorkItem = nil - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - } - - // MARK: Accessibility - - override var accessibilityLabel: String? { - get { - guard let notification else { return nil } - var str = "Post from " - str += notification.account.displayNameWithoutCustomEmoji - str += " edited " - str += notification.createdAt.formatted(.relative(presentation: .numeric)) - str += ", " - str += contentLabel.text ?? "" - return str - } - set {} - } - -} - -extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell { - func didSelectCell() { - guard let delegate, - let status = notification?.status else { - return - } - delegate.selected(status: status.id) - } -} - -extension StatusUpdatedNotificationTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - guard let delegate, - let statusID = notification?.status?.id, - let status = delegate.apiController.persistentContainer.status(for: statusID) else { - return nil - } - return (content: { - ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController) - }, actions: { - delegate.actionsForStatus(status, source: .view(self)) - }) - } -} diff --git a/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.xib b/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.xib deleted file mode 100644 index 8e7f5f54..00000000 --- a/Tusker/Views/Notifications/StatusUpdatedNotificationTableViewCell.xib +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift deleted file mode 100644 index 37f20974..00000000 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ /dev/null @@ -1,512 +0,0 @@ -// -// BaseStatusTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 11/19/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import Combine -import AVKit - -protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider { - // @available(iOS, obsoleted: 16.0) - func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) -} - -class BaseStatusTableViewCell: UITableViewCell { - - weak var delegate: StatusTableViewCellDelegate? { - didSet { - contentTextView.navigationDelegate = delegate - } - } - var overrideMastodonController: MastodonController? - var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController } - - @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var displayNameLabel: EmojiLabel! - @IBOutlet weak var usernameLabel: UILabel! - @IBOutlet weak var metaIndicatorsView: StatusMetaIndicatorsView! - @IBOutlet weak var contentWarningLabel: EmojiLabel! - @IBOutlet weak var collapseButton: UIButton! - @IBOutlet weak var contentTextView: StatusContentTextView! - @IBOutlet weak var cardView: StatusCardView! - @IBOutlet weak var attachmentsView: AttachmentsContainerView! - @IBOutlet weak var pollView: StatusPollView! - @IBOutlet weak var replyButton: UIButton! - @IBOutlet weak var favoriteButton: UIButton! - @IBOutlet weak var reblogButton: UIButton! - @IBOutlet weak var moreButton: UIButton! - private(set) var prevThreadLinkView: UIView? - private(set) var nextThreadLinkView: UIView? - - var statusID: String! - private(set) var accountID: String! - - private var favorited = false { - didSet { - favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : .tintColor - } - } - private var reblogged = false { - didSet { - reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : .tintColor - } - } - - private(set) var statusState: CollapseState! - var collapsible = false { - didSet { - collapseButton.isHidden = !collapsible - statusState?.collapsible = collapsible - } - } - private var collapsed = false { - didSet { - statusState?.collapsed = collapsed - } - } - var showStatusAutomatically = false - - private var avatarRequest: ImageCache.Request? - - private var statusUpdater: Cancellable? - private var accountUpdater: Cancellable? - - private var currentPictureInPictureVideoStatusID: String? - - private var isGrayscale = false - - override func awakeFromNib() { - super.awakeFromNib() - - displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) - usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) - - avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) - avatarImageView.layer.masksToBounds = true - avatarImageView.addInteraction(UIDragInteraction(delegate: self)) - - attachmentsView.delegate = self - - collapseButton.layer.masksToBounds = true - collapseButton.layer.cornerRadius = 5 - - accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!, pollView!] - - moreButton.showsMenuAsPrimaryAction = true - - NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) - - contentWarningLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed))) - } - - open func createObserversIfNecessary() { - if statusUpdater == nil { - statusUpdater = mastodonController.persistentContainer.statusSubject - .receive(on: DispatchQueue.main) - .filter { [unowned self] in $0 == self.statusID } - .sink { [unowned self] in - if let mastodonController = mastodonController, - let status = mastodonController.persistentContainer.status(for: $0) { - self.updateStatusState(status: status) - } - } - } - - if accountUpdater == nil { - accountUpdater = mastodonController.persistentContainer.accountSubject - .receive(on: DispatchQueue.main) - .filter { [unowned self] in $0 == self.accountID } - .sink { [unowned self] in - if let mastodonController = mastodonController, - let account = mastodonController.persistentContainer.account(for: $0) { - self.updateUI(account: account) - } - } - } - } - - final func updateUI(statusID: String, state: CollapseState) { - createObserversIfNecessary() - - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - fatalError("Missing cached status") - } - - self.statusID = statusID - - doUpdateUI(status: status, state: state) - } - - func doUpdateUI(status: StatusMO, state: CollapseState) { - self.statusState = state - - let account = status.account - self.accountID = account.id - updateUI(account: account) - contentTextView.setTextFrom(status: status) - updateGrayscaleableUI(account: account, status: status) - updateUIForPreferences(account: account, status: status) - - cardView.updateUI(status: status) - cardView.isHidden = status.card == nil - cardView.navigationDelegate = delegate - cardView.actionProvider = delegate - - attachmentsView.updateUI(status: status) - - updateStatusState(status: status) - - contentWarningLabel.text = status.spoilerText - contentWarningLabel.isHidden = status.spoilerText.isEmpty - if !contentWarningLabel.isHidden { - contentWarningLabel.setEmojis(status.emojis, identifier: statusID) - } - - let reblogDisabled: Bool - if mastodonController.instanceFeatures.boostToOriginalAudience { - // Pleroma allows 'Boost to original audience' for your own private posts - reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account?.id) - } else { - reblogDisabled = status.visibility == .private || status.visibility == .direct - } - reblogButton.isEnabled = !reblogDisabled && mastodonController.loggedIn - - favoriteButton.isEnabled = mastodonController.loggedIn - replyButton.isEnabled = mastodonController.loggedIn - - updateStatusIconsForPreferences(status) - - if state.unknown { - // for some reason the height here can't be computed correctly, so we fallback to the old hack of just considering raw length - state.resolveFor(status: status, height: 0, textLength: contentTextView.attributedText.length) - if state.collapsible! && showStatusAutomatically { - state.collapsed = false - } - } - collapsible = state.collapsible! - setCollapsed(state.collapsed!, animated: false) - } - - func updateStatusState(status: StatusMO) { - favorited = status.favourited - reblogged = status.reblogged - - if favorited { - favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label") - } else { - favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label") - } - if reblogged { - reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label") - } else { - reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label") - } - - // keep menu in sync with changed states e.g. bookmarked, muted - // do not include reply action here, because the cell already contains a button for it - moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? []) - - pollView.isHidden = status.poll == nil - pollView.mastodonController = mastodonController - pollView.toastableViewController = delegate?.toastableViewController - pollView.updateUI(status: status, poll: status.poll) - } - - func updateUI(account: AccountMO) { - usernameLabel.text = "@\(account.acct)" - avatarImageView.image = nil - } - - @objc private func preferencesChanged() { - guard let mastodonController = mastodonController, - let account = mastodonController.persistentContainer.account(for: accountID), - let status = mastodonController.persistentContainer.status(for: statusID) else { return } - updateUIForPreferences(account: account, status: status) - } - - func updateUIForPreferences(account: AccountMO, status: StatusMO) { - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - switch Preferences.shared.attachmentBlurMode { - case .never: - attachmentsView.contentHidden = false - case .always: - attachmentsView.contentHidden = true - default: - if status.sensitive { - attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning - } else { - attachmentsView.contentHidden = false - } - } - - updateStatusIconsForPreferences(status) - - if isGrayscale != Preferences.shared.grayscaleImages { - updateGrayscaleableUI(account: account, status: status) - } - } - - func updateStatusIconsForPreferences(_ status: StatusMO) { - metaIndicatorsView.updateUI(status: status) - - let reblogButtonImage: UIImage - if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled { - reblogButtonImage = UIImage(systemName: "repeat")! - } else { - reblogButtonImage = UIImage(systemName: status.visibility.imageName)! - } - reblogButton.setImage(reblogButtonImage, for: .normal) - } - - func updateGrayscaleableUI(account: AccountMO, status: StatusMO) { - isGrayscale = Preferences.shared.grayscaleImages - - let accountID = account.id - if let avatarURL = account.avatar { - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in - guard let self = self, - let image = image, - self.accountID == accountID, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return } - - DispatchQueue.main.async { - self.avatarImageView.image = transformedImage - } - } - } - - if contentTextView.hasEmojis { - contentTextView.setTextFrom(status: status) - } - - displayNameLabel.updateForAccountDisplayName(account: account) - } - - func setShowThreadLinks(prev: Bool, next: Bool) { - if prev { - if let prevThreadLinkView = prevThreadLinkView { - prevThreadLinkView.isHidden = false - } else { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .tintColor.withAlphaComponent(0.5) - view.layer.cornerRadius = 2.5 - view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - prevThreadLinkView = view - addSubview(view) - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(equalToConstant: 5), - view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), - view.topAnchor.constraint(equalTo: topAnchor), - view.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2), - ]) - } - } else { - prevThreadLinkView?.isHidden = true - } - - if next { - if let nextThreadLinkView = nextThreadLinkView { - nextThreadLinkView.isHidden = false - } else { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .tintColor.withAlphaComponent(0.5) - view.layer.cornerRadius = 2.5 - view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - nextThreadLinkView = view - addSubview(view) - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(equalToConstant: 5), - view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), - view.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 2), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - } else { - nextThreadLinkView?.isHidden = true - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - avatarRequest?.cancel() - showStatusAutomatically = false - } - - // MARK: - Interaction - - @IBAction func collapseButtonPressed() { - setCollapsed(!collapsed, animated: true) - if #available(iOS 16.0, *) { - invalidateIntrinsicContentSize() - } else { - delegate?.statusCellCollapsedStateChanged(self) - } - } - - func setCollapsed(_ collapsed: Bool, animated: Bool) { - self.collapsed = collapsed - - contentTextView.isHidden = collapsed - cardView.isHidden = cardView.card == nil || collapsed - attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed - pollView.isHidden = pollView.poll == nil || collapsed - - let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")! - - if let buttonImageView = collapseButton.imageView { - collapseButton.setImage(buttonImage, for: .normal) - - if animated { - buttonImageView.layer.opacity = 0 - - // this whole hack is necessary because when just rotating buttonImageView, it moves to the left of the button and then animates back to the center - let imageView = UIImageView(image: buttonImageView.image) - imageView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(imageView) - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalTo: buttonImageView.widthAnchor), - imageView.heightAnchor.constraint(equalTo: buttonImageView.heightAnchor), - imageView.centerXAnchor.constraint(equalTo: collapseButton.centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: collapseButton.centerYAnchor), - ]) - imageView.tintColor = .white - - UIView.animate(withDuration: 0.3, delay: 0) { - imageView.transform = CGAffineTransform(rotationAngle: .pi) - } completion: { _ in - imageView.removeFromSuperview() - buttonImageView.layer.opacity = 1 - } - } - } - - if collapsed { - collapseButton.accessibilityLabel = NSLocalizedString("Expand Status", comment: "expand status button accessibility label") - } else { - collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label") - } - - } - - @IBAction func replyPressed() { - delegate?.compose(inReplyToID: statusID) - } - - @IBAction func favoritePressed() { - guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - Task { - await FavoriteService(status: status, mastodonController: mastodonController, presenter: delegate!).toggleFavorite() - } - } - - @IBAction func reblogPressed() { - guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - Task { - await ReblogService(status: status, mastodonController: mastodonController, presenter: delegate!).toggleReblog() - } - } - - @IBAction func morePressed() { - delegate?.showMoreOptions(forStatus: statusID, source: .view(moreButton)) - } - - @objc func accountPressed() { - delegate?.selected(account: accountID) - } -} - -extension BaseStatusTableViewCell: AttachmentViewDelegate { - func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? { - guard let delegate = delegate, - let status = mastodonController.persistentContainer.status(for: statusID) else { return nil } - let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) - let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) - gallery.avPlayerViewControllerDelegate = self - return gallery - } - - func attachmentViewPresent(_ vc: UIViewController, animated: Bool) { - delegate?.present(vc, animated: animated) - } -} - -// todo: This is not ideal. It works when the original cell remains visible and when the cell is reused, but if the cell is dealloc'd -// resuming from PiP won't work because AVPlayerViewController.delegate is a weak reference. -extension BaseStatusTableViewCell: AVPlayerViewControllerDelegate { - func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { - // We need to save the current statusID when PiP is initiated, because if the user restores from PiP after this cell has - // been reused, the current value of statusID will not be correct. - currentPictureInPictureVideoStatusID = statusID - } - - func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { - currentPictureInPictureVideoStatusID = nil - } - - func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool { - // Ideally, when PiP is automatically initiated by app closing the gallery should not be dismissed - // and when PiP is started because the user has tapped the button in the player controls the gallery - // gallery should be dismissed. Unfortunately, this doesn't seem to be possible. Instead, the gallery is - // always dismissed and is recreated when restoring the interface from PiP. - return true - } - - func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { - guard let delegate = delegate, - let playerViewController = playerViewController as? GalleryPlayerViewController, - let id = currentPictureInPictureVideoStatusID, - let status = mastodonController.persistentContainer.status(for: id), - let index = status.attachments.firstIndex(where: { $0.id == playerViewController.attachment?.id }) else { - // returning without invoking completionHandler will dismiss the PiP window - return - } - - // We create a new gallery view controller starting at the appropriate index and swap the - // already-playing VC into the appropriate index so it smoothly continues playing. - - let sourceViews: [UIImageView?] - if self.statusID == id { - sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) - } else { - sourceViews = status.attachments.map { (_) in nil } - } - let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) - gallery.avPlayerViewControllerDelegate = self - - // ensure that all other page VCs are created - gallery.loadViewIfNeeded() - // replace the newly created player for the same attachment with the already-playing one - gallery.pages[index] = playerViewController - gallery.setViewControllers([playerViewController], direction: .forward, animated: false, completion: nil) - - // this isn't animated, otherwise the animation plays first and then the PiP window expands - // which looks even weirder than the black background appearing instantly and then the PiP window animating - delegate.present(gallery, animated: false) { - completionHandler(false) - } - } -} - -extension BaseStatusTableViewCell: UIDragInteractionDelegate { - func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { - guard let currentAccountID = mastodonController.accountInfo?.id, - let account = mastodonController.persistentContainer.account(for: accountID) else { - return [] - } - let provider = NSItemProvider(object: account.url as NSURL) - let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID) - activity.displaysAuxiliaryScene = true - provider.registerObject(activity, visibility: .all) - return [UIDragItem(itemProvider: provider)] - } -} diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift deleted file mode 100644 index 33491588..00000000 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ /dev/null @@ -1,457 +0,0 @@ -// -// TimelineStatusTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 8/16/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Combine -import Pachyderm - -class TimelineStatusTableViewCell: BaseStatusTableViewCell { - - static let relativeDateFormatter: RelativeDateTimeFormatter = { - let formatter = RelativeDateTimeFormatter() - formatter.dateTimeStyle = .numeric - formatter.unitsStyle = .full - return formatter - }() - - @IBOutlet weak var reblogLabel: EmojiLabel! - @IBOutlet weak var reblogSpacer: UIView! - @IBOutlet weak var timestampLabel: UILabel! - @IBOutlet weak var pinImageView: UIImageView! - @IBOutlet weak var actionsContainerView: UIView! - @IBOutlet weak var actionsContainerHeightConstraint: NSLayoutConstraint! - - var reblogStatusID: String? - var rebloggerID: String? - - var showPinned = false - var showReplyIndicator = true - - var updateTimestampWorkItem: DispatchWorkItem? - - var rebloggerAccountUpdater: Cancellable? - - deinit { - rebloggerAccountUpdater?.cancel() - updateTimestampWorkItem?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - isAccessibilityElement = true - - reblogLabel.font = .preferredFont(forTextStyle: .body) - reblogLabel.adjustsFontForContentSizeCategory = true - reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) - - avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self)) - - displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue, - ] - ]), size: 0) - displayNameLabel.adjustsFontForContentSizeCategory = true - - usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - usernameLabel.adjustsFontForContentSizeCategory = true - - timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, - ] - ]), size: 0) - timestampLabel.adjustsFontForContentSizeCategory = true - - metaIndicatorsView.primaryAxis = .vertical - metaIndicatorsView.secondaryAxisAlignment = .trailing - - contentWarningLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ - .traits: [ - UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue, - ] - ]), size: 0) - contentWarningLabel.adjustsFontForContentSizeCategory = true - - contentTextView.defaultFont = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 16)) - contentTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular)) - contentTextView.adjustsFontForContentSizeCategory = true - - // todo: double check this on RTL layouts - replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true - - updateActionsVisibility() - } - - override func updateConfiguration(using state: UICellConfigurationState) { - backgroundConfiguration = .appListPlainCell(for: state) - } - - override func createObserversIfNecessary() { - super.createObserversIfNecessary() - - if rebloggerAccountUpdater == nil { - rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject - .receive(on: DispatchQueue.main) - .filter { [unowned self] in $0 == self.rebloggerID } - .sink { [unowned self] in - if let mastodonController = self.mastodonController, - let reblogger = mastodonController.persistentContainer.account(for: $0) { - self.updateRebloggerLabel(reblogger: reblogger) - } - } - } - } - - override func doUpdateUI(status: StatusMO, state: CollapseState) { - var status = status - - if let rebloggedStatus = status.reblog { - reblogStatusID = statusID - rebloggerID = status.account.id - reblogLabel.isHidden = false - reblogSpacer.isHidden = false - updateRebloggerLabel(reblogger: status.account) - - status = rebloggedStatus - // necessary b/c statusID is initially set to the reblog status ID in updateUI(statusID:state:) - statusID = rebloggedStatus.id - } else { - reblogStatusID = nil - rebloggerID = nil - reblogLabel.isHidden = true - reblogSpacer.isHidden = true - } - - super.doUpdateUI(status: status, state: state) - - doUpdateTimestamp(status: status) - - timestampLabel.isHidden = showPinned - pinImageView.isHidden = !showPinned - } - - override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) { - super.updateGrayscaleableUI(account: account, status: status) - - if let rebloggerID = rebloggerID, - reblogLabel.hasEmojis, - let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) { - updateRebloggerLabel(reblogger: reblogger) - } - } - - private func updateRebloggerLabel(reblogger: AccountMO) { - if Preferences.shared.hideCustomEmojiInUsernames { - reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)" - reblogLabel.removeEmojis() - } else { - reblogLabel.text = "Reblogged by \(reblogger.displayOrUserName)" - reblogLabel.setEmojis(reblogger.emojis, identifier: reblogger.id) - } - } - - override func updateStatusIconsForPreferences(_ status: StatusMO) { - if showReplyIndicator { - metaIndicatorsView.allowedIndicators = .all - } else { - metaIndicatorsView.allowedIndicators = .all.subtracting(.reply) - } - - let oldState = actionsContainerView.isHidden - if oldState != Preferences.shared.hideActionsInTimeline { - updateActionsVisibility() - if #available(iOS 16.0, *) { - invalidateIntrinsicContentSize() - } else { - // not really accurate, but it notifies the vc our height has changed - delegate?.statusCellCollapsedStateChanged(self) - } - } - - super.updateStatusIconsForPreferences(status) - } - - private func updateActionsVisibility() { - if Preferences.shared.hideActionsInTimeline { - actionsContainerView.isHidden = true - } else { - actionsContainerView.isHidden = false - } - } - - private func updateTimestamp() { - // if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated - // so we bail out immediately, since there's nothing to update - // if the status cannot be found, it may have already been discarded due to not being on screen, so we do nothing - guard let mastodonController = mastodonController, - let status = mastodonController.persistentContainer.status(for: statusID) else { return } - - doUpdateTimestamp(status: status) - } - - private func doUpdateTimestamp(status: StatusMO) { - timestampLabel.text = status.createdAt.timeAgoString() - - let delay: DispatchTimeInterval? - switch status.createdAt.timeAgo().1 { - case .second: - delay = .seconds(10) - case .minute: - delay = .seconds(60) - default: - delay = nil - } - if let delay = delay { - if updateTimestampWorkItem == nil { - updateTimestampWorkItem = DispatchWorkItem { [weak self] in - self?.updateTimestamp() - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) - } else { - updateTimestampWorkItem = nil - } - } - - func reply() { - if Preferences.shared.mentionReblogger, - let rebloggerID = rebloggerID, - let rebloggerAccount = mastodonController.persistentContainer.account(for: rebloggerID) { - delegate?.compose(inReplyToID: statusID, mentioningAcct: rebloggerAccount.acct) - } else { - delegate?.compose(inReplyToID: statusID) - } - } - - override func prepareForReuse() { - super.prepareForReuse() - updateTimestampWorkItem?.cancel() - updateTimestampWorkItem = nil - showPinned = false - } - - @objc func reblogLabelPressed() { - guard let rebloggerID = rebloggerID else { return } - delegate?.selected(account: rebloggerID) - } - - override func replyPressed() { - reply() - } - - // MARK: - Accessibility - - override var accessibilityAttributedLabel: NSAttributedString? { - get { - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - return nil - } - var str: AttributedString = "" - if let rebloggerID, - let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) { - str += AttributedString("Reblogged by \(reblogger.displayNameWithoutCustomEmoji): ") - } - str += AttributedString(status.account.displayNameWithoutCustomEmoji) - str += ", " - if statusState.collapsed ?? false { - if !status.spoilerText.isEmpty { - str += AttributedString(status.spoilerText) - str += ", " - } - str += "collapsed" - } else { - str += AttributedString(contentTextView.attributedText) - - if status.attachments.count > 0 { - let includeDescriptions: Bool - switch Preferences.shared.attachmentBlurMode { - case .useStatusSetting: - includeDescriptions = !Preferences.shared.blurMediaBehindContentWarning || status.spoilerText.isEmpty - case .always: - includeDescriptions = true - case .never: - includeDescriptions = false - } - if includeDescriptions { - if status.attachments.count == 1 { - let attachment = status.attachments[0] - let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description" - str += AttributedString(", attachment: \(desc)") - } else { - for (index, attachment) in status.attachments.enumerated() { - let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description" - str += AttributedString(", attachment \(index + 1): \(desc)") - } - } - } else { - str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count == 1 ? "" : "s")") - } - } - if status.poll != nil { - str += ", poll" - } - } - - str += AttributedString(", \(status.createdAt.formatted(.relative(presentation: .numeric)))") - if status.visibility < .unlisted { - str += AttributedString(", \(status.visibility.displayName)") - } - if status.localOnly { - str += ", local" - } - return NSAttributedString(str) - } - set {} - } - - override var accessibilityHint: String? { - get { - if statusState.collapsed ?? false { - return "Double tap to expand the post." - } else { - return nil - } - } - set {} - } - - override func accessibilityActivate() -> Bool { - if statusState.collapsed ?? false { - collapseButtonPressed() - } else { - didSelectCell() - } - return true - } - - override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { - get { - guard let text = contentTextView.attributedText, - let status = mastodonController.persistentContainer.status(for: statusID) else { - return nil - } - var actions = [ - UIAccessibilityCustomAction(name: "Show \(status.account.displayNameWithoutCustomEmoji)", actionHandler: { [unowned self] _ in - self.delegate?.selected(account: status.account.id) - return true - }) - ] - text.enumerateAttribute(.link, in: NSRange(location: 0, length: text.length)) { value, range, stop in - guard let value = value as? URL else { - return - } - let text = text.attributedSubstring(from: range).string - actions.append(UIAccessibilityCustomAction(name: text) { [unowned self] _ in - self.contentTextView.handleLinkTapped(url: value, text: text) - return true - }) - } - return actions - } - set {} - } - -} - -extension TimelineStatusTableViewCell: SelectableTableViewCell { - func didSelectCell() { - delegate?.selected(status: statusID, state: statusState.copy()) - } -} - -extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { - - func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - return nil - } - return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) - } - - func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - return nil - } - return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) - } - -} - -extension TimelineStatusTableViewCell: DraggableTableViewCell { - func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { - // the poll options view is tracking while the user is dragging between options - // while that's happening, don't initiate a drag - guard let status = mastodonController.persistentContainer.status(for: statusID), - let accountID = mastodonController.accountInfo?.id, - !pollView.isTracking else { - return [] - } - let provider = NSItemProvider(object: status.url! as NSURL) - let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID) - activity.displaysAuxiliaryScene = true - provider.registerObject(activity, visibility: .all) - return [UIDragItem(itemProvider: provider)] - } -} - -extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate { - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - return UIContextMenuConfiguration(identifier: nil) { - ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController) - } actionProvider: { (_) in - return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? []) - } - } - - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { - if let viewController = animator.previewViewController, - let delegate = delegate { - animator.preferredCommitStyle = .pop - animator.addCompletion { - if let customPresenting = viewController as? CustomPreviewPresenting { - customPresenting.presentFromPreview(presenter: delegate) - } else { - delegate.show(viewController) - } - } - } - } - -} - -extension TimelineStatusTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - guard let mastodonController = mastodonController, - let status = mastodonController.persistentContainer.status(for: statusID) else { - return nil - } - return ( - content: { ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) }, - actions: { self.delegate?.actionsForStatus(status, source: .view(self)) ?? [] } - ) - } -} - -extension TimelineStatusTableViewCell: StatusSwipeActionContainer { - var navigationDelegate: TuskerNavigationDelegate { delegate! } - var toastableViewController: ToastableViewController? { delegate } - - var canReblog: Bool { - reblogButton.isEnabled - } - - func performReplyAction() { - self.replyPressed() - } -} diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.xib b/Tusker/Views/Status/TimelineStatusTableViewCell.xib deleted file mode 100644 index 1ce14e1d..00000000 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.xib +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -