diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift index 22949b3a5..fc7d0e0d9 100644 --- a/Mastodon/Diffiable/Item/NotificationItem.swift +++ b/Mastodon/Diffiable/Item/NotificationItem.swift @@ -10,7 +10,7 @@ import Foundation enum NotificationItem { case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) - + case notificationStatus(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) // display notification status without card wrapper case bottomLoader } @@ -19,6 +19,8 @@ extension NotificationItem: Equatable { switch (lhs, rhs) { case (.notification(let idLeft, _), .notification(let idRight, _)): return idLeft == idRight + case (.notificationStatus(let idLeft, _), .notificationStatus(let idRight, _)): + return idLeft == idRight case (.bottomLoader, .bottomLoader): return true default: @@ -32,6 +34,8 @@ extension NotificationItem: Hashable { switch self { case .notification(let id, _): hasher.combine(id) + case .notificationStatus(let id, _): + hasher.combine(id) case .bottomLoader: hasher.combine(String(describing: NotificationItem.bottomLoader.self)) } @@ -43,6 +47,8 @@ extension NotificationItem { switch self { case .notification(let objectID, _): return .mastodonNotification(objectID: objectID) + case .notificationStatus(let objectID, _): + return .mastodonNotification(objectID: objectID) case .bottomLoader: return nil } diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift index 215ba67cb..22283a479 100644 --- a/Mastodon/Diffiable/Section/Status/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/Status/NotificationSection.swift @@ -21,9 +21,10 @@ enum NotificationSection: Equatable, Hashable { extension NotificationSection { static func tableViewDiffableDataSource( for tableView: UITableView, + dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, delegate: NotificationTableViewCellDelegate, - dependency: NeedsDependency + statusTableViewCellDelegate: StatusTableViewCellDelegate ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { [weak delegate, weak dependency] @@ -32,137 +33,45 @@ extension NotificationSection { switch notificationItem { case .notification(let objectID, let attribute): guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification, - !notification.isDeleted else { - return UITableViewCell() - } + !notification.isDeleted + else { return UITableViewCell() } let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell - cell.delegate = delegate - - // configure author - cell.configure( - with: AvatarConfigurableViewConfiguration( - avatarImageURL: notification.account.avatarImageURL() - ) + configure( + tableView: tableView, + cell: cell, + notification: notification, + dependency: dependency, + attribute: attribute ) + cell.delegate = delegate + return cell - 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)) - } + 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 - 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 - } - + // 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: @@ -174,3 +83,136 @@ extension NotificationSection { } } +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 + } + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift b/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift index 127cca1b9..57272404e 100644 --- a/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift +++ b/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift @@ -19,21 +19,25 @@ extension NotificationViewController: StatusProvider { func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future { return Future { promise in - guard let cell = cell, - let diffableDataSource = self.viewModel.diffableDataSource, - let indexPath = self.tableView.indexPath(for: cell), + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + promise(.success(nil)) + return + } + guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), let item = diffableDataSource.itemIdentifier(for: indexPath) else { promise(.success(nil)) return } switch item { - case .notification(let objectID, _): + case .notification(let objectID, _), + .notificationStatus(let objectID, _): self.viewModel.fetchedResultsController.managedObjectContext.perform { let notification = self.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification promise(.success(notification.status)) } - default: + case .bottomLoader: promise(.success(nil)) } } @@ -68,3 +72,6 @@ extension NotificationViewController: StatusProvider { } } + +// MARK: - UserProvider +extension NotificationViewController: UserProvider { } diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 55b4504dc..0567d04dd 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -14,8 +14,10 @@ import OSLog import UIKit import Meta import MetaTextKit +import AVKit -final class NotificationViewController: UIViewController, NeedsDependency { +final class NotificationViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -23,15 +25,18 @@ final class NotificationViewController: UIViewController, NeedsDependency { var observations = Set() private(set) lazy var viewModel = NotificationViewModel(context: context) + + let mediaPreviewTransitionController = MediaPreviewTransitionController() let segmentControl: UISegmentedControl = { let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions]) - control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.EveryThing.rawValue + control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.everyThing.rawValue return control }() let tableView: UITableView = { let tableView = ControlContainableTableView() + tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.estimatedRowHeight = UITableView.automaticDimension @@ -82,7 +87,12 @@ extension NotificationViewController { tableView.delegate = self viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self - viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self) + viewModel.setupDiffableDataSource( + for: tableView, + dependency: self, + delegate: self, + statusTableViewCellDelegate: self + ) viewModel.viewDidLoad.send() // bind refresh control @@ -128,9 +138,9 @@ extension NotificationViewController { self.viewModel.needsScrollToTopAfterDataSourceUpdate = true switch segment { - case .EveryThing: + case .everyThing: self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) - case .Mentions: + case .mentions: self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue) } } @@ -148,8 +158,8 @@ extension NotificationViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - tableView.deselectRow(with: transitionCoordinator, animated: animated) + + aspectViewWillAppear(animated) // fetch latest notification when scroll position is within half screen height to prevent list reload if tableView.contentOffset.y < view.frame.height * 0.5 { @@ -181,6 +191,12 @@ extension NotificationViewController { // reset notification count context.notificationService.clearNotificationCountForActiveUser() } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + aspectViewDidDisappear(animated) + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -208,33 +224,34 @@ extension NotificationViewController { } } -// MARK: - StatusTableViewControllerAspect -extension NotificationViewController: StatusTableViewControllerAspect { } - -extension NotificationViewController { - +// MARK: - TableViewCellHeightCacheableContainer +extension NotificationViewController: TableViewCellHeightCacheableContainer { + var cellFrameCache: NSCache { return viewModel.cellFrameCache } + 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 } switch item { - case .notification(let objectID, _): + case .notification(let objectID, _), + .notificationStatus(let objectID, _): guard let object = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { return } - let key = object.id as NSString + let key = object.objectID.hashValue let frame = cell.frame - viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: key) + viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: 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 } switch item { - case .notification(let objectID, _): + case .notification(let objectID, _), + .notificationStatus(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 } + let key = object.objectID.hashValue + guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: key))?.cgRectValue else { return UITableView.automaticDimension } return frame.height case .bottomLoader: return TimelineLoaderTableViewCell.cellHeight @@ -242,22 +259,55 @@ extension NotificationViewController { } } + +// MARK: - StatusTableViewControllerAspect +extension NotificationViewController: StatusTableViewControllerAspect { } + // MARK: - UITableViewDelegate extension NotificationViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + aspectTableView(tableView, estimatedHeightForRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - open(item: item) + switch item { + case .notificationStatus: + aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath) + case .bottomLoader: + if !tableView.isDragging, !tableView.isDecelerating { + viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) + } + default: + break + } } func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) + aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + aspectTableView(tableView, didSelectRowAt: indexPath) } - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return handleTableView(tableView, estimatedHeightForRowAt: indexPath) + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) + } + + func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) } } @@ -278,19 +328,6 @@ extension NotificationViewController { break } } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - switch item { - case .bottomLoader: - if !tableView.isDragging, !tableView.isDecelerating { - viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) - } - default: - break - } - } } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate @@ -388,6 +425,7 @@ extension NotificationViewController: ScrollViewContainer { } } +// MARK: - LoadMoreConfigurableTableViewContainer extension NotificationViewController: LoadMoreConfigurableTableViewContainer { typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = NotificationViewModel.LoadOldestState.Loading @@ -395,6 +433,24 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer { var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadOldestStateMachine } } +// MARK: - AVPlayerViewControllerDelegate +extension NotificationViewController: AVPlayerViewControllerDelegate { + func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { + handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator) + } + + func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { + handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator) + } +} + +// MARK: - statusTableViewCellDelegate +extension NotificationViewController: StatusTableViewCellDelegate { + var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { + return self + } +} + extension NotificationViewController { enum CategorySwitch: String, CaseIterable { @@ -452,9 +508,9 @@ extension NotificationViewController { switch category { case .showEverything: - viewModel.selectedIndex.value = .EveryThing + viewModel.selectedIndex.value = .everyThing case .showMentions: - viewModel.selectedIndex.value = .Mentions + viewModel.selectedIndex.value = .mentions } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift index 6a542bf2c..6c7a70e43 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift @@ -14,14 +14,16 @@ import MastodonSDK extension NotificationViewModel { func setupDiffableDataSource( for tableView: UITableView, + dependency: NeedsDependency, delegate: NotificationTableViewCellDelegate, - dependency: NeedsDependency + statusTableViewCellDelegate: StatusTableViewCellDelegate ) { diffableDataSource = NotificationSection.tableViewDiffableDataSource( for: tableView, + dependency: dependency, managedObjectContext: fetchedResultsController.managedObjectContext, delegate: delegate, - dependency: dependency + statusTableViewCellDelegate: statusTableViewCellDelegate ) var snapshot = NSDiffableDataSourceSnapshot() @@ -81,11 +83,23 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { } var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) - let items: [NotificationItem] = notifications.map { notification in - let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute() - return NotificationItem.notification(objectID: notification.objectID, attribute: attribute) + + let segment = self.selectedIndex.value + switch segment { + case .everyThing: + let items: [NotificationItem] = notifications.map { notification in + let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute() + return NotificationItem.notification(objectID: notification.objectID, attribute: attribute) + } + newSnapshot.appendItems(items, toSection: .main) + case .mentions: + let items: [NotificationItem] = notifications.map { notification in + let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute() + return NotificationItem.notificationStatus(objectID: notification.objectID, attribute: attribute) + } + newSnapshot.appendItems(items, toSection: .main) } - newSnapshot.appendItems(items, toSection: .main) + if !notifications.isEmpty, self.noMoreNotification.value == false { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index 9567d6cbb..bf2c03174 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -92,13 +92,13 @@ extension NotificationViewModel.LoadOldestState { } receiveValue: { [weak viewModel] response in guard let viewModel = viewModel else { return } switch viewModel.selectedIndex.value { - case .EveryThing: + case .everyThing: if response.value.isEmpty { stateMachine.enter(NoMore.self) } else { stateMachine.enter(Idle.self) } - case .Mentions: + case .mentions: viewModel.noMoreNotification.value = response.value.isEmpty let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } if list.isEmpty { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 712380917..98b7deec3 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -23,13 +23,13 @@ final class NotificationViewModel: NSObject { weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? let viewDidLoad = PassthroughSubject() - let selectedIndex = CurrentValueSubject(.EveryThing) + let selectedIndex = CurrentValueSubject(.everyThing) let noMoreNotification = CurrentValueSubject(false) let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! let notificationPredicate = CurrentValueSubject(nil) - let cellFrameCache = NSCache() + let cellFrameCache = NSCache() var needsScrollToTopAfterDataSourceUpdate = false let dataSourceDidUpdated = PassthroughSubject() @@ -161,7 +161,7 @@ final class NotificationViewModel: NSObject { extension NotificationViewModel { enum NotificationSegment: Int { - case EveryThing - case Mentions + case everyThing + case mentions } } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 957764fa7..62eb3d6b0 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -203,6 +203,9 @@ final class StatusView: UIView { return actionToolbarContainer }() + // set display when needs bottom padding + let actionToolbarPlaceholderPaddingView = UIView() + let contentMetaText: MetaText = { let metaText = MetaText() metaText.textView.backgroundColor = .clear @@ -451,6 +454,13 @@ extension StatusView { containerStackView.sendSubviewToBack(actionToolbarContainer) actionToolbarContainer.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) actionToolbarContainer.setContentHuggingPriority(.required - 1, for: .vertical) + + actionToolbarPlaceholderPaddingView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(actionToolbarPlaceholderPaddingView) + NSLayoutConstraint.activate([ + actionToolbarPlaceholderPaddingView.heightAnchor.constraint(equalToConstant: 12).priority(.required - 1), + ]) + actionToolbarPlaceholderPaddingView.isHidden = true headerContainerView.isHidden = true statusMosaicImageViewContainer.isHidden = true