fix: make link works in notification scene

This commit is contained in:
CMK 2021-07-01 18:57:05 +08:00
parent 205cc39af3
commit bdf5a7e859
10 changed files with 113 additions and 200 deletions

View File

@ -296,6 +296,7 @@
DB6180F626391D580018D199 /* MediaPreviewableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */; };
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */; };
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */; };
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; };
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
@ -921,6 +922,7 @@
DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewableViewController.swift; sourceTree = "<group>"; };
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewingViewController.swift; sourceTree = "<group>"; };
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewController+StatusProvider.swift"; sourceTree = "<group>"; };
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
@ -2369,6 +2371,7 @@
isa = PBXGroup;
children = (
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */,
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */,
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */,
@ -3177,6 +3180,7 @@
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */,
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */,
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>21</integer>
<integer>27</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>20</integer>
<integer>21</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -60,7 +60,7 @@ extension NotificationSection {
statusItemAttribute: attribute
)
cell.actionImageBackground.backgroundColor = color
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
cell.actionLabel.text = actionText + " · " + timeText
timestampUpdatePublisher
.sink { [weak cell] _ in
@ -109,15 +109,15 @@ extension NotificationSection {
.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.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
if let url = notification.account.avatarImageURL() {
cell.avatatImageView.af.setImage(
cell.avatarImageView.af.setImage(
withURL: url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
cell.avatatImageView.gesture().sink { [weak cell] _ in
cell.avatarImageView.gesture().sink { [weak cell] _ in
cell?.delegate?.userAvatarDidPressed(notification: notification)
}
.store(in: &cell.disposeBag)

View File

@ -523,32 +523,6 @@ extension StatusProviderFacade {
extension StatusProviderFacade {
static func responseToStatusContentWarningRevealAction(dependency: NotificationViewController, cell: UITableViewCell) {
let status = Future<Status?, Never> { promise in
guard let diffableDataSource = dependency.viewModel.diffableDataSource,
let indexPath = dependency.tableView.indexPath(for: cell),
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
promise(.success(nil))
return
}
switch item {
case .notification(let objectID, _):
dependency.viewModel.fetchedResultsController.managedObjectContext.perform {
let notification = dependency.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification
promise(.success(notification.status))
}
default:
promise(.success(nil))
}
}
_responseToStatusContentWarningRevealAction(
dependency: dependency,
status: status
)
}
static func responseToStatusContentWarningRevealAction(provider: StatusProvider, cell: UITableViewCell) {
_responseToStatusContentWarningRevealAction(
dependency: provider,

View File

@ -12,7 +12,7 @@ final class AutoCompleteTableViewCell: UITableViewCell {
static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4
static let avatarToLabelSpacing: CGFloat = 12
let containerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
@ -47,12 +47,6 @@ final class AutoCompleteTableViewCell: UITableViewCell {
let separatorLine = UIView.separatorLine
override func prepareForReuse() {
super.prepareForReuse()
avatarImageView.af.cancelImageRequest()
avatarImageView.kf.cancelDownloadTask()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()

View File

@ -9,12 +9,11 @@ import os.log
import UIKit
import Combine
import PhotosUI
import Kingfisher
import MastodonSDK
import TwitterTextEditor
import MetaTextView
import MastodonMeta
import Meta
import Nuke
final class ComposeViewController: UIViewController, NeedsDependency {
@ -788,147 +787,6 @@ extension ComposeViewController: UITextViewDelegate {
}
// MARK: - TextEditorViewTextAttributesDelegate
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
func textEditorView(
_ textEditorView: TextEditorView,
updateAttributedString attributedString: NSAttributedString,
completion: @escaping (NSAttributedString?) -> Void
) {
// FIXME: needs O(1) update completion to fix performance issue
DispatchQueue.global().async {
let string = attributedString.string
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
let stringRange = NSRange(location: 0, length: string.length)
let highlightMatches = string.matches(pattern: MastodonRegex.highlightPattern)
let emojiMatches = string.matches(pattern: MastodonRegex.emojiPattern)
// only accept http/https scheme
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
DispatchQueue.main.async { [weak self] in
guard let self = self else {
completion(nil)
return
}
let customEmojiViewModel = self.viewModel.customEmojiViewModel.value
for view in self.suffixedAttachmentViews {
view.removeFromSuperview()
}
self.suffixedAttachmentViews.removeAll()
// set normal appearance
let attributedString = NSMutableAttributedString(attributedString: attributedString)
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
attributedString.removeAttribute(.underlineStyle, range: stringRange)
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
// hashtag
for match in highlightMatches {
// set highlight
var attributes = [NSAttributedString.Key: Any]()
attributes[.foregroundColor] = Asset.Colors.brandBlue.color
// See `traitCollectionDidChange(_:)`
// set accessibility
if #available(iOS 13.0, *) {
switch self.traitCollection.accessibilityContrast {
case .high:
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
default:
break
}
}
attributedString.addAttributes(attributes, range: match.range)
}
// emoji
if let customEmojiViewModel = customEmojiViewModel, !customEmojiViewModel.emojiDict.value.isEmpty {
for match in emojiMatches {
guard let name = string.substring(with: match, at: 2) else { continue }
guard let emoji = customEmojiViewModel.emoji(shortcode: name) else { continue }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
// set emoji token invisible (without upper bounce space)
var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = UIFont.systemFont(ofSize: 0.01)
attributedString.addAttributes(attributes, range: match.range)
// append emoji attachment
let imageViewSize = CGSize(width: 20, height: 20)
let imageView = UIImageView(frame: CGRect(origin: .zero, size: imageViewSize))
textEditorView.textContentView.addSubview(imageView)
self.suffixedAttachmentViews.append(imageView)
let processor = DownsamplingImageProcessor(size: imageViewSize)
imageView.kf.setImage(
with: URL(string: emoji.url),
placeholder: UIImage.placeholder(size: imageViewSize, color: .systemFill),
options: [
.processor(processor),
.scaleFactor(textEditorView.traitCollection.displayScale),
], completionHandler: nil
)
let layoutInTextContainer = { [weak textEditorView] (view: UIView, frame: CGRect) in
// `textEditorView` retains `textStorage`, which retains this block as a part of attributes.
guard let textEditorView = textEditorView else {
return
}
let insets = textEditorView.textContentInsets
view.frame = frame.offsetBy(dx: insets.left, dy: insets.top)
}
let attachment = TextAttributes.SuffixedAttachment(
size: imageViewSize,
attachment: .view(view: imageView, layoutInTextContainer: layoutInTextContainer)
)
let index = match.range.upperBound - 1
attributedString.addAttribute(
.suffixedAttachment,
value: attachment,
range: NSRange(location: index, length: 1)
)
}
}
// url
for match in urlMatches {
guard let name = string.substring(with: match, at: 0) else { continue }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
// set highlight
var attributes = [NSAttributedString.Key: Any]()
attributes[.foregroundColor] = Asset.Colors.brandBlue.color
// See `traitCollectionDidChange(_:)`
// set accessibility
if #available(iOS 13.0, *) {
switch self.traitCollection.accessibilityContrast {
case .high:
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
default:
break
}
}
attributedString.addAttributes(attributes, range: match.range)
}
if string.count > ComposeViewModel.composeContentLimit {
var attributes = [NSAttributedString.Key: Any]()
attributes[.foregroundColor] = Asset.Colors.danger.color
let boundStart = string.index(string.startIndex, offsetBy: ComposeViewModel.composeContentLimit)
let boundEnd = string.endIndex
let range = boundStart..<boundEnd
attributedString.addAttributes(attributes, range: NSRange(range, in: string))
}
completion(attributedString)
}
}
}
}
// MARK: - ComposeToolbarViewDelegate
extension ComposeViewController: ComposeToolbarViewDelegate {

View File

@ -0,0 +1,65 @@
//
// NotificationViewController+StatusProvider.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-1.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
extension NotificationViewController: StatusProvider {
func status() -> Future<Status?, Never> {
return Future<Status?, Never> { promise in
promise(.success(nil))
}
}
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
return Future<Status?, Never> { promise in
guard let cell = cell,
let diffableDataSource = self.viewModel.diffableDataSource,
let indexPath = self.tableView.indexPath(for: cell),
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
promise(.success(nil))
return
}
switch item {
case .notification(let objectID, _):
self.viewModel.fetchedResultsController.managedObjectContext.perform {
let notification = self.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification
promise(.success(notification.status))
}
default:
promise(.success(nil))
}
}
}
func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
return Future<Status?, Never> { promise in
promise(.success(nil))
}
}
var managedObjectContext: NSManagedObjectContext {
viewModel.fetchedResultsController.managedObjectContext
}
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
return nil
}
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
return nil
}
func items(indexPaths: [IndexPath]) -> [Item] {
return []
}
}

View File

@ -12,6 +12,8 @@ import GameplayKit
import MastodonSDK
import OSLog
import UIKit
import Meta
import MetaTextView
final class NotificationViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
@ -271,6 +273,7 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl
// MARK: - NotificationTableViewCellDelegate
extension NotificationViewController: NotificationTableViewCellDelegate {
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) {
viewModel.acceptFollowRequest(notification: notification)
}
@ -286,20 +289,31 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
}
}
func userNameLabelDidPressed(notification: MastodonNotification) {
let viewModel = CachedProfileViewModel(context: context, mastodonUser: notification.account)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
}
func parent() -> UIViewController {
self
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
}
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)
}
}

View File

@ -57,8 +57,8 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
return label
}()
let nameLabel: UILabel = {
let label = UILabel()
let nameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
label.textColor = Asset.Colors.brandBlue.color
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
label.lineBreakMode = .byTruncatingTail
@ -259,7 +259,7 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
}
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
// do nothing
delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
}
}

View File

@ -9,6 +9,9 @@ import Combine
import CoreDataStack
import Foundation
import UIKit
import Meta
import MetaTextView
import ActiveLabel
protocol NotificationTableViewCellDelegate: AnyObject {
var context: AppContext! { get }
@ -16,13 +19,14 @@ protocol NotificationTableViewCellDelegate: AnyObject {
func parent() -> UIViewController
func userAvatarDidPressed(notification: MastodonNotification)
func userNameLabelDidPressed(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)
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton)
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, rejectButtonDidPressed button: UIButton)
}
@ -34,7 +38,7 @@ final class NotificationTableViewCell: UITableViewCell {
var delegate: NotificationTableViewCellDelegate?
let avatatImageView: UIImageView = {
let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.layer.cornerRadius = 4
imageView.layer.cornerCurve = .continuous
@ -72,8 +76,8 @@ final class NotificationTableViewCell: UITableViewCell {
return label
}()
let nameLabel: UILabel = {
let label = UILabel()
let nameLabel: ActiveLabel = {
let label = ActiveLabel()
label.textColor = Asset.Colors.brandBlue.color
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
label.lineBreakMode = .byTruncatingTail
@ -108,7 +112,7 @@ final class NotificationTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
avatatImageView.af.cancelImageRequest()
avatarImageView.af.cancelImageRequest()
disposeBag.removeAll()
}
@ -153,13 +157,13 @@ extension NotificationTableViewCell {
avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1)
])
avatarContainer.addSubview(avatatImageView)
avatatImageView.translatesAutoresizingMaskIntoConstraints = false
avatarContainer.addSubview(avatarImageView)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
])
avatarContainer.addSubview(actionImageBackground)