// // NotificationSection.swift // Mastodon // // Created by sxiaojian on 2021/4/13. // import Combine import CoreData import CoreDataStack import Foundation import MastodonSDK import UIKit import MetaTextKit import MastodonMeta enum NotificationSection: Equatable, Hashable { case main } extension NotificationSection { static func tableViewDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, delegate: NotificationTableViewCellDelegate, statusTableViewCellDelegate: StatusTableViewCellDelegate ) -> UITableViewDiffableDataSource { 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 attribute): guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification, !notification.isDeleted else { return UITableViewCell() } let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell configure( tableView: tableView, cell: cell, notification: notification, dependency: dependency, attribute: attribute ) cell.delegate = delegate cell.isAccessibilityElement = true NotificationSection.configureStatusAccessibilityLabel(cell: cell) return cell case .notificationStatus(objectID: let objectID, attribute: let attribute): guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification, !notification.isDeleted, let status = notification.status, let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID else { return UITableViewCell() } let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell // configure cell StatusSection.configureStatusTableViewCell( cell: cell, tableView: tableView, timelineContext: .notifications, dependency: dependency, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, status: status, requestUserID: requestUserID, statusItemAttribute: attribute ) cell.statusView.headerContainerView.isHidden = true // set header hide cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false cell.delegate = statusTableViewCellDelegate cell.isAccessibilityElement = true StatusSection.configureStatusAccessibilityLabel(cell: cell) return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell cell.startAnimating() return cell } } } } extension NotificationSection { static func configure( tableView: UITableView, cell: NotificationStatusTableViewCell, notification: MastodonNotification, dependency: NeedsDependency, attribute: Item.StatusAttribute ) { // configure author cell.configure( with: AvatarConfigurableViewConfiguration( avatarImageURL: notification.account.avatarImageURL() ) ) func createActionImage() -> UIImage? { return UIImage( systemName: notification.notificationType.actionImageName, withConfiguration: UIImage.SymbolConfiguration( pointSize: 12, weight: .semibold ) )? .withTintColor(.systemBackground) .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14)) } cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color cell.avatarButton.badgeImageView.image = createActionImage() cell.traitCollectionDidChange .receive(on: DispatchQueue.main) .sink { [weak cell] in guard let cell = cell else { return } cell.avatarButton.badgeImageView.image = createActionImage() } .store(in: &cell.disposeBag) // configure author name, notification description, timestamp let nameText = notification.account.displayNameWithFallback let titleLabelText: String = { switch notification.notificationType { case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText) case .follow: return L10n.Scene.Notification.userFollowedYou(nameText) case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText) case .mention: return L10n.Scene.Notification.userMentionedYou(nameText) case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText) case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText) default: return "" } }() do { let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta) let nameMetaContent = try MastodonMetaContent.convert(document: nameContent) let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta) let metaContent = try MastodonMetaContent.convert(document: mastodonContent) cell.titleLabel.configure(content: metaContent) if let nameRange = metaContent.string.range(of: nameMetaContent.string) { let nsRange = NSRange(nameRange, in: metaContent.string) cell.titleLabel.textStorage.addAttributes([ .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20), .foregroundColor: Asset.Colors.brandBlue.color, ], range: nsRange) } } catch { let metaContent = PlaintextMetaContent(string: titleLabelText) cell.titleLabel.configure(content: metaContent) } let createAt = notification.createAt cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow AppContext.shared.timestampUpdatePublisher .receive(on: DispatchQueue.main) .sink { [weak cell] _ in guard let cell = cell else { return } cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow } .store(in: &cell.disposeBag) // configure follow request (if exist) if case .followRequest = notification.notificationType { cell.acceptButton.publisher(for: .touchUpInside) .sink { [weak cell] _ in guard let cell = cell else { return } cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton) } .store(in: &cell.disposeBag) cell.rejectButton.publisher(for: .touchUpInside) .sink { [weak cell] _ in guard let cell = cell else { return } cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton) } .store(in: &cell.disposeBag) cell.buttonStackView.isHidden = false } else { cell.buttonStackView.isHidden = true } // configure status (if exist) if let status = notification.status { 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, tableView: tableView, timelineContext: .notifications, dependency: dependency, readableLayoutFrame: frame, status: status, requestUserID: notification.userID, statusItemAttribute: attribute ) cell.statusContainerView.isHidden = false cell.containerStackView.alignment = .top cell.containerStackViewBottomLayoutConstraint.constant = 0 } else { if case .followRequest = notification.notificationType { cell.containerStackView.alignment = .top } else { cell.containerStackView.alignment = .center } cell.statusContainerView.isHidden = true cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view } } static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) { // FIXME: cell.accessibilityLabel = { var accessibilityViews: [UIView?] = [] accessibilityViews.append(contentsOf: [ cell.titleLabel, cell.timestampLabel, cell.statusView ]) if !cell.statusContainerView.isHidden { if !cell.statusView.headerContainerView.isHidden { accessibilityViews.append(cell.statusView.headerInfoLabel) } accessibilityViews.append(contentsOf: [ cell.statusView.nameMetaLabel, cell.statusView.dateLabel, cell.statusView.contentMetaText.textView, ]) } return accessibilityViews .compactMap { $0?.accessibilityLabel } .joined(separator: " ") }() } }