diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e37d0aa1..2a8f2a18 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -125,6 +125,7 @@ D646DCD62A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD52A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift */; }; D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; }; D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; }; + D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; }; D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; @@ -525,6 +526,7 @@ D646DCD52A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowNotificationGroupCollectionViewCell.swift; sourceTree = ""; }; D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = ""; }; D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = ""; }; + D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = ""; }; D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; @@ -1084,6 +1086,7 @@ D646DCD52A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift */, D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */, D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */, + D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */, ); path = Notifications; sourceTree = ""; @@ -2242,6 +2245,7 @@ D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */, D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */, + D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 339c038f..340ad9d3 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -99,6 +99,10 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle cell.delegate = self cell.updateUI(notification: itemIdentifier) } + let updateCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, itemIdentifier in + cell.delegate = self + cell.updateUI(notification: itemIdentifier) + } let unknownCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in var config = cell.defaultContentConfiguration() config.text = "Unknown Notification" @@ -118,6 +122,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle return collectionView.dequeueConfiguredReusableCell(using: followRequestCell, for: indexPath, item: group.notifications.first!) case .poll: return collectionView.dequeueConfiguredReusableCell(using: pollCell, for: indexPath, item: group.notifications.first!) + case .update: + return collectionView.dequeueConfiguredReusableCell(using: updateCell, for: indexPath, item: group.notifications.first!) default: return collectionView.dequeueConfiguredReusableCell(using: unknownCell, for: indexPath, item: ()) } diff --git a/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift b/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift new file mode 100644 index 00000000..873931e7 --- /dev/null +++ b/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift @@ -0,0 +1,191 @@ +// +// StatusUpdatedNotificationCollectionViewCell.swift +// Tusker +// +// Created by Shadowfacts on 5/7/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm +import SwiftSoup + +class StatusUpdatedNotificationCollectionViewCell: UICollectionViewCell { + + private let iconView = UIImageView(image: UIImage(systemName: "pencil")).configure { + $0.contentMode = .scaleAspectFit + NSLayoutConstraint.activate([ + $0.heightAnchor.constraint(equalToConstant: 30), + $0.widthAnchor.constraint(equalToConstant: 30), + ]) + } + + private let descriptionLabel = UILabel().configure { + $0.text = "A post was edited" + $0.font = .preferredFont(forTextStyle: .body) + $0.adjustsFontForContentSizeCategory = true + $0.setContentHuggingPriority(.init(249), for: .horizontal) + } + + private let timestampLabel = UILabel().configure { + $0.textColor = .secondaryLabel + $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ + .traits: [ + UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, + ] + ]), size: 0) + $0.adjustsFontForContentSizeCategory = true + } + + private lazy var hStack = UIStackView(arrangedSubviews: [ + descriptionLabel, + timestampLabel, + ]).configure { + $0.axis = .horizontal + $0.alignment = .fill + } + + private let displayNameLabel = EmojiLabel().configure { + $0.textColor = .secondaryLabel + $0.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! + $0.adjustsFontForContentSizeCategory = true + $0.numberOfLines = 2 + $0.lineBreakMode = .byTruncatingTail + } + + private let contentLabel = UILabel().configure { + $0.textColor = .secondaryLabel + $0.font = .preferredFont(forTextStyle: .body) + $0.adjustsFontForContentSizeCategory = true + $0.numberOfLines = 2 + $0.lineBreakMode = .byTruncatingTail + } + + private lazy var vStack = UIStackView(arrangedSubviews: [ + hStack, + displayNameLabel, + contentLabel, + ]).configure { + $0.axis = .vertical + $0.alignment = .fill + } + + weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? + private var mastodonController: MastodonController { delegate!.apiController } + + private var notification: Pachyderm.Notification! + + private var updateTimestampWorkItem: DispatchWorkItem? + + deinit { + updateTimestampWorkItem?.cancel() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + iconView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(iconView) + vStack.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(vStack) + NSLayoutConstraint.activate([ + iconView.topAnchor.constraint(equalTo: vStack.topAnchor), + iconView.trailingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + 50), + + vStack.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8), + vStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + vStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), + ]) + + NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateConfiguration(using state: UICellConfigurationState) { + backgroundConfiguration = .appListPlainCell(for: state) + } + + func updateUI(notification: Pachyderm.Notification) { + guard notification.kind == .update, + let status = notification.status, + let account = mastodonController.persistentContainer.account(for: notification.account.id) else { + return + } + self.notification = notification + + updateTimestamp() + updateDisplayName(account: account) + + // todo: use htmlconverter + let doc = try! SwiftSoup.parseBodyFragment(status.content) + contentLabel.text = try! doc.text() + } + + @objc private func updateUIForPreferences() { + if let account = mastodonController.persistentContainer.account(for: notification.account.id) { + updateDisplayName(account: account) + } + } + + private func updateDisplayName(account: AccountMO) { + if Preferences.shared.hideCustomEmojiInUsernames { + displayNameLabel.text = account.displayNameWithoutCustomEmoji + displayNameLabel.removeEmojis() + } else { + displayNameLabel.text = account.displayOrUserName + displayNameLabel.setEmojis(account.emojis, identifier: account.id) + } + } + + private func updateTimestamp() { + guard let notification else { return } + + timestampLabel.text = notification.createdAt.timeAgoString() + + let delay: DispatchTimeInterval? + switch notification.createdAt.timeAgo().1 { + case .second: + delay = .seconds(10) + case .minute: + delay = .seconds(60) + default: + delay = nil + } + if let delay = delay { + if updateTimestampWorkItem == nil { + updateTimestampWorkItem = DispatchWorkItem { [weak self] in + self?.updateTimestamp() + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) + } + } else { + updateTimestampWorkItem = nil + } + } + + override func prepareForReuse() { + super.prepareForReuse() + updateTimestampWorkItem?.cancel() + } + + // MARK: Accessibility + + override var accessibilityLabel: String? { + get { + guard let notification else { return nil } + var str = "Post from " + str += notification.account.displayNameWithoutCustomEmoji + str += " edited " + str += notification.createdAt.formatted(.relative(presentation: .numeric)) + str += ", " + str += contentLabel.text ?? "" + return str + } + set {} + } + +}