diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 9728c75be..63e88bdd6 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -30,7 +30,8 @@ extension NotificationSection { 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 else { + guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification, + !notification.isDeleted else { return UITableViewCell() } @@ -38,21 +39,21 @@ extension NotificationSection { cell.delegate = delegate // configure author - cell.avatarImageViewTask = Nuke.loadImage( - with: notification.account.avatarImageURL(), - options: ImageLoadingOptions( - placeholder: UIImage.placeholder(color: .systemFill), - transition: .fadeIn(duration: 0.2) - ), - into: cell.avatarImageView + cell.configure( + with: AvatarConfigurableViewConfiguration( + avatarImageURL: notification.account.avatarImageURL() + ) ) cell.actionImageView.image = UIImage( systemName: notification.notificationType.actionImageName, withConfiguration: UIImage.SymbolConfiguration( pointSize: 12, weight: .semibold ) - )?.withRenderingMode(.alwaysTemplate) - cell.actionImageBackground.backgroundColor = notification.notificationType.color + )? + .withRenderingMode(.alwaysTemplate) + .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14)) + + cell.actionImageView.backgroundColor = notification.notificationType.color // configure author name, notification description, timestamp cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index c3ad661b7..658c5fcab 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -198,32 +198,34 @@ extension NotificationViewController { // MARK: - StatusTableViewControllerAspect extension NotificationViewController: StatusTableViewControllerAspect { } -// MARK: - TableViewCellHeightCacheableContainer -extension NotificationViewController: TableViewCellHeightCacheableContainer { - var cellFrameCache: NSCache { - viewModel.cellFrameCache - } +extension NotificationViewController { func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - let key = item.hashValue - let frame = cell.frame - viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) + switch item { + case .notification(let objectID, _): + guard let object = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { return } + let key = object.id as NSString + let frame = cell.frame + viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: key) + case .bottomLoader: + break + } } func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension } - guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { - if case .bottomLoader = item { - return TimelineLoaderTableViewCell.cellHeight - } else { - return UITableView.automaticDimension - } + switch item { + case .notification(let objectID, _): + guard let object = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { return UITableView.automaticDimension } + let key = object.id as NSString + guard let frame = viewModel.cellFrameCache.object(forKey: key)?.cgRectValue else { return UITableView.automaticDimension } + return frame.height + case .bottomLoader: + return TimelineLoaderTableViewCell.cellHeight } - - return ceil(frame.height) } } @@ -237,6 +239,14 @@ extension NotificationViewController: UITableViewDelegate { open(item: item) } + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return handleTableView(tableView, estimatedHeightForRowAt: indexPath) + } + } extension NotificationViewController { diff --git a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift index f0dbd2503..6a542bf2c 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift @@ -19,7 +19,7 @@ extension NotificationViewModel { ) { diffableDataSource = NotificationSection.tableViewDiffableDataSource( for: tableView, - managedObjectContext: context.managedObjectContext, + managedObjectContext: fetchedResultsController.managedObjectContext, delegate: delegate, dependency: dependency ) diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index 8075ce375..9567d6cbb 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -71,43 +71,44 @@ extension NotificationViewModel.LoadOldestState { sinceID: nil, minID: nil, limit: nil, - excludeTypes: [.followRequest], - accountID: nil) + excludeTypes: [], + accountID: nil + ) viewModel.context.apiService.allNotifications( domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox ) - .sink { completion in - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) - case .finished: - // handle isFetchingLatestTimeline in fetch controller delegate - break + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + case .finished: + // handle isFetchingLatestTimeline in fetch controller delegate + break + } + + stateMachine.enter(Idle.self) + } receiveValue: { [weak viewModel] response in + guard let viewModel = viewModel else { return } + switch viewModel.selectedIndex.value { + case .EveryThing: + if response.value.isEmpty { + stateMachine.enter(NoMore.self) + } else { + stateMachine.enter(Idle.self) } - - stateMachine.enter(Idle.self) - } receiveValue: { [weak viewModel] response in - guard let viewModel = viewModel else { return } - switch viewModel.selectedIndex.value { - case .EveryThing: - if response.value.isEmpty { - stateMachine.enter(NoMore.self) - } else { - stateMachine.enter(Idle.self) - } - case .Mentions: - viewModel.noMoreNotification.value = response.value.isEmpty - let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } - if list.isEmpty { - stateMachine.enter(NoMore.self) - } else { - stateMachine.enter(Idle.self) - } + case .Mentions: + viewModel.noMoreNotification.value = response.value.isEmpty + let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } + if list.isEmpty { + stateMachine.enter(NoMore.self) + } else { + stateMachine.enter(Idle.self) } } - .store(in: &viewModel.disposeBag) + } + .store(in: &viewModel.disposeBag) } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 4947dbbd0..4c3b975cf 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -29,7 +29,7 @@ final class NotificationViewModel: NSObject { let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! let notificationPredicate = CurrentValueSubject(nil) - let cellFrameCache = NSCache() + let cellFrameCache = NSCache() var needsScrollToTopAfterDataSourceUpdate = false let dataSourceDidUpdated = PassthroughSubject() @@ -75,6 +75,7 @@ final class NotificationViewModel: NSObject { self.fetchedResultsController = { let fetchRequest = MastodonNotification.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 10 fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)] let controller = NSFetchedResultsController( fetchRequest: fetchRequest, diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index ad6092ad5..f4987dc45 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -37,6 +37,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { static let actionImageBorderWidth: CGFloat = 2 static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) + static let actionImageViewSize = CGSize(width: 24, height: 24) var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? @@ -45,32 +46,26 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { var containerStackViewBottomLayoutConstraint: NSLayoutConstraint! let containerStackView = UIStackView() - var avatarImageViewTask: ImageTask? let avatarImageView: UIImageView = { let imageView = FLAnimatedImageView() - imageView.layer.cornerRadius = 4 - imageView.layer.cornerCurve = .continuous - imageView.clipsToBounds = true return imageView }() let actionImageView: UIImageView = { let imageView = UIImageView() + imageView.contentMode = .center imageView.tintColor = Asset.Colors.Background.systemBackground.color + imageView.isOpaque = true + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = NotificationStatusTableViewCell.actionImageViewSize.width * 0.5 + imageView.layer.cornerCurve = .circular + imageView.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth + imageView.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor + imageView.layer.shouldRasterize = true + imageView.layer.rasterizationScale = UIScreen.main.scale 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.systemBackground.color.cgColor - view.tintColor = Asset.Colors.Background.systemBackground.color - return view - }() - let avatarContainer: UIView = { let view = UIView() return view @@ -148,8 +143,6 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() isFiltered = false - avatarImageViewTask?.cancel() - avatarImageViewTask = nil statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() @@ -199,20 +192,13 @@ extension NotificationStatusTableViewCell { avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), ]) - actionImageBackground.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addSubview(actionImageBackground) - NSLayoutConstraint.activate([ - actionImageBackground.heightAnchor.constraint(equalToConstant: 24 + NotificationStatusTableViewCell.actionImageBorderWidth).priority(.required - 1), - actionImageBackground.widthAnchor.constraint(equalToConstant: 24 + NotificationStatusTableViewCell.actionImageBorderWidth).priority(.required - 1), - actionImageBackground.centerYAnchor.constraint(equalTo: avatarImageView.bottomAnchor), - actionImageBackground.centerXAnchor.constraint(equalTo: avatarContainer.trailingAnchor), - ]) - actionImageView.translatesAutoresizingMaskIntoConstraints = false - actionImageBackground.addSubview(actionImageView) + avatarContainer.addSubview(actionImageView) NSLayoutConstraint.activate([ - actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor), - actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor), + actionImageView.centerYAnchor.constraint(equalTo: avatarContainer.bottomAnchor), + actionImageView.centerXAnchor.constraint(equalTo: avatarContainer.trailingAnchor), + actionImageView.widthAnchor.constraint(equalToConstant: NotificationStatusTableViewCell.actionImageViewSize.width), + actionImageView.heightAnchor.constraint(equalToConstant: NotificationStatusTableViewCell.actionImageViewSize.height), ]) containerStackView.addArrangedSubview(contentStackView) @@ -302,7 +288,7 @@ extension NotificationStatusTableViewCell { super.traitCollectionDidChange(previousTraitCollection) resetSeparatorLineLayout() - actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor + avatarImageView.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor statusContainerView.layer.borderColor = Asset.Colors.Border.notificationStatus.color.cgColor } @@ -405,3 +391,11 @@ extension NotificationStatusTableViewCell { } } + +// MARK: - AvatarConfigurableView +extension NotificationStatusTableViewCell: AvatarConfigurableView { + static var configurableAvatarImageSize: CGSize { CGSize(width: 35, height: 35) } + static var configurableAvatarImageCornerRadius: CGFloat { 4 } + var configurableAvatarImageView: UIImageView? { avatarImageView } + var configurableAvatarButton: UIButton? { nil } +} diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 86c820bb0..e596b39e4 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -152,6 +152,7 @@ final class StatusView: UIView { let pollTableView: PollTableView = { let tableView = PollTableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self)) + tableView.rowHeight = PollOptionView.height tableView.isScrollEnabled = false tableView.separatorStyle = .none tableView.backgroundColor = .clear @@ -450,7 +451,7 @@ extension StatusView { // action toolbar container containerStackView.addArrangedSubview(actionToolbarContainer) containerStackView.sendSubviewToBack(actionToolbarContainer) - actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + actionToolbarContainer.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) actionToolbarContainer.setContentHuggingPriority(.required - 1, for: .vertical) headerContainerView.isHidden = true