feat: add navigation to Notification Cell

This commit is contained in:
sunxiaojian 2021-04-14 17:37:58 +08:00
parent 2d71a48e36
commit f3394ff382
9 changed files with 154 additions and 12 deletions

View File

@ -45,6 +45,7 @@
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */; };
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; };
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
@ -433,6 +434,7 @@
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = "<group>"; };
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
@ -1615,6 +1617,7 @@
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */,
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
@ -2264,6 +2267,7 @@
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */,
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,

View File

@ -10,7 +10,7 @@ import CoreData
enum NotificationItem {
case notification(ObjectID: NSManagedObjectID)
case notification(objectID: NSManagedObjectID)
case bottomLoader
}

View File

@ -92,7 +92,10 @@ extension NotificationSection {
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
cell.avatatImageView.gesture().sink { [weak cell] _ in
cell?.delegate?.userAvatarDidPressed(notification: notification)
}
.store(in: &cell.disposeBag)
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
cell.actionImageView.image = actionImage
}
@ -115,7 +118,10 @@ extension NotificationSection {
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
cell.avatatImageView.gesture().sink { [weak cell] _ in
cell?.delegate?.userAvatarDidPressed(notification: notification)
}
.store(in: &cell.disposeBag)
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
cell.actionImageView.image = actionImage
}
@ -399,9 +405,20 @@ extension NotificationSection {
}
}
}()
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed
if poll.expired {
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed
} else if let expiresAt = poll.expiresAt {
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
cell.pollCountdownSubscription = timestampUpdatePublisher
.sink { _ in
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
}
} else {
// assertionFailure()
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = "-"
}
cell.statusView.pollTableView.allowsSelection = !poll.expired

View File

@ -0,0 +1,93 @@
//
// UIView+Gesture.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/14.
//
import Combine
import Foundation
import UIKit
struct GesturePublisher: Publisher {
typealias Output = GestureType
typealias Failure = Never
private let view: UIView
private let gestureType: GestureType
init(view: UIView, gestureType: GestureType) {
self.view = view
self.gestureType = gestureType
}
func receive<S>(subscriber: S) where S: Subscriber,
GesturePublisher.Failure == S.Failure, GesturePublisher.Output
== S.Input
{
let subscription = GestureSubscription(
subscriber: subscriber,
view: view,
gestureType: gestureType
)
subscriber.receive(subscription: subscription)
}
}
enum GestureType {
case tap(UITapGestureRecognizer = .init())
case swipe(UISwipeGestureRecognizer = .init())
case longPress(UILongPressGestureRecognizer = .init())
case pan(UIPanGestureRecognizer = .init())
case pinch(UIPinchGestureRecognizer = .init())
case edge(UIScreenEdgePanGestureRecognizer = .init())
func get() -> UIGestureRecognizer {
switch self {
case let .tap(tapGesture):
return tapGesture
case let .swipe(swipeGesture):
return swipeGesture
case let .longPress(longPressGesture):
return longPressGesture
case let .pan(panGesture):
return panGesture
case let .pinch(pinchGesture):
return pinchGesture
case let .edge(edgePanGesture):
return edgePanGesture
}
}
}
class GestureSubscription<S: Subscriber>: Subscription where S.Input == GestureType, S.Failure == Never {
private var subscriber: S?
private var gestureType: GestureType
private var view: UIView
init(subscriber: S, view: UIView, gestureType: GestureType) {
self.subscriber = subscriber
self.view = view
self.gestureType = gestureType
configureGesture(gestureType)
}
private func configureGesture(_ gestureType: GestureType) {
let gesture = gestureType.get()
gesture.addTarget(self, action: #selector(handler))
view.addGestureRecognizer(gesture)
}
func request(_ demand: Subscribers.Demand) {}
func cancel() {
subscriber = nil
}
@objc
private func handler() {
_ = subscriber?.receive(gestureType)
}
}
extension UIView {
func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher {
self.isUserInteractionEnabled = true
return GesturePublisher(view: self, gestureType: gestureType)
}
}

View File

@ -18,7 +18,7 @@ final class NotificationViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = NotificationViewModel(context: context, coordinator: coordinator)
private(set) lazy var viewModel = NotificationViewModel(context: context)
let segmentControl: UISegmentedControl = {
let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions])
@ -136,6 +136,24 @@ extension NotificationViewController {
// MARK: - UITableViewDelegate
extension NotificationViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .notification(let objectID):
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
if notification.status != nil {
// TODO goto status detail vc
} else {
let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
}
default:
break
}
}
}
@ -147,9 +165,18 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl
}
extension NotificationViewController: NotificationTableViewCellDelegate {
func userAvatarDidPressed(notification: MastodonNotification) {
let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
}
func parent() -> UIViewController {
self
}
}
//// MARK: - UIScrollViewDelegate

View File

@ -71,7 +71,7 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
newSnapshot.appendSections([.main])
newSnapshot.appendItems(notifications.map({NotificationItem.notification(ObjectID: $0.objectID)}), toSection: .main)
newSnapshot.appendItems(notifications.map({NotificationItem.notification(objectID: $0.objectID)}), toSection: .main)
if !notifications.isEmpty {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}

View File

@ -19,7 +19,6 @@ final class NotificationViewModel: NSObject {
// input
let context: AppContext
weak var coordinator: SceneCoordinator!
weak var tableView: UITableView?
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
@ -49,8 +48,7 @@ final class NotificationViewModel: NSObject {
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
init(context: AppContext,coordinator: SceneCoordinator) {
self.coordinator = coordinator
init(context: AppContext) {
self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
self.fetchedResultsController = {

View File

@ -14,7 +14,7 @@ final class NotificationStatusTableViewCell: UITableViewCell {
static let statusPadding: UIEdgeInsets = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24)
var disposeBag = Set<AnyCancellable>()
var pollCountdownSubscription: AnyCancellable?
var delegate: NotificationTableViewCellDelegate?
let avatatImageView: UIImageView = {

View File

@ -8,11 +8,14 @@
import Combine
import Foundation
import UIKit
import CoreDataStack
protocol NotificationTableViewCellDelegate: class {
var context: AppContext! { get }
func parent() -> UIViewController
func userAvatarDidPressed(notification:MastodonNotification)
}
final class NotificationTableViewCell: UITableViewCell {