diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b2258c1df..150c29afb 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; 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 */; }; 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 */; }; @@ -431,6 +432,7 @@ 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; + 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; @@ -898,6 +900,7 @@ isa = PBXGroup; children = ( 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */, + 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -2363,6 +2366,7 @@ DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, + 2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */, DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 09f0f87a2..de7f5d6c8 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -31,11 +31,12 @@ extension NotificationSection { guard let dependency = dependency else { return nil } switch notificationItem { case .notification(let objectID): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell - cell.delegate = delegate + let notification = managedObjectContext.object(with: objectID) as! MastodonNotification let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type) + let timeText = notification.createAt.shortTimeAgoSinceNow + var actionText: String var actionImageName: String var color: UIColor @@ -66,39 +67,59 @@ extension NotificationSection { color = .clear } - timestampUpdatePublisher - .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow - cell.actionLabel.text = actionText + " · " + timeText - } - .store(in: &cell.disposeBag) - let timeText = notification.createAt.shortTimeAgoSinceNow - cell.actionImageBackground.backgroundColor = color - cell.actionLabel.text = actionText + " · " + timeText - cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName - cell.avatatImageView.af.setImage( - withURL: URL(string: notification.account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - - if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { - cell.actionImageView.image = actionImage - } if let status = notification.status { - let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - 61 - 2, height: tableView.readableContentGuide.layoutFrame.height) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell + cell.delegate = delegate NotificationSection.configure(cell: cell, dependency: dependency, - readableLayoutFrame: frame, + readableLayoutFrame: nil, timestampUpdatePublisher: timestampUpdatePublisher, status: status, - requestUserID: "", + requestUserID: requestUserID, statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false)) - cell.nameLabelLayoutIn(center: false) + timestampUpdatePublisher + .sink { _ in + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionLabel.text = actionText + " · " + timeText + } + .store(in: &cell.disposeBag) + cell.actionImageBackground.backgroundColor = color + cell.actionLabel.text = actionText + " · " + timeText + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName + cell.avatatImageView.af.setImage( + withURL: URL(string: notification.account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + + if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { + cell.actionImageView.image = actionImage + } + return cell + } else { - cell.nameLabelLayoutIn(center: true) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + cell.delegate = delegate + timestampUpdatePublisher + .sink { _ in + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionLabel.text = actionText + " · " + timeText + } + .store(in: &cell.disposeBag) + cell.actionImageBackground.backgroundColor = color + cell.actionLabel.text = actionText + " · " + timeText + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName + cell.avatatImageView.af.setImage( + withURL: URL(string: notification.account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + + if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { + cell.actionImageView.image = actionImage + } + return cell } - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader cell.startAnimating() @@ -111,7 +132,7 @@ extension NotificationSection { extension NotificationSection { static func configure( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, dependency: NeedsDependency, readableLayoutFrame: CGRect?, timestampUpdatePublisher: AnyPublisher, @@ -317,7 +338,7 @@ extension NotificationSection { } static func configureHeader( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, status: Status ) { if status.reblog != nil { @@ -343,7 +364,7 @@ extension NotificationSection { static func configurePoll( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, poll: Poll?, requestUserID: String, updateProgressAnimated: Bool, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 2c96c65d6..d047a0a5d 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -32,6 +32,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) tableView.tableFooterView = UIView() return tableView diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift new file mode 100644 index 000000000..1a0df283e --- /dev/null +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -0,0 +1,147 @@ +// +// NotificationStatusTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/14. +// + +import Combine +import Foundation +import UIKit + +final class NotificationStatusTableViewCell: UITableViewCell { + static let actionImageBorderWidth: CGFloat = 2 + + var disposeBag = Set() + + var delegate: NotificationTableViewCellDelegate? + + let avatatImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.cornerCurve = .continuous + imageView.clipsToBounds = true + return imageView + }() + + let actionImageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = Asset.Colors.Background.searchResult.color + return imageView + }() + + let actionImageBackground: UIView = { + let view = UIView() + view.layer.cornerRadius = (24 + NotificationStatusTableViewCell.actionImageBorderWidth) / 2 + view.layer.cornerCurve = .continuous + view.clipsToBounds = true + view.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth + view.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + view.tintColor = Asset.Colors.Background.searchResult.color + return view + }() + + let actionLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = UIFont.preferredFont(forTextStyle: .body) + label.lineBreakMode = .byTruncatingTail + return label + }() + + let nameLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.brandBlue.color + label.font = .systemFont(ofSize: 15, weight: .semibold) + label.lineBreakMode = .byTruncatingTail + return label + }() + + let statusContainer: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.cornerRadius = 6 + view.layer.borderWidth = 2 + view.layer.cornerCurve = .continuous + view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + view.clipsToBounds = true + return view + }() + + let statusView = StatusView() + + override func prepareForReuse() { + super.prepareForReuse() + avatatImageView.af.cancelImageRequest() + statusView.isStatusTextSensitive = false + statusView.cleanUpContentWarning() + statusView.pollTableView.dataSource = nil + statusView.playerContainerView.reset() + statusView.playerContainerView.isHidden = true + disposeBag.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } +} + +extension NotificationStatusTableViewCell { + func configure() { + selectionStyle = .none + + contentView.addSubview(avatatImageView) + avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) + avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) + + contentView.addSubview(actionImageBackground) + actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationStatusTableViewCell.actionImageBorderWidth, height: 24 + NotificationStatusTableViewCell.actionImageBorderWidth)) + actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) + + actionImageBackground.addSubview(actionImageView) + actionImageView.constrainToCenter() + + contentView.addSubview(nameLabel) + nameLabel.constrain([ + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) + ]) + + contentView.addSubview(actionLabel) + actionLabel.constrain([ + actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), + actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), + contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + ]) + + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + addStatusAndContainer() + } + + func addStatusAndContainer() { + contentView.addSubview(statusContainer) + statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) + + contentView.addSubview(statusView) + statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + } +} diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index d7decfddf..5124dab63 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -63,30 +63,9 @@ final class NotificationTableViewCell: UITableViewCell { return label }() - var nameLabelTop: NSLayoutConstraint! - var nameLabelBottom: NSLayoutConstraint! - - let statusContainer: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.layer.cornerRadius = 6 - view.layer.borderWidth = 2 - view.layer.cornerCurve = .continuous - view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor - view.clipsToBounds = true - return view - }() - - let statusView = StatusView() - override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() - statusView.isStatusTextSensitive = false - statusView.cleanUpContentWarning() - statusView.pollTableView.dataSource = nil - statusView.playerContainerView.reset() - statusView.playerContainerView.isHidden = true disposeBag.removeAll() } @@ -99,13 +78,6 @@ final class NotificationTableViewCell: UITableViewCell { super.init(coder: coder) configure() } - - override func layoutSubviews() { - super.layoutSubviews() - DispatchQueue.main.async { - self.statusView.drawContentWarningImageView() - } - } } extension NotificationTableViewCell { @@ -122,12 +94,11 @@ extension NotificationTableViewCell { actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - - nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24) - nameLabelBottom = contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24) + contentView.addSubview(nameLabel) nameLabel.constrain([ - nameLabelTop, + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), + contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) ]) @@ -137,35 +108,11 @@ extension NotificationTableViewCell { actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) ]) - - statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color - } - public func nameLabelLayoutIn(center: Bool) { - if center { - nameLabelTop.constant = 24 - NSLayoutConstraint.activate([nameLabelBottom]) - statusView.removeFromSuperview() - statusContainer.removeFromSuperview() - } else { - nameLabelTop.constant = 12 - NSLayoutConstraint.deactivate([nameLabelBottom]) - addStatusAndContainer() - } - } - - func addStatusAndContainer() { - contentView.addSubview(statusContainer) - statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) - - contentView.addSubview(statusView) - statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) - } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor } }