feat: implement content warning dismiss action logic

This commit is contained in:
CMK 2021-02-24 16:11:48 +08:00
parent f455faa273
commit 4d2e75f3ca
14 changed files with 167 additions and 47 deletions

View File

@ -49,7 +49,7 @@
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; };
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.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 */; }; 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 */; }; 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; }; 2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.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 = "<group>"; }; 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = "<group>"; };
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; }; 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; }; 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
2D76319E25C1521200929FB9 /* TimelineSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSection.swift; sourceTree = "<group>"; }; 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; }; 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; }; 2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; }; 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
@ -514,8 +514,8 @@
2D69CFF225CA9E2200C3A1B2 /* Protocol */ = { 2D69CFF225CA9E2200C3A1B2 /* Protocol */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
2D38F1FC25CD47D900561493 /* StatusProvider */, 2D38F1FC25CD47D900561493 /* StatusProvider */,
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
@ -549,7 +549,7 @@
2D76319D25C151F600929FB9 /* Section */ = { 2D76319D25C151F600929FB9 /* Section */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2D76319E25C1521200929FB9 /* TimelineSection.swift */, 2D76319E25C1521200929FB9 /* StatusSection.swift */,
); );
path = Section; path = Section;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1382,7 +1382,7 @@
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */, DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,

View File

@ -12,10 +12,11 @@ import MastodonSDK
/// Note: update Equatable when change case /// Note: update Equatable when change case
enum Item { enum Item {
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: Attribute) // timeline
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute)
// normal list // normal list
case toot(objectID: NSManagedObjectID) case toot(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute)
// loader // loader
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID) case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
@ -23,16 +24,31 @@ enum Item {
case bottomLoader case bottomLoader
} }
extension Item { protocol StatusContentWarningAttribute {
class Attribute: Hashable { var isStatusTextSensitive: Bool { get set }
var separatorLineStyle: SeparatorLineStyle = .indent }
static func == (lhs: Item.Attribute, rhs: Item.Attribute) -> Bool { extension Item {
return lhs.separatorLineStyle == rhs.separatorLineStyle 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) { func hash(into hasher: inout Hasher) {
hasher.combine(separatorLineStyle) hasher.combine(separatorLineStyle)
hasher.combine(isStatusTextSensitive)
} }
enum SeparatorLineStyle { enum SeparatorLineStyle {
@ -48,7 +64,7 @@ extension Item: Equatable {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)): case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
return objectIDLeft == objectIDRight return objectIDLeft == objectIDRight
case (.toot(let objectIDLeft), .toot(let objectIDRight)): case (.toot(let objectIDLeft, _), .toot(let objectIDRight, _)):
return objectIDLeft == objectIDRight return objectIDLeft == objectIDRight
case (.bottomLoader, .bottomLoader): case (.bottomLoader, .bottomLoader):
return true return true
@ -67,7 +83,7 @@ extension Item: Hashable {
switch self { switch self {
case .homeTimelineIndex(let objectID, _): case .homeTimelineIndex(let objectID, _):
hasher.combine(objectID) hasher.combine(objectID)
case .toot(let objectID): case .toot(let objectID, _):
hasher.combine(objectID) hasher.combine(objectID)
case .publicMiddleLoader(let upper): case .publicMiddleLoader(let upper):
hasher.combine(String(describing: Item.publicMiddleLoader.self)) hasher.combine(String(describing: Item.publicMiddleLoader.self))

View File

@ -11,11 +11,11 @@ import CoreDataStack
import os.log import os.log
import UIKit import UIKit
enum TimelineSection: Equatable, Hashable { enum StatusSection: Equatable, Hashable {
case main case main
} }
extension TimelineSection { extension StatusSection {
static func tableViewDiffableDataSource( static func tableViewDiffableDataSource(
for tableView: UITableView, for tableView: UITableView,
dependency: NeedsDependency, dependency: NeedsDependency,
@ -23,29 +23,29 @@ extension TimelineSection {
timestampUpdatePublisher: AnyPublisher<Date, Never>, timestampUpdatePublisher: AnyPublisher<Date, Never>,
timelinePostTableViewCellDelegate: StatusTableViewCellDelegate, timelinePostTableViewCellDelegate: StatusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
) -> UITableViewDiffableDataSource<TimelineSection, Item> { ) -> UITableViewDiffableDataSource<StatusSection, Item> {
UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() } guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
switch item { 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 let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
// configure cell // configure cell
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex 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 cell.delegate = timelinePostTableViewCellDelegate
return cell 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 cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
// configure cell // configure cell
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
let toot = managedObjectContext.object(with: objectID) as! Toot 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 cell.delegate = timelinePostTableViewCellDelegate
return cell return cell
@ -72,7 +72,8 @@ extension TimelineSection {
readableLayoutFrame: CGRect?, readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher<Date, Never>, timestampUpdatePublisher: AnyPublisher<Date, Never>,
toot: Toot, toot: Toot,
requestUserID: String requestUserID: String,
statusContentWarningAttribute: StatusContentWarningAttribute?
) { ) {
// set header // set header
cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil
@ -94,7 +95,8 @@ extension TimelineSection {
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
// set content warning // 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 cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in
return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)" return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)"
} ?? L10n.Common.Controls.Status.contentWarning } ?? L10n.Common.Controls.Status.contentWarning
@ -146,14 +148,14 @@ extension TimelineSection {
// toolbar // toolbar
let replyCountTitle: String = { let replyCountTitle: String = {
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0 let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
return TimelineSection.formattedNumberTitleForActionButton(count) return StatusSection.formattedNumberTitleForActionButton(count)
}() }()
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
let isLike = (toot.reblog ?? toot).favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let isLike = (toot.reblog ?? toot).favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let favoriteCountTitle: String = { let favoriteCountTitle: String = {
let count = (toot.reblog ?? toot).favouritesCount.intValue 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.starButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike
@ -179,7 +181,7 @@ extension TimelineSection {
let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let favoriteCount = targetToot.favouritesCount.intValue 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.starButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike 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) 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 { private static func formattedNumberTitleForActionButton(_ number: Int?) -> String {
guard let number = number, number > 0 else { return "" } guard let number = number, number > 0 else { return "" }
return String(number) return String(number)

View File

@ -20,5 +20,26 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell) 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)
}
} }

View File

@ -13,4 +13,7 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl
func toot() -> Future<Toot?, Never> func toot() -> Future<Toot?, Never>
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never> func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never>
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never> func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never>
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never>
} }

View File

@ -12,7 +12,7 @@ import CoreDataStack
// MARK: - StatusProvider // MARK: - StatusProvider
extension HomeTimelineViewController: StatusProvider { extension HomeTimelineViewController: StatusProvider {
func toot() -> Future<Toot?, Never> { func toot() -> Future<Toot?, Never> {
return Future { promise in promise(.success(nil)) } return Future { promise in promise(.success(nil)) }
} }
@ -47,4 +47,25 @@ extension HomeTimelineViewController: StatusProvider {
return Future { promise in promise(.success(nil)) } return Future { promise in promise(.success(nil)) }
} }
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
return viewModel.diffableDataSource
}
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never> {
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))
}
}
} }

View File

@ -15,7 +15,7 @@ import GameplayKit
import MastodonSDK import MastodonSDK
import AlamofireImage import AlamofireImage
final class HomeTimelineViewController: UIViewController, NeedsDependency,StatusTableViewCellDelegate { final class HomeTimelineViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
@ -313,3 +313,6 @@ extension HomeTimelineViewController: ScrollViewContainer {
} }
} }
// MARK: - StatusTableViewCellDelegate
extension HomeTimelineViewController: StatusTableViewCellDelegate { }

View File

@ -23,7 +23,7 @@ extension HomeTimelineViewModel {
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
diffableDataSource = TimelineSection.tableViewDiffableDataSource( diffableDataSource = StatusSection.tableViewDiffableDataSource(
for: tableView, for: tableView,
dependency: dependency, dependency: dependency,
managedObjectContext: fetchedResultsController.managedObjectContext, 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 // 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 { for item in oldSnapshot.itemIdentifiers {
guard case let .homeTimelineIndex(objectID, attribute) = item else { continue } guard case let .homeTimelineIndex(objectID, attribute) = item else { continue }
@ -83,7 +83,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
var newTimelineItems: [Item] = [] var newTimelineItems: [Item] = []
for (i, timelineIndex) in timelineIndexes.enumerated() { 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 // append new item into snapshot
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
@ -103,7 +103,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
} }
} // end for } // end for
var newSnapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>() var newSnapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
newSnapshot.appendSections([.main]) newSnapshot.appendSections([.main])
newSnapshot.appendItems(newTimelineItems, toSection: .main) newSnapshot.appendItems(newTimelineItems, toSection: .main)
@ -142,8 +142,8 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
private func calculateReloadSnapshotDifference<T: Hashable>( private func calculateReloadSnapshotDifference<T: Hashable>(
navigationBar: UINavigationBar, navigationBar: UINavigationBar,
tableView: UITableView, tableView: UITableView,
oldSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>, oldSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>,
newSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T> newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>
) -> Difference<T>? { ) -> Difference<T>? {
guard oldSnapshot.numberOfItems != 0 else { return nil } guard oldSnapshot.numberOfItems != 0 else { return nil }

View File

@ -63,7 +63,7 @@ final class HomeTimelineViewModel: NSObject {
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil) lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
// middle loader // middle loader
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>? var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
var cellFrameCache = NSCache<NSNumber, NSValue>() var cellFrameCache = NSCache<NSNumber, NSValue>()

View File

@ -32,7 +32,7 @@ extension PublicTimelineViewController: StatusProvider {
} }
switch item { switch item {
case .toot(let objectID): case .toot(let objectID, _):
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
managedObjectContext.perform { managedObjectContext.perform {
let toot = managedObjectContext.object(with: objectID) as? Toot let toot = managedObjectContext.object(with: objectID) as? Toot
@ -48,4 +48,25 @@ extension PublicTimelineViewController: StatusProvider {
return Future { promise in promise(.success(nil)) } return Future { promise in promise(.success(nil)) }
} }
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
return viewModel.diffableDataSource
}
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never> {
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))
}
}
} }

View File

@ -22,7 +22,7 @@ extension PublicTimelineViewModel {
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
diffableDataSource = TimelineSection.tableViewDiffableDataSource( diffableDataSource = StatusSection.tableViewDiffableDataSource(
for: tableView, for: tableView,
dependency: dependency, dependency: dependency,
managedObjectContext: fetchedResultsController.managedObjectContext, managedObjectContext: fetchedResultsController.managedObjectContext,
@ -50,11 +50,18 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
return indexes.firstIndex(of: toot.id).map { index in (index, toot) } return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
} }
.sorted { $0.0 < $1.0 } .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]() var items = [Item]()
for tuple in indexTootTuples { for (_, toot) in indexTootTuples {
items.append(Item.toot(objectID: tuple.1.objectID)) let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: toot.sensitive)
if tootIDsWhichHasGap.contains(tuple.1.id) { items.append(Item.toot(objectID: toot.objectID, attribute: attribute))
items.append(Item.publicMiddleLoader(tootID: tuple.1.id)) if tootIDsWhichHasGap.contains(toot.id) {
items.append(Item.publicMiddleLoader(tootID: toot.id))
} }
} }

View File

@ -33,7 +33,7 @@ class PublicTimelineViewModel: NSObject {
// //
var tootIDsWhichHasGap = [String]() var tootIDsWhichHasGap = [String]()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>? var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
lazy var stateMachine: GKStateMachine = { lazy var stateMachine: GKStateMachine = {
let stateMachine = GKStateMachine(states: [ let stateMachine = GKStateMachine(states: [
@ -82,7 +82,7 @@ class PublicTimelineViewModel: NSObject {
let oldSnapshot = diffableDataSource.snapshot() let oldSnapshot = diffableDataSource.snapshot()
os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function) os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function)
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>() var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems(items) snapshot.appendItems(items)
if let currentState = self.stateMachine.currentState { if let currentState = self.stateMachine.currentState {
@ -140,8 +140,8 @@ class PublicTimelineViewModel: NSObject {
private func calculateReloadSnapshotDifference<T: Hashable>( private func calculateReloadSnapshotDifference<T: Hashable>(
navigationBar: UINavigationBar, navigationBar: UINavigationBar,
tableView: UITableView, tableView: UITableView,
oldSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>, oldSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>,
newSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T> newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>
) -> Difference<T>? { ) -> Difference<T>? {
guard oldSnapshot.numberOfItems != 0 else { return nil } guard oldSnapshot.numberOfItems != 0 else { return nil }

View File

@ -5,17 +5,24 @@
// Created by sxiaojian on 2021/1/28. // Created by sxiaojian on 2021/1/28.
// //
import os.log
import UIKit import UIKit
import AVKit import AVKit
import ActiveLabel import ActiveLabel
import AlamofireImage import AlamofireImage
protocol StatusViewDelegate: class {
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
}
final class StatusView: UIView { final class StatusView: UIView {
static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4 static let avatarImageCornerRadius: CGFloat = 4
static let contentWarningBlurRadius: CGFloat = 12 static let contentWarningBlurRadius: CGFloat = 12
weak var delegate: StatusViewDelegate?
let headerContainerStackView = UIStackView() let headerContainerStackView = UIStackView()
let headerIconLabel: UILabel = { let headerIconLabel: UILabel = {
@ -231,7 +238,7 @@ extension StatusView {
statusContentWarningContainerStackView.distribution = .fill statusContentWarningContainerStackView.distribution = .fill
statusContentWarningContainerStackView.alignment = .center statusContentWarningContainerStackView.alignment = .center
statusTextContainerView.addSubview(statusContentWarningContainerStackView) statusTextContainerView.addSubview(statusContentWarningContainerStackView)
statusContentWarningContainerStackViewBottomLayoutConstraint = statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: statusContentWarningContainerStackView.bottomAnchor, constant: 8) statusContentWarningContainerStackViewBottomLayoutConstraint = statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: statusContentWarningContainerStackView.bottomAnchor)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
statusContentWarningContainerStackView.topAnchor.constraint(equalTo: statusTextContainerView.topAnchor), statusContentWarningContainerStackView.topAnchor.constraint(equalTo: statusTextContainerView.topAnchor),
statusContentWarningContainerStackView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor), statusContentWarningContainerStackView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
@ -252,6 +259,8 @@ extension StatusView {
contentWarningBlurContentImageView.isHidden = true contentWarningBlurContentImageView.isHidden = true
statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackView.isHidden = true
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false 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 { extension StatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 } static var configurableAvatarImageCornerRadius: CGFloat { return 4 }

View File

@ -13,6 +13,7 @@ import Combine
protocol StatusTableViewCellDelegate: class { protocol StatusTableViewCellDelegate: class {
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
} }
final class StatusTableViewCell: UITableViewCell { final class StatusTableViewCell: UITableViewCell {
@ -69,11 +70,20 @@ extension StatusTableViewCell {
bottomPaddingView.heightAnchor.constraint(equalToConstant: 10).priority(.defaultHigh), bottomPaddingView.heightAnchor.constraint(equalToConstant: 10).priority(.defaultHigh),
]) ])
statusView.delegate = self
statusView.actionToolbarContainer.delegate = self statusView.actionToolbarContainer.delegate = self
bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color 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 // MARK: - ActionToolbarContainerDelegate
extension StatusTableViewCell: ActionToolbarContainerDelegate { extension StatusTableViewCell: ActionToolbarContainerDelegate {
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {