diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 277a40f50..09f0f87a2 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -20,13 +20,19 @@ extension NotificationSection { static func tableViewDiffableDataSource( for tableView: UITableView, timestampUpdatePublisher: AnyPublisher, - managedObjectContext: NSManagedObjectContext + managedObjectContext: NSManagedObjectContext, + delegate: NotificationTableViewCellDelegate, + dependency: NeedsDependency, + requestUserID: String ) -> UITableViewDiffableDataSource { - - return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, notificationItem) -> UITableViewCell? in + return UITableViewDiffableDataSource(tableView: tableView) { + [weak delegate,weak dependency] + (tableView, indexPath, notificationItem) -> UITableViewCell? in + 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) @@ -69,7 +75,7 @@ extension NotificationSection { let timeText = notification.createAt.shortTimeAgoSinceNow cell.actionImageBackground.backgroundColor = color cell.actionLabel.text = actionText + " ยท " + timeText - cell.nameLabel.text = notification.account.displayName + 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), @@ -79,10 +85,18 @@ extension NotificationSection { if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } - if let _ = notification.status { - cell.nameLabelLayoutIn(center: true) - } else { + if let status = notification.status { + let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - 61 - 2, height: tableView.readableContentGuide.layoutFrame.height) + NotificationSection.configure(cell: cell, + dependency: dependency, + readableLayoutFrame: frame, + timestampUpdatePublisher: timestampUpdatePublisher, + status: status, + requestUserID: "", + statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false)) cell.nameLabelLayoutIn(center: false) + } else { + cell.nameLabelLayoutIn(center: true) } return cell case .bottomLoader: @@ -93,3 +107,352 @@ extension NotificationSection { } } } + + +extension NotificationSection { + static func configure( + cell: NotificationTableViewCell, + dependency: NeedsDependency, + readableLayoutFrame: CGRect?, + timestampUpdatePublisher: AnyPublisher, + status: Status, + requestUserID: String, + statusItemAttribute: Item.StatusAttribute + ) { + // disable interaction + cell.statusView.isUserInteractionEnabled = false + // remove actionToolBar + cell.statusView.actionToolbarContainer.removeFromSuperview() + // setup attribute + statusItemAttribute.setupForStatus(status: status) + + // set header + NotificationSection.configureHeader(cell: cell, status: status) + ManagedObjectObserver.observe(object: status) + .receive(on: DispatchQueue.main) + .sink { _ in + // do nothing + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newStatus = object as? Status else { return } + NotificationSection.configureHeader(cell: cell, status: newStatus) + } + .store(in: &cell.disposeBag) + + // set name username + cell.statusView.nameLabel.text = { + let author = (status.reblog ?? status).author + return author.displayName.isEmpty ? author.username : author.displayName + }() + cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct + // set avatar + + cell.statusView.avatarButton.isHidden = false + cell.statusView.avatarStackedContainerButton.isHidden = true + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) + + + // set text + cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) + + // set status text content warning + let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false + let spoilerText = (status.reblog ?? status).spoilerText ?? "" + cell.statusView.isStatusTextSensitive = isStatusTextSensitive + cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) + cell.statusView.contentWarningTitle.text = { + if spoilerText.isEmpty { + return L10n.Common.Controls.Status.statusContentWarning + } else { + return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)" + } + }() + + // prepare media attachments + let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } + + // set image + let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) + let imageViewMaxSize: CGSize = { + let maxWidth: CGFloat = { + // use timelinePostView width as container width + // that width follows readable width and keep constant width after rotate + let containerFrame = readableLayoutFrame ?? cell.statusView.frame + var containerWidth = containerFrame.width + containerWidth -= 10 + containerWidth -= StatusView.avatarImageSize.width + return containerWidth + }() + let scale: CGFloat = { + switch mosiacImageViewModel.metas.count { + case 1: return 1.3 + default: return 0.7 + } + }() + return CGSize(width: maxWidth, height: maxWidth * scale) + }() + if mosiacImageViewModel.metas.count == 1 { + let meta = mosiacImageViewModel.metas[0] + let imageView = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } else { + let imageViews = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + for (i, imageView) in imageViews.enumerated() { + let meta = mosiacImageViewModel.metas[i] + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } + } + cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty + let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive + + // set audio + if let _ = mediaAttachments.filter({ $0.type == .audio }).first { + cell.statusView.audioView.isHidden = false + cell.statusView.audioView.playButton.isSelected = false + cell.statusView.audioView.slider.isEnabled = false + cell.statusView.audioView.slider.setValue(0, animated: false) + } else { + cell.statusView.audioView.isHidden = true + } + + // set GIF & video + let playerViewMaxSize: CGSize = { + let maxWidth: CGFloat = { + // use statusView width as container width + // that width follows readable width and keep constant width after rotate + let containerFrame = readableLayoutFrame ?? cell.statusView.frame + return containerFrame.width + }() + let scale: CGFloat = 1.3 + return CGSize(width: maxWidth, height: maxWidth * scale) + }() + + cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil + cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive + + if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, + let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) + { + let parent = cell.delegate?.parent() + let playerContainerView = cell.statusView.playerContainerView + let playerViewController = playerContainerView.setupPlayer( + aspectRatio: videoPlayerViewModel.videoSize, + maxSize: playerViewMaxSize, + parent: parent + ) + playerViewController.player = videoPlayerViewModel.player + playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif + playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) + if videoPlayerViewModel.videoKind == .gif { + playerContainerView.setMediaIndicator(isHidden: false) + } else { + videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in + UIView.animate(withDuration: 0.33) { + switch timeControlStatus { + case .playing: + playerContainerView.setMediaIndicator(isHidden: true) + case .paused, .waitingToPlayAtSpecifiedRate: + playerContainerView.setMediaIndicator(isHidden: false) + @unknown default: + assertionFailure() + } + } + } + .store(in: &cell.disposeBag) + } + playerContainerView.isHidden = false + + } else { + cell.statusView.playerContainerView.playerViewController.player?.pause() + cell.statusView.playerContainerView.playerViewController.player = nil + } + // set poll + let poll = (status.reblog ?? status).poll + NotificationSection.configurePoll( + cell: cell, + poll: poll, + requestUserID: requestUserID, + updateProgressAnimated: false, + timestampUpdatePublisher: timestampUpdatePublisher + ) + if let poll = poll { + ManagedObjectObserver.observe(object: poll) + .sink { _ in + // do nothing + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newPoll = object as? Poll else { return } + NotificationSection.configurePoll( + cell: cell, + poll: newPoll, + requestUserID: requestUserID, + updateProgressAnimated: true, + timestampUpdatePublisher: timestampUpdatePublisher + ) + } + .store(in: &cell.disposeBag) + } + + // set date + let createdAt = (status.reblog ?? status).createdAt + cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + timestampUpdatePublisher + .sink { _ in + cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + } + .store(in: &cell.disposeBag) + + } + + static func configureHeader( + cell: NotificationTableViewCell, + status: Status + ) { + if status.reblog != nil { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) + cell.statusView.headerInfoLabel.text = { + let author = status.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userReblogged(name) + }() + } else if let replyTo = status.replyTo { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerInfoLabel.text = { + let author = replyTo.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userRepliedTo(name) + }() + } else { + cell.statusView.headerContainerStackView.isHidden = true + } + } + + + static func configurePoll( + cell: NotificationTableViewCell, + poll: Poll?, + requestUserID: String, + updateProgressAnimated: Bool, + timestampUpdatePublisher: AnyPublisher + ) { + guard let poll = poll, + let managedObjectContext = poll.managedObjectContext + else { + cell.statusView.pollTableView.isHidden = true + cell.statusView.pollStatusStackView.isHidden = true + cell.statusView.pollVoteButton.isHidden = true + return + } + + cell.statusView.pollTableView.isHidden = false + cell.statusView.pollStatusStackView.isHidden = false + cell.statusView.pollVoteCountLabel.text = { + if poll.multiple { + let count = poll.votersCount?.intValue ?? 0 + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoterCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoterCount.multiple(count) + } + } else { + let count = poll.votesCount.intValue + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoteCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoteCount.multiple(count) + } + } + }() + + cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed + + + cell.statusView.pollTableView.allowsSelection = !poll.expired + + let votedOptions = poll.options.filter { option in + (option.votedBy ?? Set()).map(\.id).contains(requestUserID) + } + let didVotedLocal = !votedOptions.isEmpty + let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID) + cell.statusView.pollVoteButton.isEnabled = didVotedLocal + cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired) + + cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource( + for: cell.statusView.pollTableView, + managedObjectContext: managedObjectContext + ) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + let pollItems = poll.options + .sorted(by: { $0.index.intValue < $1.index.intValue }) + .map { option -> PollItem in + let attribute: PollItem.Attribute = { + let selectState: PollItem.Attribute.SelectState = { + // check didVotedRemote later to make the local change possible + if !votedOptions.isEmpty { + return votedOptions.contains(option) ? .on : .off + } else if poll.expired { + return .none + } else if didVotedRemote, votedOptions.isEmpty { + return .none + } else { + return .off + } + }() + let voteState: PollItem.Attribute.VoteState = { + var needsReveal: Bool + if poll.expired { + needsReveal = true + } else if didVotedRemote { + needsReveal = true + } else { + needsReveal = false + } + guard needsReveal else { return .hidden } + let percentage: Double = { + guard poll.votesCount.intValue > 0 else { return 0.0 } + return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue) + }() + let voted = votedOptions.isEmpty ? true : votedOptions.contains(option) + return .reveal(voted: voted, percentage: percentage, animated: updateProgressAnimated) + }() + return PollItem.Attribute(selectState: selectState, voteState: voteState) + }() + let option = PollItem.opion(objectID: option.objectID, attribute: attribute) + return option + } + snapshot.appendItems(pollItems, toSection: .main) + cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + } + + static func configureEmptyStateHeader( + cell: TimelineHeaderTableViewCell, + attribute: Item.EmptyStateHeaderAttribute + ) { + cell.timelineHeaderView.iconImageView.image = attribute.reason.iconImage + cell.timelineHeaderView.messageLabel.text = attribute.reason.message + } +} + +extension NotificationSection { + private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { + guard let number = number, number > 0 else { return "" } + return String(number) + } +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index ef7ae9292..a9963ce9c 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -53,6 +53,7 @@ internal enum Asset { internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background") } internal enum Border { + internal static let notification = ColorAsset(name: "Colors/Border/notification") internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard") } internal enum Button { diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json new file mode 100644 index 000000000..afc18df10 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE8", + "green" : "0xE1", + "red" : "0xD9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "60", + "green" : "58", + "red" : "58" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index b87172925..5a7009270 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -8,6 +8,8 @@ import UIKit import Combine import OSLog +import CoreData +import CoreDataStack final class NotificationViewController: UIViewController, NeedsDependency { @@ -58,7 +60,7 @@ extension NotificationViewController { tableView.delegate = self viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self - viewModel.setupDiffableDataSource(for: tableView) + viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self) viewModel.viewDidLoad.send() // bind refresh control viewModel.isFetchingLatestNotification @@ -125,10 +127,6 @@ extension NotificationViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return 68 } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - 68 - } } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate @@ -138,6 +136,12 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl } } +extension NotificationViewController: NotificationTableViewCellDelegate { + func parent() -> UIViewController { + self + } +} + //// MARK: - UIScrollViewDelegate //extension NotificationViewController { // func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index 1d77d41b4..c9c0dcf6f 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -13,17 +13,24 @@ import CoreDataStack extension NotificationViewModel { func setupDiffableDataSource( - for tableView: UITableView + for tableView: UITableView, + delegate: NotificationTableViewCellDelegate, + dependency: NeedsDependency ) { let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() - + guard let userid = activeMastodonAuthenticationBox.value?.userID else { + return + } diffableDataSource = NotificationSection.tableViewDiffableDataSource( for: tableView, timestampUpdatePublisher: timestampUpdatePublisher, - managedObjectContext: context.managedObjectContext + managedObjectContext: context.managedObjectContext, + delegate: delegate, + dependency: dependency, + requestUserID: userid ) } diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 1b4890317..b8d74152c 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -85,7 +85,12 @@ final class NotificationViewModel: NSObject { guard let self = self else { return } self.fetchedResultsController.fetchRequest.predicate = predicate do { + self.diffableDataSource?.defaultRowAnimation = .fade try self.fetchedResultsController.performFetch() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + guard let self = self else { return } + self.diffableDataSource?.defaultRowAnimation = .automatic + } } catch { assertionFailure(error.localizedDescription) } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index b252b76d8..d7decfddf 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -5,17 +5,23 @@ // Created by sxiaojian on 2021/4/13. // +import Combine import Foundation import UIKit -import Combine +protocol NotificationTableViewCellDelegate: class { + var context: AppContext! { get } + + func parent() -> UIViewController +} final class NotificationTableViewCell: UITableViewCell { - static let actionImageBorderWidth: CGFloat = 2 var disposeBag = Set() + var delegate: NotificationTableViewCellDelegate? + let avatatImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 4 @@ -32,7 +38,7 @@ final class NotificationTableViewCell: UITableViewCell { let actionImageBackground: UIView = { let view = UIView() - view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth)/2 + view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth) / 2 view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth @@ -58,11 +64,30 @@ final class NotificationTableViewCell: UITableViewCell { }() 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() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -74,11 +99,19 @@ final class NotificationTableViewCell: UITableViewCell { super.init(coder: coder) configure() } + + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } } extension NotificationTableViewCell { - 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) @@ -90,7 +123,8 @@ extension NotificationTableViewCell { actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor) + nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24) + nameLabelBottom = contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24) contentView.addSubview(nameLabel) nameLabel.constrain([ nameLabelTop, @@ -100,21 +134,38 @@ extension NotificationTableViewCell { contentView.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), - actionLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor), - contentView.trailingAnchor.constraint(equalTo: actionLabel.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 + } 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) - - self.actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor } }