From f9240628148304380980bbd817510e946a01eac6 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Thu, 4 Feb 2021 18:56:14 -0800 Subject: [PATCH] Notification navigation --- Localizations/Localizable.strings | 2 +- .../Endpoints/NotificationEndpoint.swift | 31 +++++++ .../Services/IdentityService.swift | 15 ++++ .../Services/NavigationService.swift | 1 + .../MainNavigationViewController.swift | 31 +++++-- .../NotificationsViewController.swift | 15 ++++ View Controllers/TableViewController.swift | 88 +++++++++++-------- .../View Models/NavigationViewModel.swift | 17 +++- 8 files changed, 153 insertions(+), 47 deletions(-) create mode 100644 MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationEndpoint.swift diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 982ff1d..a7b426b 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -145,7 +145,7 @@ "main-navigation.notifications" = "Notifications"; "main-navigation.conversations" = "Messages"; "metatext" = "Metatext"; -"notification.signed-in-as-%@" = "Signed in as %@"; +"notification.signed-in-as-%@" = "Logged in as %@"; "notifications.all" = "All"; "notifications.mentions" = "Mentions"; "ok" = "OK"; diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationEndpoint.swift new file mode 100644 index 0000000..60c68bc --- /dev/null +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/NotificationEndpoint.swift @@ -0,0 +1,31 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import HTTP +import Mastodon + +public enum NotificationEndpoint { + case notification(id: MastodonNotification.Id) +} + +extension NotificationEndpoint: Endpoint { + public typealias ResultType = MastodonNotification + + public var context: [String] { + defaultContext + ["notifications"] + } + + public var pathComponentsInContext: [String] { + switch self { + case let .notification(id): + return [id] + } + } + + public var method: HTTPMethod { + switch self { + case .notification: + return .get + } + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index 53d2bf2..e1a623b 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -247,6 +247,21 @@ public extension IdentityService { mastodonAPIClient.request(StatusEndpoint.post(statusComponents)).map(\.id).eraseToAnyPublisher() } + func notificationService(pushNotification: PushNotification) -> AnyPublisher { + mastodonAPIClient.request(NotificationEndpoint.notification(id: .init(pushNotification.notificationId))) + .flatMap { notification in + contentDatabase.insert(notifications: [notification]) + .collect() + .map { _ in + NotificationService( + notification: notification, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) + } + } + .eraseToAnyPublisher() + } + func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService { AccountListService( endpoint: accountList, diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift index 9a6cd4f..4b4b83d 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift @@ -10,6 +10,7 @@ public enum Navigation { case url(URL) case collection(CollectionService) case profile(ProfileService) + case notification(NotificationService) case searchScope(SearchScope) case webfingerStart case webfingerEnd diff --git a/View Controllers/MainNavigationViewController.swift b/View Controllers/MainNavigationViewController.swift index 9081581..7e53ed4 100644 --- a/View Controllers/MainNavigationViewController.swift +++ b/View Controllers/MainNavigationViewController.swift @@ -33,7 +33,7 @@ final class MainNavigationViewController: UITabBarController { } .store(in: &cancellables) - viewModel.$presentingSecondaryNavigation.sink { [weak self] in + viewModel.$presentingSecondaryNavigation.removeDuplicates().sink { [weak self] in if $0 { self?.presentSecondaryNavigation() } else { @@ -182,27 +182,40 @@ private extension MainNavigationViewController { } func handle(navigation: Navigation) { - let vc: UIViewController - switch navigation { case let .collection(collectionService): - vc = TableViewController( + let vc = TableViewController( viewModel: CollectionItemsViewModel( collectionService: collectionService, identityContext: viewModel.identityContext), rootViewModel: rootViewModel) + + selectedViewController?.show(vc, sender: self) case let .profile(profileService): - vc = ProfileViewController( + let vc = ProfileViewController( viewModel: ProfileViewModel( profileService: profileService, identityContext: viewModel.identityContext), rootViewModel: rootViewModel, identityContext: viewModel.identityContext, parentNavigationController: nil) - default: - return - } - selectedViewController?.show(vc, sender: self) + selectedViewController?.show(vc, sender: self) + case .notification: + let index = NavigationViewModel.Tab.notifications.rawValue + + guard let viewControllers = viewControllers, + viewControllers.count > index, + let notificationsNavigationController = viewControllers[index] as? UINavigationController, + let notificationsViewController = + notificationsNavigationController.viewControllers.first as? NotificationsViewController + else { break } + + selectedIndex = index + notificationsNavigationController.popToRootViewController(animated: false) + notificationsViewController.handle(navigation: navigation) + default: + break + } } } diff --git a/View Controllers/NotificationsViewController.swift b/View Controllers/NotificationsViewController.swift index 3ec3c4d..0c125e8 100644 --- a/View Controllers/NotificationsViewController.swift +++ b/View Controllers/NotificationsViewController.swift @@ -71,6 +71,21 @@ final class NotificationsViewController: UIPageViewController { } } +extension NotificationsViewController { + func handle(navigation: Navigation) { + switch navigation { + case .notification: + guard let firstViewController = notificationViewControllers.first else { return } + + segmentedControl.selectedSegmentIndex = 0 + setViewControllers([firstViewController], direction: .reverse, animated: false) + firstViewController.handle(navigation: navigation) + default: + break + } + } +} + extension NotificationsViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 28ceb9a..dc687e3 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -195,6 +195,48 @@ extension TableViewController { } } } + + func handle(navigation: Navigation) { + switch navigation { + case let .collection(collectionService): + let vc = TableViewController( + viewModel: CollectionItemsViewModel( + collectionService: collectionService, + identityContext: viewModel.identityContext), + rootViewModel: rootViewModel, + parentNavigationController: parentNavigationController) + + if let parentNavigationController = parentNavigationController { + parentNavigationController.pushViewController(vc, animated: true) + } else { + show(vc, sender: self) + } + case let .profile(profileService): + let vc = ProfileViewController( + viewModel: ProfileViewModel( + profileService: profileService, + identityContext: viewModel.identityContext), + rootViewModel: rootViewModel, + identityContext: viewModel.identityContext, + parentNavigationController: parentNavigationController) + + if let parentNavigationController = parentNavigationController { + parentNavigationController.pushViewController(vc, animated: true) + } else { + show(vc, sender: self) + } + case let .notification(notificationService): + navigate(toNotification: notificationService.notification) + case let .url(url): + present(SFSafariViewController(url: url), animated: true) + case .searchScope: + break + case .webfingerStart: + webfingerIndicatorView.startAnimating() + case .webfingerEnd: + webfingerIndicatorView.stopAnimating() + } + } } extension TableViewController: UITableViewDataSourcePrefetching { @@ -374,44 +416,18 @@ private extension TableViewController { } } - func handle(navigation: Navigation) { - switch navigation { - case let .collection(collectionService): - let vc = TableViewController( - viewModel: CollectionItemsViewModel( - collectionService: collectionService, - identityContext: viewModel.identityContext), - rootViewModel: rootViewModel, - parentNavigationController: parentNavigationController) + func navigate(toNotification: MastodonNotification) { + guard let item = dataSource.snapshot().itemIdentifiers.first(where: { + guard case let .notification(notification, _) = $0 else { return false } - if let parentNavigationController = parentNavigationController { - parentNavigationController.pushViewController(vc, animated: true) - } else { - show(vc, sender: self) - } - case let .profile(profileService): - let vc = ProfileViewController( - viewModel: ProfileViewModel( - profileService: profileService, - identityContext: viewModel.identityContext), - rootViewModel: rootViewModel, - identityContext: viewModel.identityContext, - parentNavigationController: parentNavigationController) + return notification.id == toNotification.id + }), + let indexPath = dataSource.indexPath(for: item) + else { return } - if let parentNavigationController = parentNavigationController { - parentNavigationController.pushViewController(vc, animated: true) - } else { - show(vc, sender: self) - } - case let .url(url): - present(SFSafariViewController(url: url), animated: true) - case .searchScope: - break - case .webfingerStart: - webfingerIndicatorView.startAnimating() - case .webfingerEnd: - webfingerIndicatorView.stopAnimating() - } + tableView.scrollToRow(at: indexPath, at: .none, animated: !UIAccessibility.isReduceMotionEnabled) + + viewModel.select(indexPath: indexPath) } func present(attachmentViewModel: AttachmentViewModel, statusViewModel: StatusViewModel) { diff --git a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift index 35bf9ac..6c65a5d 100644 --- a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift @@ -95,24 +95,39 @@ public extension NavigationViewModel { func navigateToProfile(id: Account.Id) { presentingSecondaryNavigation = false + presentedNewStatusViewModel = nil navigationsSubject.send(.profile(identityContext.service.navigationService.profileService(id: id))) } func navigate(timeline: Timeline) { presentingSecondaryNavigation = false + presentedNewStatusViewModel = nil navigationsSubject.send( .collection(identityContext.service.navigationService.timelineService(timeline: timeline))) } func navigateToFollowerRequests() { presentingSecondaryNavigation = false + presentedNewStatusViewModel = nil navigationsSubject.send(.collection(identityContext.service.service( accountList: .followRequests, titleComponents: ["follow-requests"]))) } func navigate(pushNotification: PushNotification) { - // TODO + switch pushNotification.notificationType { + case .followRequest: + navigateToFollowerRequests() + default: + identityContext.service.notificationService(pushNotification: pushNotification) + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .sink { [weak self] in + self?.presentingSecondaryNavigation = false + self?.presentedNewStatusViewModel = nil + self?.navigationsSubject.send(.notification($0)) + } + .store(in: &cancellables) + } } func viewModel(timeline: Timeline) -> CollectionItemsViewModel {