feat: make content warning works in the notification scene

This commit is contained in:
CMK 2021-04-20 13:18:27 +08:00
parent f8127428dd
commit 04d427ea93
7 changed files with 125 additions and 28 deletions

View File

@ -7,7 +7,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>10</integer> <integer>13</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>

View File

@ -9,7 +9,7 @@ import CoreData
import Foundation import Foundation
enum NotificationItem { enum NotificationItem {
case notification(objectID: NSManagedObjectID) case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
case bottomLoader case bottomLoader
} }
@ -17,7 +17,7 @@ enum NotificationItem {
extension NotificationItem: Equatable { extension NotificationItem: Equatable {
static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool { static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.notification(let idLeft), .notification(let idRight)): case (.notification(let idLeft, _), .notification(let idRight, _)):
return idLeft == idRight return idLeft == idRight
case (.bottomLoader, .bottomLoader): case (.bottomLoader, .bottomLoader):
return true return true
@ -30,7 +30,7 @@ extension NotificationItem: Equatable {
extension NotificationItem: Hashable { extension NotificationItem: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case .notification(let id): case .notification(let id, _):
hasher.combine(id) hasher.combine(id)
case .bottomLoader: case .bottomLoader:
hasher.combine(String(describing: NotificationItem.bottomLoader.self)) hasher.combine(String(describing: NotificationItem.bottomLoader.self))

View File

@ -22,15 +22,14 @@ extension NotificationSection {
timestampUpdatePublisher: AnyPublisher<Date, Never>, timestampUpdatePublisher: AnyPublisher<Date, Never>,
managedObjectContext: NSManagedObjectContext, managedObjectContext: NSManagedObjectContext,
delegate: NotificationTableViewCellDelegate, delegate: NotificationTableViewCellDelegate,
dependency: NeedsDependency, dependency: NeedsDependency
requestUserID: String
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> { ) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
UITableViewDiffableDataSource(tableView: tableView) { UITableViewDiffableDataSource(tableView: tableView) {
[weak delegate, weak dependency] [weak delegate, weak dependency]
(tableView, indexPath, notificationItem) -> UITableViewCell? in (tableView, indexPath, notificationItem) -> UITableViewCell? in
guard let dependency = dependency else { return nil } guard let dependency = dependency else { return nil }
switch notificationItem { switch notificationItem {
case .notification(let objectID): case .notification(let objectID, let attribute):
let notification = managedObjectContext.object(with: objectID) as! MastodonNotification let notification = managedObjectContext.object(with: objectID) as! MastodonNotification
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else { guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else {
@ -46,14 +45,18 @@ extension NotificationSection {
if let status = notification.status { if let status = notification.status {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
cell.delegate = delegate cell.delegate = delegate
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height) let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height)
StatusSection.configure(cell: cell, StatusSection.configure(
dependency: dependency, cell: cell,
readableLayoutFrame: frame, dependency: dependency,
timestampUpdatePublisher: timestampUpdatePublisher, readableLayoutFrame: frame,
status: status, timestampUpdatePublisher: timestampUpdatePublisher,
requestUserID: requestUserID, status: status,
statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false)) requestUserID: requestUserID,
statusItemAttribute: attribute
)
timestampUpdatePublisher timestampUpdatePublisher
.sink { _ in .sink { _ in
let timeText = notification.createAt.shortTimeAgoSinceNow let timeText = notification.createAt.shortTimeAgoSinceNow

View File

@ -36,6 +36,7 @@ final class NotificationViewController: UIViewController, NeedsDependency {
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
tableView.tableFooterView = UIView() tableView.tableFooterView = UIView()
tableView.estimatedRowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = UITableView.automaticDimension
tableView.backgroundColor = .clear
return tableView return tableView
}() }()
@ -45,13 +46,14 @@ final class NotificationViewController: UIViewController, NeedsDependency {
extension NotificationViewController { extension NotificationViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
navigationItem.titleView = segmentControl navigationItem.titleView = segmentControl
segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged)
tableView.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView) view.addSubview(tableView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
@ -65,6 +67,7 @@ extension NotificationViewController {
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self) viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self)
viewModel.viewDidLoad.send() viewModel.viewDidLoad.send()
// bind refresh control // bind refresh control
viewModel.isFetchingLatestNotification viewModel.isFetchingLatestNotification
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -83,6 +86,8 @@ extension NotificationViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
// needs trigger manually after onboarding dismiss // needs trigger manually after onboarding dismiss
setNeedsStatusBarAppearanceUpdate() setNeedsStatusBarAppearanceUpdate()
} }
@ -159,11 +164,10 @@ extension NotificationViewController {
extension NotificationViewController: UITableViewDelegate { extension NotificationViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item { switch item {
case .notification(let objectID): case .notification(let objectID, _):
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
if let status = notification.status { if let status = notification.status {
let viewModel = ThreadViewModel(context: context, optionalStatus: status) let viewModel = ThreadViewModel(context: context, optionalStatus: status)
@ -199,6 +203,7 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl
} }
} }
// MARK: - NotificationTableViewCellDelegate
extension NotificationViewController: NotificationTableViewCellDelegate { extension NotificationViewController: NotificationTableViewCellDelegate {
func userAvatarDidPressed(notification: MastodonNotification) { func userAvatarDidPressed(notification: MastodonNotification) {
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account) let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account)
@ -210,6 +215,18 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
func parent() -> UIViewController { func parent() -> UIViewController {
self self
} }
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
}
} }
// MARK: - UIScrollViewDelegate // MARK: - UIScrollViewDelegate

View File

@ -20,16 +20,13 @@ extension NotificationViewModel {
.autoconnect() .autoconnect()
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
guard let userid = activeMastodonAuthenticationBox.value?.userID else {
return
}
diffableDataSource = NotificationSection.tableViewDiffableDataSource( diffableDataSource = NotificationSection.tableViewDiffableDataSource(
for: tableView, for: tableView,
timestampUpdatePublisher: timestampUpdatePublisher, timestampUpdatePublisher: timestampUpdatePublisher,
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
delegate: delegate, delegate: delegate,
dependency: dependency, dependency: dependency
requestUserID: userid
) )
} }
} }
@ -67,9 +64,31 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
DispatchQueue.main.async { DispatchQueue.main.async {
let oldSnapshot = diffableDataSource.snapshot() let oldSnapshot = diffableDataSource.snapshot()
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
for item in oldSnapshot.itemIdentifiers {
guard case let .notification(objectID, attribute) = item else { continue }
oldSnapshotAttributeDict[objectID] = attribute
}
var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>() var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
newSnapshot.appendSections([.main]) newSnapshot.appendSections([.main])
newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main) let items: [NotificationItem] = notifications.map { notification in
let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
// let attribute: Item.StatusAttribute = {
// if let attribute = oldSnapshotAttributeDict[notification.objectID] {
// return attribute
// } else if let status = notification.status {
// let attribute = Item.StatusAttribute()
// let isSensitive = status.sensitive || !(status.spoilerText ?? "").isEmpty
// attribute.isRevealing.value = !isSensitive
// return attribute
// } else {
// return Item.StatusAttribute()
// }
// }()
return NotificationItem.notification(objectID: notification.objectID, attribute: attribute)
}
newSnapshot.appendItems(items, toSection: .main)
if !notifications.isEmpty, self.noMoreNotification.value == false { if !notifications.isEmpty, self.noMoreNotification.value == false {
newSnapshot.appendItems([.bottomLoader], toSection: .main) newSnapshot.appendItems([.bottomLoader], toSection: .main)
} }

View File

@ -8,6 +8,7 @@
import Combine import Combine
import Foundation import Foundation
import UIKit import UIKit
import ActiveLabel
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
static let actionImageBorderWidth: CGFloat = 2 static let actionImageBorderWidth: CGFloat = 2
@ -78,8 +79,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatatImageView.af.cancelImageRequest() avatatImageView.af.cancelImageRequest()
statusView.isStatusTextSensitive = false statusView.updateContentWarningDisplay(isHidden: true, animated: false)
statusView.cleanUpContentWarning()
statusView.pollTableView.dataSource = nil statusView.pollTableView.dataSource = nil
statusView.playerContainerView.reset() statusView.playerContainerView.reset()
statusView.playerContainerView.isHidden = true statusView.playerContainerView.isHidden = true
@ -99,6 +99,9 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
// precondition: app is active
guard UIApplication.shared.applicationState == .active else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self.statusView.drawContentWarningImageView() self.statusView.drawContentWarningImageView()
} }
@ -107,6 +110,8 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
extension NotificationStatusTableViewCell { extension NotificationStatusTableViewCell {
func configure() { func configure() {
backgroundColor = Asset.Colors.Background.systemBackground.color
let containerStackView = UIStackView() let containerStackView = UIStackView()
containerStackView.axis = .horizontal containerStackView.axis = .horizontal
containerStackView.alignment = .top containerStackView.alignment = .top
@ -154,7 +159,6 @@ extension NotificationStatusTableViewCell {
actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor) actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor)
]) ])
let actionStackView = UIStackView() let actionStackView = UIStackView()
actionStackView.axis = .horizontal actionStackView.axis = .horizontal
actionStackView.distribution = .fill actionStackView.distribution = .fill
@ -187,13 +191,12 @@ extension NotificationStatusTableViewCell {
statusBorder.trailingAnchor.constraint(equalTo: statusView.trailingAnchor, constant: 12), statusBorder.trailingAnchor.constraint(equalTo: statusView.trailingAnchor, constant: 12),
]) ])
statusView.delegate = self
statusStackView.addArrangedSubview(statusBorder) statusStackView.addArrangedSubview(statusBorder)
containerStackView.addArrangedSubview(statusStackView) containerStackView.addArrangedSubview(statusStackView)
statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
statusView.isUserInteractionEnabled = false
// remove item don't display // remove item don't display
statusView.actionToolbarContainer.removeFromStackView() statusView.actionToolbarContainer.removeFromStackView()
// it affect stackView's height,need remove // it affect stackView's height,need remove
@ -206,4 +209,54 @@ extension NotificationStatusTableViewCell {
statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
} }
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: highlighted)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: selected)
}
private func resetContentOverlayBlurImageBackgroundColor(selected: Bool) {
let imageViewBackgroundColor: UIColor? = selected ? selectedBackgroundView?.backgroundColor : backgroundColor
statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = imageViewBackgroundColor
}
}
// MARK: - StatusViewDelegate
extension NotificationStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
// do nothing
}
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) {
// do nothing
}
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
delegate?.notificationStatusTableViewCell(self, statusView: statusView, revealContentWarningButtonDidPressed: button)
}
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
delegate?.notificationStatusTableViewCell(self, statusView: statusView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
delegate?.notificationStatusTableViewCell(self, statusView: statusView, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
// do nothing
}
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
// do nothing
}
} }

View File

@ -16,6 +16,11 @@ protocol NotificationTableViewCellDelegate: AnyObject {
func parent() -> UIViewController func parent() -> UIViewController
func userAvatarDidPressed(notification: MastodonNotification) func userAvatarDidPressed(notification: MastodonNotification)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
} }
final class NotificationTableViewCell: UITableViewCell { final class NotificationTableViewCell: UITableViewCell {