From 195f2d6a29a165a06f1fe2565bf24cd658838612 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Mon, 25 Jan 2021 18:10:24 -0800 Subject: [PATCH] Mentions tab --- DB/Sources/DB/Content/ContentDatabase.swift | 6 +- Localizations/Localizable.strings | 2 + .../Entities/MastodonNotification.swift | 1 + .../Endpoints/NotificationsEndpoint.swift | 9 +- Metatext.xcodeproj/project.pbxproj | 4 + .../Services/IdentityService.swift | 6 +- .../Services/NotificationsService.swift | 14 ++- .../MainNavigationViewController.swift | 10 +- .../NotificationsViewController.swift | 109 ++++++++++++++++++ .../View Models/NavigationViewModel.swift | 24 ++-- Views/NotificationView.swift | 2 + 11 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 View Controllers/NotificationsViewController.swift diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 4e37e78..558c370 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -575,10 +575,12 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func notificationsPublisher() -> AnyPublisher<[CollectionSection], Error> { + func notificationsPublisher( + excludeTypes: Set) -> AnyPublisher<[CollectionSection], Error> { ValueObservation.tracking( NotificationInfo.request( - NotificationRecord.order(NotificationRecord.Columns.id.desc)).fetchAll) + NotificationRecord.order(NotificationRecord.Columns.id.desc) + .filter(!excludeTypes.map(\.rawValue).contains(NotificationRecord.Columns.type))).fetchAll) .removeDuplicates() .publisher(in: databaseWriter) .map { [.init(items: $0.map { diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 3ed9b91..a8c18c3 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -92,6 +92,8 @@ "main-navigation.explore" = "Explore"; "main-navigation.notifications" = "Notifications"; "main-navigation.conversations" = "Messages"; +"notifications.all" = "All"; +"notifications.mentions" = "Mentions"; "ok" = "OK"; "pending.pending-confirmation" = "Your account is pending confirmation"; "post" = "Post"; diff --git a/Mastodon/Sources/Mastodon/Entities/MastodonNotification.swift b/Mastodon/Sources/Mastodon/Entities/MastodonNotification.swift index e6c3dff..60cd759 100644 --- a/Mastodon/Sources/Mastodon/Entities/MastodonNotification.swift +++ b/Mastodon/Sources/Mastodon/Entities/MastodonNotification.swift @@ -26,6 +26,7 @@ public extension MastodonNotification { case favourite case poll case followRequest = "follow_request" + case status case unknown public static var unknownCase: Self { .unknown } diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationsEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationsEndpoint.swift index 63dd769..d8fdfc7 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationsEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationsEndpoint.swift @@ -5,7 +5,7 @@ import HTTP import Mastodon public enum NotificationsEndpoint { - case notifications + case notifications(excludeTypes: Set) } extension NotificationsEndpoint: Endpoint { @@ -15,6 +15,13 @@ extension NotificationsEndpoint: Endpoint { ["notifications"] } + public var queryParameters: [URLQueryItem] { + switch self { + case let .notifications(excludeTypes): + return Array(excludeTypes).map { URLQueryItem(name: "exclude_types[]", value: $0.rawValue) } + } + } + public var method: HTTPMethod { switch self { case .notifications: diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 2a68e70..7b27686 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; }; D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; }; D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */; }; + D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; }; D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; @@ -270,6 +271,7 @@ D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShareExtensionError+Extensions.swift"; sourceTree = ""; }; D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = ""; }; D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = ""; }; + D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; }; D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; }; D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = ""; }; D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = ""; }; @@ -570,6 +572,7 @@ D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */, D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */, D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */, + D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */, D06BC5E525202AD90079541D /* ProfileViewController.swift */, D0F0B12D251A97E400942152 /* TableViewController.swift */, D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */, @@ -940,6 +943,7 @@ D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */, D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */, D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */, + D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */, D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */, D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */, D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */, diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index 7549223..91505da 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -253,8 +253,10 @@ public extension IdentityService { ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } - func notificationsService() -> NotificationsService { - NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + func notificationsService(excludeTypes: Set) -> NotificationsService { + NotificationsService(excludeTypes: excludeTypes, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } func conversationsService() -> ConversationsService { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift index 8fb9697..1e548d3 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift @@ -11,18 +11,22 @@ public struct NotificationsService { public let nextPageMaxId: AnyPublisher public let navigationService: NavigationService + private let excludeTypes: Set private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase private let nextPageMaxIdSubject: CurrentValueSubject - init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(excludeTypes: Set, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { + self.excludeTypes = excludeTypes self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase let nextPageMaxIdSubject = CurrentValueSubject(String(Int.max)) self.nextPageMaxIdSubject = nextPageMaxIdSubject - sections = contentDatabase.notificationsPublisher() + sections = contentDatabase.notificationsPublisher(excludeTypes: excludeTypes) .handleEvents(receiveOutput: { guard case let .notification(notification, _) = $0.last?.items.last, notification.id < nextPageMaxIdSubject.value @@ -37,10 +41,12 @@ public struct NotificationsService { } extension NotificationsService: CollectionService { - public var markerTimeline: Marker.Timeline? { .notifications } + public var markerTimeline: Marker.Timeline? { excludeTypes.isEmpty ? .notifications : nil } public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher { - mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications, maxId: maxId, minId: minId) + mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications(excludeTypes: excludeTypes), + maxId: maxId, + minId: minId) .handleEvents(receiveOutput: { guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return } diff --git a/View Controllers/MainNavigationViewController.swift b/View Controllers/MainNavigationViewController.swift index 6466bb9..ef2c75a 100644 --- a/View Controllers/MainNavigationViewController.swift +++ b/View Controllers/MainNavigationViewController.swift @@ -63,14 +63,8 @@ private extension MainNavigationViewController { rootViewModel: rootViewModel) ] - if let notificationsViewModel = viewModel.notificationsViewModel { - let notificationsViewController = TableViewController( - viewModel: notificationsViewModel, - rootViewModel: rootViewModel) - - notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem - - controllers.append(notificationsViewController) + if viewModel.identityContext.identity.authenticated { + controllers.append(NotificationsViewController(viewModel: viewModel, rootViewModel: rootViewModel)) } if let conversationsViewModel = viewModel.conversationsViewModel { diff --git a/View Controllers/NotificationsViewController.swift b/View Controllers/NotificationsViewController.swift new file mode 100644 index 0000000..3ec3c4d --- /dev/null +++ b/View Controllers/NotificationsViewController.swift @@ -0,0 +1,109 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import Mastodon +import UIKit +import ViewModels + +final class NotificationsViewController: UIPageViewController { + private let segmentedControl = UISegmentedControl(items: [ + NSLocalizedString("notifications.all", comment: ""), + NSLocalizedString("notifications.mentions", comment: "") + ]) + private let notificationViewControllers: [TableViewController] + private let viewModel: NavigationViewModel + private let rootViewModel: RootViewModel + private var cancellables = Set() + + init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) { + self.viewModel = viewModel + self.rootViewModel = rootViewModel + + var excludingAllExceptMentions = Set(MastodonNotification.NotificationType.allCasesExceptUnknown) + + excludingAllExceptMentions.remove(.mention) + + notificationViewControllers = [ + TableViewController(viewModel: viewModel.notificationsViewModel(excludeTypes: []), + rootViewModel: rootViewModel), + TableViewController(viewModel: viewModel.notificationsViewModel(excludeTypes: excludingAllExceptMentions), + rootViewModel: rootViewModel) + ] + + super.init(transitionStyle: .scroll, + navigationOrientation: .horizontal, + options: [.interPageSpacing: CGFloat.defaultSpacing]) + + if let firstViewController = notificationViewControllers.first { + setViewControllers([firstViewController], direction: .forward, animated: false) + } + + tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + dataSource = self + delegate = self + + navigationItem.titleView = segmentedControl + segmentedControl.selectedSegmentIndex = 0 + segmentedControl.addAction( + UIAction { [weak self] _ in + guard let self = self, + let currentViewController = self.viewControllers?.first as? TableViewController, + let currentIndex = self.notificationViewControllers.firstIndex(of: currentViewController), + self.segmentedControl.selectedSegmentIndex != currentIndex + else { return } + + self.setViewControllers( + [self.notificationViewControllers[self.segmentedControl.selectedSegmentIndex]], + direction: self.segmentedControl.selectedSegmentIndex > currentIndex ? .forward : .reverse, + animated: !UIAccessibility.isReduceMotionEnabled) + }, + for: .valueChanged) + } +} + +extension NotificationsViewController: UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard + let viewController = viewController as? TableViewController, + let index = notificationViewControllers.firstIndex(of: viewController), + index + 1 < notificationViewControllers.count + else { return nil } + + return notificationViewControllers[index + 1] + } + + func pageViewController(_ pageViewController: UIPageViewController, + viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard + let viewController = viewController as? TableViewController, + let index = notificationViewControllers.firstIndex(of: viewController), + index > 0 + else { return nil } + + return notificationViewControllers[index - 1] + } +} + +extension NotificationsViewController: UIPageViewControllerDelegate { + func pageViewController(_ pageViewController: UIPageViewController, + didFinishAnimating finished: Bool, + previousViewControllers: [UIViewController], + transitionCompleted completed: Bool) { + guard let viewController = viewControllers?.first as? TableViewController, + let index = notificationViewControllers.firstIndex(of: viewController) + else { return } + + segmentedControl.selectedSegmentIndex = index + } +} diff --git a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift index 7f82996..0c76b11 100644 --- a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift @@ -23,20 +23,6 @@ public final class NavigationViewModel: ObservableObject { return exploreViewModel }() - public lazy var notificationsViewModel: CollectionViewModel? = { - if identityContext.identity.authenticated { - let notificationsViewModel = CollectionItemsViewModel( - collectionService: identityContext.service.notificationsService(), - identityContext: identityContext) - - notificationsViewModel.request(maxId: nil, minId: nil, search: nil) - - return notificationsViewModel - } else { - return nil - } - }() - public lazy var conversationsViewModel: CollectionViewModel? = { if identityContext.identity.authenticated { let conversationsViewModel = CollectionItemsViewModel( @@ -140,4 +126,14 @@ public extension NavigationViewModel { collectionService: identityContext.service.service(timeline: timeline), identityContext: identityContext) } + + func notificationsViewModel(excludeTypes: Set) -> CollectionItemsViewModel { + let viewModel = CollectionItemsViewModel( + collectionService: identityContext.service.notificationsService(excludeTypes: excludeTypes), + identityContext: identityContext) + + viewModel.request(maxId: nil, minId: nil, search: nil) + + return viewModel + } } diff --git a/Views/NotificationView.swift b/Views/NotificationView.swift index 2176059..20a15c8 100644 --- a/Views/NotificationView.swift +++ b/Views/NotificationView.swift @@ -223,6 +223,8 @@ extension MastodonNotification.NotificationType { return "star.fill" case .poll: return "chart.bar.doc.horizontal" + case .status: + return "house" case .mention, .unknown: return "at" }