feat: restore post filter supports

This commit is contained in:
CMK 2022-02-15 19:44:45 +08:00
parent d80b8d718a
commit 792208aebb
14 changed files with 155 additions and 357 deletions

View File

@ -24,6 +24,8 @@ extension NotificationSection {
struct Configuration { struct Configuration {
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate? weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context?
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
} }
static func diffableDataSource( static func diffableDataSource(
@ -58,57 +60,6 @@ extension NotificationSection {
cell.activityIndicatorView.startAnimating() cell.activityIndicatorView.startAnimating()
return cell return cell
} }
// switch notificationItem {
// case .notification(let objectID, let attribute):
// guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
// !notification.isDeleted
// else { return UITableViewCell() }
//
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
// configure(
// tableView: tableView,
// cell: cell,
// notification: notification,
// dependency: dependency,
// attribute: attribute
// )
// cell.delegate = delegate
// cell.isAccessibilityElement = true
// NotificationSection.configureStatusAccessibilityLabel(cell: cell)
// return cell
//
// 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
//
// // 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:
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
// cell.startAnimating()
// return cell
// }
} }
} }
} }
@ -142,163 +93,17 @@ extension NotificationSection {
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.notificationTableViewCellDelegate delegate: configuration.notificationTableViewCellDelegate
) )
cell.notificationView.statusView.viewModel.filterContext = configuration.filterContext
cell.notificationView.quoteStatusView.viewModel.filterContext = configuration.filterContext
configuration.activeFilters?
.assign(to: \.activeFilters, on: cell.notificationView.statusView.viewModel)
.store(in: &cell.disposeBag)
configuration.activeFilters?
.assign(to: \.activeFilters, on: cell.notificationView.quoteStatusView.viewModel)
.store(in: &cell.disposeBag)
} }
// 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
// }
// }
//
// static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
// // FIXME:
// cell.accessibilityLabel = {
// var accessibilityViews: [UIView?] = []
// accessibilityViews.append(contentsOf: [
// cell.titleLabel,
// cell.timestampLabel,
// cell.statusView
// ])
// if !cell.statusContainerView.isHidden {
// if !cell.statusView.headerContainerView.isHidden {
// accessibilityViews.append(cell.statusView.headerInfoLabel)
// }
// accessibilityViews.append(contentsOf: [
// cell.statusView.nameMetaLabel,
// cell.statusView.dateLabel,
// cell.statusView.contentMetaText.textView,
// ])
// }
// return accessibilityViews
// .compactMap { $0?.accessibilityLabel }
// .joined(separator: " ")
// }()
// }
} }

View File

@ -28,6 +28,8 @@ extension StatusSection {
struct Configuration { struct Configuration {
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate? weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context?
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
} }
static func diffableDataSource( static func diffableDataSource(
@ -258,6 +260,11 @@ extension StatusSection {
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.statusTableViewCellDelegate delegate: configuration.statusTableViewCellDelegate
) )
cell.statusView.viewModel.filterContext = configuration.filterContext
configuration.activeFilters?
.assign(to: \.activeFilters, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
} }
static func configure( static func configure(
@ -282,6 +289,11 @@ extension StatusSection {
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.statusTableViewCellDelegate delegate: configuration.statusTableViewCellDelegate
) )
cell.statusView.viewModel.filterContext = configuration.filterContext
configuration.activeFilters?
.assign(to: \.activeFilters, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
} }
static func configure( static func configure(
@ -296,133 +308,3 @@ extension StatusSection {
} }
} }
extension StatusSection {
enum TimelineContext {
case home
case notifications
case `public`
case thread
case account
case favorite
case hashtag
case report
case search
var filterContext: Mastodon.Entity.Filter.Context? {
switch self {
case .home: return .home
case .notifications: return .notifications
case .public: return .public
case .thread: return .thread
case .account: return .account
default: return nil
}
}
}
private static func needsFilterStatus(
content: MastodonMetaContent?,
filters: [Mastodon.Entity.Filter],
timelineContext: TimelineContext
) -> AnyPublisher<Bool, Never> {
guard let content = content,
let currentFilterContext = timelineContext.filterContext,
!filters.isEmpty else {
return Just(false).eraseToAnyPublisher()
}
return Future<Bool, Never> { promise in
DispatchQueue.global(qos: .userInteractive).async {
var wordFilters: [Mastodon.Entity.Filter] = []
var nonWordFilters: [Mastodon.Entity.Filter] = []
for filter in filters {
guard filter.context.contains(where: { $0 == currentFilterContext }) else { continue }
if filter.wholeWord {
wordFilters.append(filter)
} else {
nonWordFilters.append(filter)
}
}
let text = content.original.lowercased()
var needsFilter = false
for filter in nonWordFilters {
guard text.contains(filter.phrase.lowercased()) else { continue }
needsFilter = true
break
}
if needsFilter {
DispatchQueue.main.async {
promise(.success(true))
}
return
}
let tokenizer = NLTokenizer(unit: .word)
tokenizer.string = text
let phraseWords = wordFilters.map { $0.phrase.lowercased() }
tokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { range, _ in
let word = String(text[range])
if phraseWords.contains(word) {
needsFilter = true
return false
} else {
return true
}
}
DispatchQueue.main.async {
promise(.success(needsFilter))
}
}
}
.eraseToAnyPublisher()
}
}
class StatusContentOperation: Operation {
let logger = Logger(subsystem: "StatusContentOperation", category: "logic")
// input
let statusObjectID: NSManagedObjectID
let mastodonContent: MastodonContent
// output
var result: Result<MastodonMetaContent, Error>?
init(
statusObjectID: NSManagedObjectID,
mastodonContent: MastodonContent
) {
self.statusObjectID = statusObjectID
self.mastodonContent = mastodonContent
super.init()
}
override func main() {
guard !isCancelled else { return }
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): prcoess \(self.statusObjectID)")
do {
let content = try MastodonMetaContent.convert(document: mastodonContent)
result = .success(content)
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): process success \(self.statusObjectID)")
} catch {
result = .failure(error)
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): process fail \(self.statusObjectID)")
}
}
override func cancel() {
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel \(self.statusObjectID.debugDescription)")
super.cancel()
}
}

View File

@ -21,7 +21,9 @@ extension HashtagTimelineViewModel {
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,
activeFilters: nil
) )
) )

View File

@ -22,7 +22,9 @@ extension HomeTimelineViewModel {
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
filterContext: .home,
activeFilters: context.statusFilterService.$activeFilters
) )
) )

View File

@ -20,7 +20,9 @@ extension NotificationTimelineViewModel {
tableView: tableView, tableView: tableView,
context: context, context: context,
configuration: NotificationSection.Configuration( configuration: NotificationSection.Configuration(
notificationTableViewCellDelegate: notificationTableViewCellDelegate notificationTableViewCellDelegate: notificationTableViewCellDelegate,
filterContext: .notifications,
activeFilters: context.statusFilterService.$activeFilters
) )
) )

View File

@ -18,7 +18,9 @@ extension FavoriteViewModel {
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,
activeFilters: nil
) )
) )
// set empty section to make update animation top-to-bottom style // set empty section to make update animation top-to-bottom style

View File

@ -19,7 +19,9 @@ extension UserTimelineViewModel {
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,
activeFilters: nil
) )
) )

View File

@ -9,11 +9,16 @@ import UIKit
import Combine import Combine
import MastodonUI import MastodonUI
import CoreDataStack import CoreDataStack
import MastodonSDK
import MastodonLocalization import MastodonLocalization
import MastodonMeta import MastodonMeta
import Meta import Meta
import NaturalLanguage
extension StatusView { extension StatusView {
static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue")
public func configure(feed: Feed) { public func configure(feed: Feed) {
switch feed.kind { switch feed.kind {
case .home: case .home:
@ -48,7 +53,8 @@ extension StatusView {
configureContent(status: status) configureContent(status: status)
configureMedia(status: status) configureMedia(status: status)
configurePoll(status: status) configurePoll(status: status)
configureToolbar(status: status) configureToolbar(status: status)
configureFilter(status: status)
} }
} }
@ -397,5 +403,58 @@ extension StatusView {
.assign(to: \.isFavorite, on: viewModel) .assign(to: \.isFavorite, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func configureFilter(status: Status) {
let status = status.reblog ?? status
let content = status.content.lowercased()
Publishers.CombineLatest(
viewModel.$activeFilters,
viewModel.$filterContext
)
.receive(on: StatusView.statusFilterWorkingQueue)
.map { filters, filterContext in
var wordFilters: [Mastodon.Entity.Filter] = []
var nonWordFilters: [Mastodon.Entity.Filter] = []
for filter in filters {
guard filter.context.contains(where: { $0 == filterContext }) else { continue }
if filter.wholeWord {
wordFilters.append(filter)
} else {
nonWordFilters.append(filter)
}
}
var needsFilter = false
for filter in nonWordFilters {
guard content.contains(filter.phrase.lowercased()) else { continue }
needsFilter = true
break
}
if needsFilter {
return true
}
let tokenizer = NLTokenizer(unit: .word)
tokenizer.string = content
let phraseWords = wordFilters.map { $0.phrase.lowercased() }
tokenizer.enumerateTokens(in: content.startIndex..<content.endIndex) { range, _ in
let word = String(content[range])
if phraseWords.contains(word) {
needsFilter = true
return false
} else {
return true
}
}
return needsFilter
}
.receive(on: DispatchQueue.main)
.assign(to: \.isFiltered, on: viewModel)
.store(in: &disposeBag)
}
} }

View File

@ -23,7 +23,9 @@ extension ThreadViewModel {
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .thread,
activeFilters: context.statusFilterService.$activeFilters
) )
) )

View File

@ -23,7 +23,7 @@ final class StatusFilterService {
let filterUpdatePublisher = PassthroughSubject<Void, Never>() let filterUpdatePublisher = PassthroughSubject<Void, Never>()
// output // output
let activeFilters = CurrentValueSubject<[Mastodon.Entity.Filter], Never>([]) @Published var activeFilters: [Mastodon.Entity.Filter] = []
init( init(
apiService: APIService, apiService: APIService,
@ -57,7 +57,14 @@ final class StatusFilterService {
.map { response in .map { response in
let now = Date() let now = Date()
let newResponse = response.map { filters in let newResponse = response.map { filters in
return filters.filter { $0.expiresAt > now } // filter out expired rules return filters.filter { filter in
if let expiresAt = filter.expiresAt {
// filter out expired rules
return expiresAt > now
} else {
return true
}
}
} }
return Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.success(newResponse) return Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.success(newResponse)
} }
@ -70,7 +77,7 @@ final class StatusFilterService {
switch result { switch result {
case .success(let response): case .success(let response):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters success. %ld items", ((#file as NSString).lastPathComponent), #line, #function, response.value.count) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters success. %ld items", ((#file as NSString).lastPathComponent), #line, #function, response.value.count)
self.activeFilters.value = response.value self.activeFilters = response.value
case .failure(let error): case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D62" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES"> <entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/> <attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/> <attribute name="name" attributeType="String"/>

View File

@ -22,7 +22,7 @@ extension Mastodon.Entity {
public let id: ID public let id: ID
public let phrase: String public let phrase: String
public let context: [Context] public let context: [Context]
public let expiresAt: Date public let expiresAt: Date?
public let irreversible: Bool public let irreversible: Bool
public let wholeWord: Bool public let wholeWord: Bool
@ -38,7 +38,7 @@ extension Mastodon.Entity {
} }
extension Mastodon.Entity.Filter { extension Mastodon.Entity.Filter {
public enum Context: RawRepresentable, Codable { public enum Context: RawRepresentable, Codable, Hashable {
case home case home
case notifications case notifications
case `public` case `public`

View File

@ -89,6 +89,11 @@ extension StatusView {
@Published public var replyCount: Int = 0 @Published public var replyCount: Int = 0
@Published public var reblogCount: Int = 0 @Published public var reblogCount: Int = 0
@Published public var favoriteCount: Int = 0 @Published public var favoriteCount: Int = 0
// Filter
@Published public var activeFilters: [Mastodon.Entity.Filter] = []
@Published public var filterContext: Mastodon.Entity.Filter.Context?
@Published public var isFiltered = false
@Published public var groupedAccessibilityLabel = "" @Published public var groupedAccessibilityLabel = ""
@ -128,9 +133,8 @@ extension StatusView {
isMediaSensitive = false isMediaSensitive = false
isMediaSensitiveToggled = false isMediaSensitiveToggled = false
// isSensitive = false activeFilters = []
// isContentReveal = false filterContext = nil
// isMediaReveal = false
} }
init() { init() {
@ -192,6 +196,7 @@ extension StatusView.ViewModel {
bindToolbar(statusView: statusView) bindToolbar(statusView: statusView)
bindMetric(statusView: statusView) bindMetric(statusView: statusView)
bindMenu(statusView: statusView) bindMenu(statusView: statusView)
bindFilter(statusView: statusView)
bindAccessibility(statusView: statusView) bindAccessibility(statusView: statusView)
} }
@ -611,6 +616,17 @@ extension StatusView.ViewModel {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func bindFilter(statusView: StatusView) {
$isFiltered
.sink { isFiltered in
statusView.containerStackView.isHidden = isFiltered
if isFiltered {
statusView.setFilterHintLabelDisplay()
}
}
.store(in: &disposeBag)
}
private func bindAccessibility(statusView: StatusView) { private func bindAccessibility(statusView: StatusView) {
let authorAccessibilityLabel = Publishers.CombineLatest3( let authorAccessibilityLabel = Publishers.CombineLatest3(
$header, $header,

View File

@ -226,6 +226,15 @@ public final class StatusView: UIView {
// metric // metric
public let statusMetricView = StatusMetricView() public let statusMetricView = StatusMetricView()
// filter hint
public let filterHintLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color
label.text = L10n.Common.Controls.Timeline.filtered
label.font = .systemFont(ofSize: 17, weight: .regular)
return label
}()
public func prepareForReuse() { public func prepareForReuse() {
disposeBag.removeAll() disposeBag.removeAll()
@ -249,7 +258,7 @@ public final class StatusView: UIView {
mediaContainerView.isHidden = true mediaContainerView.isHidden = true
pollContainerView.isHidden = true pollContainerView.isHidden = true
statusVisibilityView.isHidden = true statusVisibilityView.isHidden = true
// setSpoilerBannerViewHidden(isHidden: true) filterHintLabel.isHidden = true
} }
public override init(frame: CGRect) { public override init(frame: CGRect) {
@ -570,6 +579,14 @@ extension StatusView.Style {
statusView.actionToolbarContainer.configure(for: .inline) statusView.actionToolbarContainer.configure(for: .inline)
statusView.actionToolbarContainer.preservesSuperviewLayoutMargins = true statusView.actionToolbarContainer.preservesSuperviewLayoutMargins = true
statusView.containerStackView.addArrangedSubview(statusView.actionToolbarContainer) statusView.containerStackView.addArrangedSubview(statusView.actionToolbarContainer)
// filterHintLabel
statusView.filterHintLabel.translatesAutoresizingMaskIntoConstraints = false
statusView.addSubview(statusView.filterHintLabel)
NSLayoutConstraint.activate([
statusView.filterHintLabel.centerXAnchor.constraint(equalTo: statusView.containerStackView.centerXAnchor),
statusView.filterHintLabel.centerYAnchor.constraint(equalTo: statusView.containerStackView.centerYAnchor),
])
} }
func inline(statusView: StatusView) { func inline(statusView: StatusView) {
@ -673,9 +690,9 @@ extension StatusView {
statusVisibilityView.isHidden = false statusVisibilityView.isHidden = false
} }
// func setSpoilerBannerViewHidden(isHidden: Bool) { func setFilterHintLabelDisplay() {
// spoilerBannerView.isHidden = isHidden filterHintLabel.isHidden = false
// } }
// content text Width // content text Width
public var contentMaxLayoutWidth: CGFloat { public var contentMaxLayoutWidth: CGFloat {