diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2dbcfd676..821dc8a60 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -49,7 +49,7 @@ 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */; }; - 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* TimelineSection.swift */; }; + 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; }; 2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; }; 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; }; @@ -239,7 +239,7 @@ 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+Diffable.swift"; sourceTree = ""; }; - 2D76319E25C1521200929FB9 /* TimelineSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSection.swift; sourceTree = ""; }; + 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; 2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; @@ -514,8 +514,8 @@ 2D69CFF225CA9E2200C3A1B2 /* Protocol */ = { isa = PBXGroup; children = ( - DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */, 2D38F1FC25CD47D900561493 /* StatusProvider */, + DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */, 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, @@ -549,7 +549,7 @@ 2D76319D25C151F600929FB9 /* Section */ = { isa = PBXGroup; children = ( - 2D76319E25C1521200929FB9 /* TimelineSection.swift */, + 2D76319E25C1521200929FB9 /* StatusSection.swift */, ); path = Section; sourceTree = ""; @@ -1382,7 +1382,7 @@ DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, - 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, + 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 8ee2f9a6c..2b34753b3 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -12,10 +12,11 @@ import MastodonSDK /// Note: update Equatable when change case enum Item { - case homeTimelineIndex(objectID: NSManagedObjectID, attribute: Attribute) + // timeline + case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute) // normal list - case toot(objectID: NSManagedObjectID) + case toot(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute) // loader case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID) @@ -23,16 +24,31 @@ enum Item { case bottomLoader } -extension Item { - class Attribute: Hashable { - var separatorLineStyle: SeparatorLineStyle = .indent +protocol StatusContentWarningAttribute { + var isStatusTextSensitive: Bool { get set } +} - static func == (lhs: Item.Attribute, rhs: Item.Attribute) -> Bool { - return lhs.separatorLineStyle == rhs.separatorLineStyle +extension Item { + class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { + var separatorLineStyle: SeparatorLineStyle = .indent + var isStatusTextSensitive: Bool = false + + public init( + separatorLineStyle: Item.StatusTimelineAttribute.SeparatorLineStyle = .indent, + isStatusTextSensitive: Bool + ) { + self.separatorLineStyle = separatorLineStyle + self.isStatusTextSensitive = isStatusTextSensitive + } + + static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool { + return lhs.separatorLineStyle == rhs.separatorLineStyle && + lhs.isStatusTextSensitive == rhs.isStatusTextSensitive } func hash(into hasher: inout Hasher) { hasher.combine(separatorLineStyle) + hasher.combine(isStatusTextSensitive) } enum SeparatorLineStyle { @@ -48,7 +64,7 @@ extension Item: Equatable { switch (lhs, rhs) { case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)): return objectIDLeft == objectIDRight - case (.toot(let objectIDLeft), .toot(let objectIDRight)): + case (.toot(let objectIDLeft, _), .toot(let objectIDRight, _)): return objectIDLeft == objectIDRight case (.bottomLoader, .bottomLoader): return true @@ -67,7 +83,7 @@ extension Item: Hashable { switch self { case .homeTimelineIndex(let objectID, _): hasher.combine(objectID) - case .toot(let objectID): + case .toot(let objectID, _): hasher.combine(objectID) case .publicMiddleLoader(let upper): hasher.combine(String(describing: Item.publicMiddleLoader.self)) diff --git a/Mastodon/Diffiable/Section/TimelineSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift similarity index 87% rename from Mastodon/Diffiable/Section/TimelineSection.swift rename to Mastodon/Diffiable/Section/StatusSection.swift index ab5a82f27..3030c140c 100644 --- a/Mastodon/Diffiable/Section/TimelineSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -11,11 +11,11 @@ import CoreDataStack import os.log import UIKit -enum TimelineSection: Equatable, Hashable { +enum StatusSection: Equatable, Hashable { case main } -extension TimelineSection { +extension StatusSection { static func tableViewDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, @@ -23,29 +23,29 @@ extension TimelineSection { timestampUpdatePublisher: AnyPublisher, timelinePostTableViewCellDelegate: StatusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? - ) -> UITableViewDiffableDataSource { + ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() } switch item { - case .homeTimelineIndex(objectID: let objectID, attribute: _): + case .homeTimelineIndex(objectID: let objectID, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell // configure cell managedObjectContext.performAndWait { let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex - TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID) + StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusContentWarningAttribute: attribute) } cell.delegate = timelinePostTableViewCellDelegate return cell - case .toot(let objectID): + case .toot(let objectID, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" // configure cell managedObjectContext.performAndWait { let toot = managedObjectContext.object(with: objectID) as! Toot - TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID) + StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusContentWarningAttribute: attribute) } cell.delegate = timelinePostTableViewCellDelegate return cell @@ -72,7 +72,8 @@ extension TimelineSection { readableLayoutFrame: CGRect?, timestampUpdatePublisher: AnyPublisher, toot: Toot, - requestUserID: String + requestUserID: String, + statusContentWarningAttribute: StatusContentWarningAttribute? ) { // set header cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil @@ -94,7 +95,8 @@ extension TimelineSection { cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) // set content warning - cell.statusView.updateContentWarningDisplay(isHidden: !(toot.reblog ?? toot).sensitive) + let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? (toot.reblog ?? toot).sensitive + cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)" } ?? L10n.Common.Controls.Status.contentWarning @@ -146,14 +148,14 @@ extension TimelineSection { // toolbar let replyCountTitle: String = { let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0 - return TimelineSection.formattedNumberTitleForActionButton(count) + return StatusSection.formattedNumberTitleForActionButton(count) }() cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) let isLike = (toot.reblog ?? toot).favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let favoriteCountTitle: String = { let count = (toot.reblog ?? toot).favouritesCount.intValue - return TimelineSection.formattedNumberTitleForActionButton(count) + return StatusSection.formattedNumberTitleForActionButton(count) }() cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal) cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike @@ -179,7 +181,7 @@ extension TimelineSection { let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let favoriteCount = targetToot.favouritesCount.intValue - let favoriteCountTitle = TimelineSection.formattedNumberTitleForActionButton(favoriteCount) + let favoriteCountTitle = StatusSection.formattedNumberTitleForActionButton(favoriteCount) cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal) cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, targetToot.id, favoriteCount) @@ -188,7 +190,7 @@ extension TimelineSection { } } -extension TimelineSection { +extension StatusSection { private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { guard let number = number, number > 0 else { return "" } return String(number) diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift index 1850f7f5e..9c6127b08 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift @@ -20,5 +20,26 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell) } + func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + item(for: cell, indexPath: nil) + .receive(on: DispatchQueue.main) + .sink { [weak self] item in + guard let _ = self else { return } + guard let item = item else { return } + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusTextSensitive = false + case .toot(_, let attribute): + attribute.isStatusTextSensitive = false + default: + return + } + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + diffableDataSource.apply(snapshot) + } + .store(in: &cell.disposeBag) + } } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift index 667fc05ac..781ccc9f3 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift @@ -13,4 +13,7 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl func toot() -> Future func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future func toot(for cell: UICollectionViewCell) -> Future + + var tableViewDiffableDataSource: UITableViewDiffableDataSource? { get } + func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift index 4c4bda1da..697820072 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift @@ -12,7 +12,7 @@ import CoreDataStack // MARK: - StatusProvider extension HomeTimelineViewController: StatusProvider { - + func toot() -> Future { return Future { promise in promise(.success(nil)) } } @@ -47,4 +47,25 @@ extension HomeTimelineViewController: StatusProvider { return Future { promise in promise(.success(nil)) } } + var tableViewDiffableDataSource: UITableViewDiffableDataSource? { + return viewModel.diffableDataSource + } + + func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future { + return Future { promise in + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + promise(.success(nil)) + return + } + guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + promise(.success(nil)) + return + } + + promise(.success(item)) + } + } + } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 2e5ca5e77..153f46130 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -15,7 +15,7 @@ import GameplayKit import MastodonSDK import AlamofireImage -final class HomeTimelineViewController: UIViewController, NeedsDependency,StatusTableViewCellDelegate { +final class HomeTimelineViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -313,3 +313,6 @@ extension HomeTimelineViewController: ScrollViewContainer { } } + +// MARK: - StatusTableViewCellDelegate +extension HomeTimelineViewController: StatusTableViewCellDelegate { } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index d37d5e12c..0091f06bf 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -23,7 +23,7 @@ extension HomeTimelineViewModel { .share() .eraseToAnyPublisher() - diffableDataSource = TimelineSection.tableViewDiffableDataSource( + diffableDataSource = StatusSection.tableViewDiffableDataSource( for: tableView, dependency: dependency, managedObjectContext: fetchedResultsController.managedObjectContext, @@ -73,7 +73,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { // that's will be the most fastest fetch because of upstream just update and no modify needs consider - var oldSnapshotAttributeDict: [NSManagedObjectID : Item.Attribute] = [:] + var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusTimelineAttribute] = [:] for item in oldSnapshot.itemIdentifiers { guard case let .homeTimelineIndex(objectID, attribute) = item else { continue } @@ -83,7 +83,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { var newTimelineItems: [Item] = [] for (i, timelineIndex) in timelineIndexes.enumerated() { - let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.Attribute() + let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: timelineIndex.toot.sensitive) // append new item into snapshot newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) @@ -103,7 +103,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { } } // end for - var newSnapshot = NSDiffableDataSourceSnapshot() + var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) newSnapshot.appendItems(newTimelineItems, toSection: .main) @@ -142,8 +142,8 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { private func calculateReloadSnapshotDifference( navigationBar: UINavigationBar, tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 5ccfca2fd..dd5ee97b1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -63,7 +63,7 @@ final class HomeTimelineViewModel: NSObject { lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) // middle loader let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine - var diffableDataSource: UITableViewDiffableDataSource? + var diffableDataSource: UITableViewDiffableDataSource? var cellFrameCache = NSCache() diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift index 889e9c6f5..6d83e79af 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift @@ -32,7 +32,7 @@ extension PublicTimelineViewController: StatusProvider { } switch item { - case .toot(let objectID): + case .toot(let objectID, _): let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext managedObjectContext.perform { let toot = managedObjectContext.object(with: objectID) as? Toot @@ -48,4 +48,25 @@ extension PublicTimelineViewController: StatusProvider { return Future { promise in promise(.success(nil)) } } + var tableViewDiffableDataSource: UITableViewDiffableDataSource? { + return viewModel.diffableDataSource + } + + func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future { + return Future { promise in + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + promise(.success(nil)) + return + } + guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + promise(.success(nil)) + return + } + + promise(.success(item)) + } + } + } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index 0e235b7ab..26638578f 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -22,7 +22,7 @@ extension PublicTimelineViewModel { .share() .eraseToAnyPublisher() - diffableDataSource = TimelineSection.tableViewDiffableDataSource( + diffableDataSource = StatusSection.tableViewDiffableDataSource( for: tableView, dependency: dependency, managedObjectContext: fetchedResultsController.managedObjectContext, @@ -50,11 +50,18 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { return indexes.firstIndex(of: toot.id).map { index in (index, toot) } } .sorted { $0.0 < $1.0 } + var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusTimelineAttribute] = [:] + for item in self.items.value { + guard case let .toot(objectID, attribute) = item else { continue } + oldSnapshotAttributeDict[objectID] = attribute + } + var items = [Item]() - for tuple in indexTootTuples { - items.append(Item.toot(objectID: tuple.1.objectID)) - if tootIDsWhichHasGap.contains(tuple.1.id) { - items.append(Item.publicMiddleLoader(tootID: tuple.1.id)) + for (_, toot) in indexTootTuples { + let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: toot.sensitive) + items.append(Item.toot(objectID: toot.objectID, attribute: attribute)) + if tootIDsWhichHasGap.contains(toot.id) { + items.append(Item.publicMiddleLoader(tootID: toot.id)) } } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift index 42590a919..d7d6448a5 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift @@ -33,7 +33,7 @@ class PublicTimelineViewModel: NSObject { // var tootIDsWhichHasGap = [String]() // output - var diffableDataSource: UITableViewDiffableDataSource? + var diffableDataSource: UITableViewDiffableDataSource? lazy var stateMachine: GKStateMachine = { let stateMachine = GKStateMachine(states: [ @@ -82,7 +82,7 @@ class PublicTimelineViewModel: NSObject { let oldSnapshot = diffableDataSource.snapshot() os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function) - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(items) if let currentState = self.stateMachine.currentState { @@ -140,8 +140,8 @@ class PublicTimelineViewModel: NSObject { private func calculateReloadSnapshotDifference( navigationBar: UINavigationBar, tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index f36150f0f..fc502597e 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -5,17 +5,24 @@ // Created by sxiaojian on 2021/1/28. // +import os.log import UIKit import AVKit import ActiveLabel import AlamofireImage +protocol StatusViewDelegate: class { + func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) +} + final class StatusView: UIView { static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageCornerRadius: CGFloat = 4 static let contentWarningBlurRadius: CGFloat = 12 + weak var delegate: StatusViewDelegate? + let headerContainerStackView = UIStackView() let headerIconLabel: UILabel = { @@ -231,7 +238,7 @@ extension StatusView { statusContentWarningContainerStackView.distribution = .fill statusContentWarningContainerStackView.alignment = .center statusTextContainerView.addSubview(statusContentWarningContainerStackView) - statusContentWarningContainerStackViewBottomLayoutConstraint = statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: statusContentWarningContainerStackView.bottomAnchor, constant: 8) + statusContentWarningContainerStackViewBottomLayoutConstraint = statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: statusContentWarningContainerStackView.bottomAnchor) NSLayoutConstraint.activate([ statusContentWarningContainerStackView.topAnchor.constraint(equalTo: statusTextContainerView.topAnchor), statusContentWarningContainerStackView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor), @@ -252,6 +259,8 @@ extension StatusView { contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false + + contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside) } } @@ -284,6 +293,13 @@ extension StatusView { } +extension StatusView { + @objc private func contentWarningActionButtonPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.statusView(self, contentWarningActionButtonPressed: sender) + } +} + extension StatusView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageCornerRadius: CGFloat { return 4 } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 187079be3..3c968f795 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -13,6 +13,7 @@ import Combine protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) + func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) } final class StatusTableViewCell: UITableViewCell { @@ -69,11 +70,20 @@ extension StatusTableViewCell { bottomPaddingView.heightAnchor.constraint(equalToConstant: 10).priority(.defaultHigh), ]) + statusView.delegate = self statusView.actionToolbarContainer.delegate = self bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color } } + +// MARK: - StatusViewDelegate +extension StatusTableViewCell: StatusViewDelegate { + func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { + delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button) + } +} + // MARK: - ActionToolbarContainerDelegate extension StatusTableViewCell: ActionToolbarContainerDelegate { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {