Mentions tab

This commit is contained in:
Justin Mazzocchi 2021-01-25 18:10:24 -08:00
parent 32a019f8d7
commit 195f2d6a29
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
11 changed files with 156 additions and 31 deletions

View file

@ -575,10 +575,12 @@ public extension ContentDatabase {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func notificationsPublisher() -> AnyPublisher<[CollectionSection], Error> { func notificationsPublisher(
excludeTypes: Set<MastodonNotification.NotificationType>) -> AnyPublisher<[CollectionSection], Error> {
ValueObservation.tracking( ValueObservation.tracking(
NotificationInfo.request( 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() .removeDuplicates()
.publisher(in: databaseWriter) .publisher(in: databaseWriter)
.map { [.init(items: $0.map { .map { [.init(items: $0.map {

View file

@ -92,6 +92,8 @@
"main-navigation.explore" = "Explore"; "main-navigation.explore" = "Explore";
"main-navigation.notifications" = "Notifications"; "main-navigation.notifications" = "Notifications";
"main-navigation.conversations" = "Messages"; "main-navigation.conversations" = "Messages";
"notifications.all" = "All";
"notifications.mentions" = "Mentions";
"ok" = "OK"; "ok" = "OK";
"pending.pending-confirmation" = "Your account is pending confirmation"; "pending.pending-confirmation" = "Your account is pending confirmation";
"post" = "Post"; "post" = "Post";

View file

@ -26,6 +26,7 @@ public extension MastodonNotification {
case favourite case favourite
case poll case poll
case followRequest = "follow_request" case followRequest = "follow_request"
case status
case unknown case unknown
public static var unknownCase: Self { .unknown } public static var unknownCase: Self { .unknown }

View file

@ -5,7 +5,7 @@ import HTTP
import Mastodon import Mastodon
public enum NotificationsEndpoint { public enum NotificationsEndpoint {
case notifications case notifications(excludeTypes: Set<MastodonNotification.NotificationType>)
} }
extension NotificationsEndpoint: Endpoint { extension NotificationsEndpoint: Endpoint {
@ -15,6 +15,13 @@ extension NotificationsEndpoint: Endpoint {
["notifications"] ["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 { public var method: HTTPMethod {
switch self { switch self {
case .notifications: case .notifications:

View file

@ -96,6 +96,7 @@
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; }; D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.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 */; }; 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 */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; }; D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.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 = "<group>"; }; D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShareExtensionError+Extensions.swift"; sourceTree = "<group>"; };
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; }; D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = "<group>"; }; D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = "<group>"; };
D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; }; D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; }; D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; }; D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
@ -570,6 +572,7 @@
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */, D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */, D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */,
D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */, D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */,
D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */,
D06BC5E525202AD90079541D /* ProfileViewController.swift */, D06BC5E525202AD90079541D /* ProfileViewController.swift */,
D0F0B12D251A97E400942152 /* TableViewController.swift */, D0F0B12D251A97E400942152 /* TableViewController.swift */,
D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */, D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */,
@ -940,6 +943,7 @@
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */, D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */,
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */, D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */, D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */, D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */, D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */, D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,

View file

@ -253,8 +253,10 @@ public extension IdentityService {
ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
func notificationsService() -> NotificationsService { func notificationsService(excludeTypes: Set<MastodonNotification.NotificationType>) -> NotificationsService {
NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) NotificationsService(excludeTypes: excludeTypes,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func conversationsService() -> ConversationsService { func conversationsService() -> ConversationsService {

View file

@ -11,18 +11,22 @@ public struct NotificationsService {
public let nextPageMaxId: AnyPublisher<String, Never> public let nextPageMaxId: AnyPublisher<String, Never>
public let navigationService: NavigationService public let navigationService: NavigationService
private let excludeTypes: Set<MastodonNotification.NotificationType>
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let nextPageMaxIdSubject: CurrentValueSubject<String, Never> private let nextPageMaxIdSubject: CurrentValueSubject<String, Never>
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(excludeTypes: Set<MastodonNotification.NotificationType>,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.excludeTypes = excludeTypes
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
let nextPageMaxIdSubject = CurrentValueSubject<String, Never>(String(Int.max)) let nextPageMaxIdSubject = CurrentValueSubject<String, Never>(String(Int.max))
self.nextPageMaxIdSubject = nextPageMaxIdSubject self.nextPageMaxIdSubject = nextPageMaxIdSubject
sections = contentDatabase.notificationsPublisher() sections = contentDatabase.notificationsPublisher(excludeTypes: excludeTypes)
.handleEvents(receiveOutput: { .handleEvents(receiveOutput: {
guard case let .notification(notification, _) = $0.last?.items.last, guard case let .notification(notification, _) = $0.last?.items.last,
notification.id < nextPageMaxIdSubject.value notification.id < nextPageMaxIdSubject.value
@ -37,10 +41,12 @@ public struct NotificationsService {
} }
extension NotificationsService: CollectionService { 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<Never, Error> { public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications, maxId: maxId, minId: minId) mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications(excludeTypes: excludeTypes),
maxId: maxId,
minId: minId)
.handleEvents(receiveOutput: { .handleEvents(receiveOutput: {
guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return } guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return }

View file

@ -63,14 +63,8 @@ private extension MainNavigationViewController {
rootViewModel: rootViewModel) rootViewModel: rootViewModel)
] ]
if let notificationsViewModel = viewModel.notificationsViewModel { if viewModel.identityContext.identity.authenticated {
let notificationsViewController = TableViewController( controllers.append(NotificationsViewController(viewModel: viewModel, rootViewModel: rootViewModel))
viewModel: notificationsViewModel,
rootViewModel: rootViewModel)
notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem
controllers.append(notificationsViewController)
} }
if let conversationsViewModel = viewModel.conversationsViewModel { if let conversationsViewModel = viewModel.conversationsViewModel {

View file

@ -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<AnyCancellable>()
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
}
}

View file

@ -23,20 +23,6 @@ public final class NavigationViewModel: ObservableObject {
return exploreViewModel 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? = { public lazy var conversationsViewModel: CollectionViewModel? = {
if identityContext.identity.authenticated { if identityContext.identity.authenticated {
let conversationsViewModel = CollectionItemsViewModel( let conversationsViewModel = CollectionItemsViewModel(
@ -140,4 +126,14 @@ public extension NavigationViewModel {
collectionService: identityContext.service.service(timeline: timeline), collectionService: identityContext.service.service(timeline: timeline),
identityContext: identityContext) identityContext: identityContext)
} }
func notificationsViewModel(excludeTypes: Set<MastodonNotification.NotificationType>) -> CollectionItemsViewModel {
let viewModel = CollectionItemsViewModel(
collectionService: identityContext.service.notificationsService(excludeTypes: excludeTypes),
identityContext: identityContext)
viewModel.request(maxId: nil, minId: nil, search: nil)
return viewModel
}
} }

View file

@ -223,6 +223,8 @@ extension MastodonNotification.NotificationType {
return "star.fill" return "star.fill"
case .poll: case .poll:
return "chart.bar.doc.horizontal" return "chart.bar.doc.horizontal"
case .status:
return "house"
case .mention, .unknown: case .mention, .unknown:
return "at" return "at"
} }