Sensitive attachments

This commit is contained in:
Justin Mazzocchi 2020-10-13 17:03:01 -07:00
parent 40b795cd7e
commit cc370af881
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
25 changed files with 250 additions and 136 deletions

View File

@ -62,7 +62,11 @@ extension ContentDatabase {
t.column("pinned", .boolean) t.column("pinned", .boolean)
} }
try db.create(table: "statusShowMoreToggle") { t in try db.create(table: "statusShowContentToggle") { t in
t.column("statusId", .text).primaryKey().references("statusRecord", onDelete: .cascade)
}
try db.create(table: "statusShowAttachmentsToggle") { t in
t.column("statusId", .text).primaryKey().references("statusRecord", onDelete: .cascade) t.column("statusId", .text).primaryKey().references("statusRecord", onDelete: .cascade)
} }

View File

@ -149,37 +149,56 @@ public extension ContentDatabase {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> { func toggleShowContent(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher { databaseWriter.writePublisher {
if let toggle = try StatusShowMoreToggle if let toggle = try StatusShowContentToggle
.filter(StatusShowMoreToggle.Columns.statusId == id) .filter(StatusShowContentToggle.Columns.statusId == id)
.fetchOne($0) { .fetchOne($0) {
try toggle.delete($0) try toggle.delete($0)
} else { } else {
try StatusShowMoreToggle(statusId: id).save($0) try StatusShowContentToggle(statusId: id).save($0)
} }
} }
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func showMore(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> { func toggleShowAttachments(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
if let toggle = try StatusShowAttachmentsToggle
.filter(StatusShowAttachmentsToggle.Columns.statusId == id)
.fetchOne($0) {
try toggle.delete($0)
} else {
try StatusShowAttachmentsToggle(statusId: id).save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func expand(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher { databaseWriter.writePublisher {
for id in ids { for id in ids {
try StatusShowMoreToggle(statusId: id).save($0) try StatusShowContentToggle(statusId: id).save($0)
try StatusShowAttachmentsToggle(statusId: id).save($0)
} }
} }
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func showLess(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> { func collapse(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher( databaseWriter.writePublisher {
updates: StatusShowMoreToggle try StatusShowContentToggle
.filter(ids.contains(StatusShowMoreToggle.Columns.statusId)) .filter(ids.contains(StatusShowContentToggle.Columns.statusId))
.deleteAll) .deleteAll($0)
.ignoreOutput() try StatusShowAttachmentsToggle
.eraseToAnyPublisher() .filter(ids.contains(StatusShowContentToggle.Columns.statusId))
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
} }
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> { func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {

View File

@ -35,7 +35,8 @@ extension ContextItemsInfo {
return .status( return .status(
.init(info: statusInfo), .init(info: statusInfo),
.init(showMoreToggled: statusInfo.showMoreToggled, .init(showContentToggled: statusInfo.showContentToggled,
showAttachmentsToggled: statusInfo.showAttachmentsToggled,
isContextParent: statusInfo.record.id == parent.record.id, isContextParent: statusInfo.record.id == parent.record.id,
isReplyInContext: isReplyInContext, isReplyInContext: isReplyInContext,
hasReplyFollowing: hasReplyFollowing)) hasReplyFollowing: hasReplyFollowing))

View File

@ -8,8 +8,10 @@ struct StatusInfo: Codable, Hashable, FetchableRecord {
let accountInfo: AccountInfo let accountInfo: AccountInfo
let reblogAccountInfo: AccountInfo? let reblogAccountInfo: AccountInfo?
let reblogRecord: StatusRecord? let reblogRecord: StatusRecord?
let showMoreToggle: StatusShowMoreToggle? let showContentToggle: StatusShowContentToggle?
let reblogShowMoreToggle: StatusShowMoreToggle? let reblogShowContentToggle: StatusShowContentToggle?
let showAttachmentsToggle: StatusShowAttachmentsToggle?
let reblogShowAttachmentsToggle: StatusShowAttachmentsToggle?
} }
extension StatusInfo { extension StatusInfo {
@ -18,9 +20,11 @@ extension StatusInfo {
.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount) .including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount)
.forKey(CodingKeys.reblogAccountInfo)) .forKey(CodingKeys.reblogAccountInfo))
.including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord)) .including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord))
.including(optional: StatusRecord.showMoreToggle.forKey(CodingKeys.showMoreToggle)) .including(optional: StatusRecord.showContentToggle.forKey(CodingKeys.showContentToggle))
.including(optional: StatusRecord.reblogShowMoreToggle .including(optional: StatusRecord.reblogShowContentToggle.forKey(CodingKeys.reblogShowContentToggle))
.forKey(CodingKeys.reblogShowMoreToggle)) .including(optional: StatusRecord.showAttachmentsToggle.forKey(CodingKeys.showAttachmentsToggle))
.including(optional: StatusRecord.reblogShowAttachmentsToggle
.forKey(CodingKeys.reblogShowAttachmentsToggle))
} }
static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> { static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> {
@ -31,7 +35,11 @@ extension StatusInfo {
(record.filterableContent + (reblogRecord?.filterableContent ?? [])).joined(separator: " ") (record.filterableContent + (reblogRecord?.filterableContent ?? [])).joined(separator: " ")
} }
var showMoreToggled: Bool { var showContentToggled: Bool {
showMoreToggle != nil || reblogShowMoreToggle != nil showContentToggle != nil || reblogShowContentToggle != nil
}
var showAttachmentsToggled: Bool {
showAttachmentsToggle != nil || reblogShowAttachmentsToggle != nil
} }
} }

View File

@ -92,11 +92,16 @@ extension StatusRecord {
through: Self.reblogAccount, through: Self.reblogAccount,
using: AccountRecord.moved) using: AccountRecord.moved)
static let reblog = belongsTo(StatusRecord.self) static let reblog = belongsTo(StatusRecord.self)
static let showMoreToggle = hasOne(StatusShowMoreToggle.self) static let showContentToggle = hasOne(StatusShowContentToggle.self)
static let reblogShowMoreToggle = hasOne( static let reblogShowContentToggle = hasOne(
StatusShowMoreToggle.self, StatusShowContentToggle.self,
through: Self.reblog, through: Self.reblog,
using: Self.showMoreToggle) using: Self.showContentToggle)
static let showAttachmentsToggle = hasOne(StatusShowAttachmentsToggle.self)
static let reblogShowAttachmentsToggle = hasOne(
StatusShowAttachmentsToggle.self,
through: Self.reblog,
using: Self.showAttachmentsToggle)
static let ancestorJoins = hasMany( static let ancestorJoins = hasMany(
StatusAncestorJoin.self, StatusAncestorJoin.self,
using: ForeignKey([StatusAncestorJoin.Columns.parentId])) using: ForeignKey([StatusAncestorJoin.Columns.parentId]))

View File

@ -0,0 +1,25 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
struct StatusShowAttachmentsToggle: Codable, Hashable {
let statusId: Status.Id
}
extension StatusShowAttachmentsToggle {
enum Columns {
static let statusId = Column(StatusShowAttachmentsToggle.CodingKeys.statusId)
}
}
extension StatusShowAttachmentsToggle: FetchableRecord, PersistableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
MastodonDecoder()
}
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
MastodonEncoder()
}
}

View File

@ -4,17 +4,17 @@ import Foundation
import GRDB import GRDB
import Mastodon import Mastodon
struct StatusShowMoreToggle: Codable, Hashable { struct StatusShowContentToggle: Codable, Hashable {
let statusId: Status.Id let statusId: Status.Id
} }
extension StatusShowMoreToggle { extension StatusShowContentToggle {
enum Columns { enum Columns {
static let statusId = Column(StatusShowMoreToggle.CodingKeys.statusId) static let statusId = Column(StatusShowContentToggle.CodingKeys.statusId)
} }
} }
extension StatusShowMoreToggle: FetchableRecord, PersistableRecord { extension StatusShowContentToggle: FetchableRecord, PersistableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder { static func databaseJSONDecoder(for column: String) -> JSONDecoder {
MastodonDecoder() MastodonDecoder()
} }

View File

@ -35,7 +35,8 @@ extension TimelineItemsInfo {
.map { .map {
CollectionItem.status( CollectionItem.status(
.init(info: $0), .init(info: $0),
.init(showMoreToggled: $0.showMoreToggled)) .init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled))
} }
for loadMoreRecord in loadMoreRecords { for loadMoreRecord in loadMoreRecords {
@ -58,7 +59,9 @@ extension TimelineItemsInfo {
.map { .map {
CollectionItem.status( CollectionItem.status(
.init(info: $0), .init(info: $0),
.init(showMoreToggled: $0.showMoreToggled, isPinned: true)) .init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled,
isPinned: true))
}, },
timelineItems] timelineItems]
} else { } else {

View File

@ -10,18 +10,21 @@ public enum CollectionItem: Hashable {
public extension CollectionItem { public extension CollectionItem {
struct StatusConfiguration: Hashable { struct StatusConfiguration: Hashable {
public let showMoreToggled: Bool public let showContentToggled: Bool
public let showAttachmentsToggled: Bool
public let isContextParent: Bool public let isContextParent: Bool
public let isPinned: Bool public let isPinned: Bool
public let isReplyInContext: Bool public let isReplyInContext: Bool
public let hasReplyFollowing: Bool public let hasReplyFollowing: Bool
init(showMoreToggled: Bool, init(showContentToggled: Bool,
showAttachmentsToggled: Bool,
isContextParent: Bool = false, isContextParent: Bool = false,
isPinned: Bool = false, isPinned: Bool = false,
isReplyInContext: Bool = false, isReplyInContext: Bool = false,
hasReplyFollowing: Bool = false) { hasReplyFollowing: Bool = false) {
self.showMoreToggled = showMoreToggled self.showContentToggled = showContentToggled
self.showAttachmentsToggled = showAttachmentsToggled
self.isContextParent = isContextParent self.isContextParent = isContextParent
self.isPinned = isPinned self.isPinned = isPinned
self.isReplyInContext = isReplyInContext self.isReplyInContext = isReplyInContext
@ -31,5 +34,5 @@ public extension CollectionItem {
} }
public extension CollectionItem.StatusConfiguration { public extension CollectionItem.StatusConfiguration {
static let `default` = Self(showMoreToggled: false) static let `default` = Self(showContentToggled: false, showAttachmentsToggled: false)
} }

View File

@ -30,20 +30,5 @@ class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItemIden
return cell return cell
} }
defaultRowAnimation = .none
}
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, CollectionItemIdentifier>,
animatingDifferences: Bool = true,
completion: (() -> Void)? = nil) {
let differenceExceptShowMoreToggled = self.snapshot().itemIdentifiers.difference(
from: snapshot.itemIdentifiers,
by: CollectionItemIdentifier.isSameExceptShowMoreToggled(lhs:rhs:))
let animated = snapshot.itemIdentifiers.count > 0 && differenceExceptShowMoreToggled.count == 0
updateQueue.async {
super.apply(snapshot, animatingDifferences: animated, completion: completion)
}
} }
} }

View File

@ -11,6 +11,8 @@
"add-identity.join" = "Join"; "add-identity.join" = "Join";
"add-identity.request-invite" = "Request an invite"; "add-identity.request-invite" = "Request an invite";
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance"; "add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
"attachment.sensitive-content" = "Sensitive content";
"attachment.media-hidden" = "Media hidden";
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue"; "registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
"registration.username" = "Username"; "registration.username" = "Username";
"registration.email" = "Email"; "registration.email" = "Email";

View File

@ -41,8 +41,4 @@ extension AccountListService: CollectionService {
.flatMap { contentDatabase.append(accounts: $0.result, toList: list) } .flatMap { contentDatabase.append(accounts: $0.result, toList: list) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
contentDatabase.toggleShowMore(id: id)
}
} }

View File

@ -9,7 +9,6 @@ public protocol CollectionService {
var title: AnyPublisher<String, Never> { get } var title: AnyPublisher<String, Never> { get }
var navigationService: NavigationService { get } var navigationService: NavigationService { get }
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error>
} }
extension CollectionService { extension CollectionService {

View File

@ -32,15 +32,11 @@ extension ContextService: CollectionService {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> { public func expand(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
contentDatabase.toggleShowMore(id: id) contentDatabase.expand(ids: ids)
} }
public func showMore(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> { public func collapse(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
contentDatabase.showMore(ids: ids) contentDatabase.collapse(ids: ids)
}
public func showLess(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
contentDatabase.showLess(ids: ids)
} }
} }

View File

@ -24,8 +24,12 @@ public struct StatusService {
} }
public extension StatusService { public extension StatusService {
func toggleShowMore() -> AnyPublisher<Never, Error> { func toggleShowContent() -> AnyPublisher<Never, Error> {
contentDatabase.toggleShowMore(id: status.displayStatus.id) contentDatabase.toggleShowContent(id: status.displayStatus.id)
}
func toggleShowAttachments() -> AnyPublisher<Never, Error> {
contentDatabase.toggleShowAttachments(id: status.displayStatus.id)
} }
func toggleFavorited() -> AnyPublisher<Never, Error> { func toggleFavorited() -> AnyPublisher<Never, Error> {

View File

@ -44,8 +44,4 @@ extension TimelineService: CollectionService {
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) } .flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
contentDatabase.toggleShowMore(id: id)
}
} }

View File

@ -164,8 +164,8 @@ private extension TableViewController {
.sink { [weak self] in self?.handle(event: $0) } .sink { [weak self] in self?.handle(event: $0) }
.store(in: &cancellables) .store(in: &cancellables)
viewModel.showMoreForAll.receive(on: DispatchQueue.main) viewModel.expandAll.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.set(showMoreForAllState: $0) } .sink { [weak self] in self?.set(expandAllState: $0) }
.store(in: &cancellables) .store(in: &cancellables)
viewModel.loading.receive(on: RunLoop.main).sink { [weak self] in viewModel.loading.receive(on: RunLoop.main).sink { [weak self] in
@ -194,7 +194,7 @@ private extension TableViewController {
offsetFromNavigationBar = tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY offsetFromNavigationBar = tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
} }
self.dataSource.apply(update.items.snapshot()) { [weak self] in self.dataSource.apply(update.items.snapshot(), animatingDifferences: false) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if if
@ -241,20 +241,20 @@ private extension TableViewController {
} }
} }
func set(showMoreForAllState: ShowMoreForAllState) { func set(expandAllState: ExpandAllState) {
switch showMoreForAllState { switch expandAllState {
case .hidden: case .hidden:
navigationItem.rightBarButtonItem = nil navigationItem.rightBarButtonItem = nil
case .showMore: case .expand:
navigationItem.rightBarButtonItem = UIBarButtonItem( navigationItem.rightBarButtonItem = UIBarButtonItem(
title: NSLocalizedString("status.show-more", comment: ""), title: NSLocalizedString("status.show-more", comment: ""),
image: UIImage(systemName: "eye.slash"), image: UIImage(systemName: "eye"),
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() }) primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() })
case .showLess: case .collapse:
navigationItem.rightBarButtonItem = UIBarButtonItem( navigationItem.rightBarButtonItem = UIBarButtonItem(
title: NSLocalizedString("status.show-less", comment: ""), title: NSLocalizedString("status.show-less", comment: ""),
image: UIImage(systemName: "eye"), image: UIImage(systemName: "eye.slash"),
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() }) primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() })
} }
} }

View File

@ -15,7 +15,7 @@ final public class CollectionItemsViewModel: ObservableObject {
private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]() private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]()
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>() private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
private let loadingSubject = PassthroughSubject<Bool, Never>() private let loadingSubject = PassthroughSubject<Bool, Never>()
private let showMoreForAllSubject: CurrentValueSubject<ShowMoreForAllState, Never> private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
private var maintainScrollPosition: CollectionItemIdentifier? private var maintainScrollPosition: CollectionItemIdentifier?
private var topVisibleIndexPath = IndexPath(item: 0, section: 0) private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
private var lastSelectedLoadMore: LoadMore? private var lastSelectedLoadMore: LoadMore?
@ -24,9 +24,9 @@ final public class CollectionItemsViewModel: ObservableObject {
public init(collectionService: CollectionService, identification: Identification) { public init(collectionService: CollectionService, identification: Identification) {
self.collectionService = collectionService self.collectionService = collectionService
self.identification = identification self.identification = identification
showMoreForAllSubject = CurrentValueSubject( expandAllSubject = CurrentValueSubject(
collectionService is ContextService && !identification.identity.preferences.readingExpandSpoilers collectionService is ContextService && !identification.identity.preferences.readingExpandSpoilers
? .showMore : .hidden) ? .expand : .hidden)
collectionService.sections collectionService.sections
.handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) }) .handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) })
@ -52,8 +52,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
public var title: AnyPublisher<String, Never> { collectionService.title } public var title: AnyPublisher<String, Never> { collectionService.title }
public var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> { public var expandAll: AnyPublisher<ExpandAllState, Never> {
showMoreForAllSubject.eraseToAnyPublisher() expandAllSubject.eraseToAnyPublisher()
} }
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() } public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
@ -153,27 +153,27 @@ extension CollectionItemsViewModel: CollectionViewModel {
} }
} }
public func toggleShowMoreForAll() { public func toggleExpandAll() {
let statusIds = Set(items.value.reduce([], +).compactMap { item -> Status.Id? in let statusIds = Set(items.value.reduce([], +).compactMap { item -> Status.Id? in
guard case let .status(status, _) = item else { return nil } guard case let .status(status, _) = item else { return nil }
return status.id return status.id
}) })
switch showMoreForAllSubject.value { switch expandAllSubject.value {
case .hidden: case .hidden:
break break
case .showMore: case .expand:
(collectionService as? ContextService)?.showMore(ids: statusIds) (collectionService as? ContextService)?.expand(ids: statusIds)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.collect() .collect()
.sink { [weak self] _ in self?.showMoreForAllSubject.send(.showLess) } .sink { [weak self] _ in self?.expandAllSubject.send(.collapse) }
.store(in: &cancellables) .store(in: &cancellables)
case .showLess: case .collapse:
(collectionService as? ContextService)?.showLess(ids: statusIds) (collectionService as? ContextService)?.collapse(ids: statusIds)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.collect() .collect()
.sink { [weak self] _ in self?.showMoreForAllSubject.send(.showMore) } .sink { [weak self] _ in self?.expandAllSubject.send(.expand) }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

View File

@ -6,7 +6,7 @@ import Foundation
public protocol CollectionViewModel { public protocol CollectionViewModel {
var updates: AnyPublisher<CollectionUpdate, Never> { get } var updates: AnyPublisher<CollectionUpdate, Never> { get }
var title: AnyPublisher<String, Never> { get } var title: AnyPublisher<String, Never> { get }
var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> { get } var expandAll: AnyPublisher<ExpandAllState, Never> { get }
var alertItems: AnyPublisher<AlertItem, Never> { get } var alertItems: AnyPublisher<AlertItem, Never> { get }
var loading: AnyPublisher<Bool, Never> { get } var loading: AnyPublisher<Bool, Never> { get }
var events: AnyPublisher<CollectionItemEvent, Never> { get } var events: AnyPublisher<CollectionItemEvent, Never> { get }
@ -16,5 +16,5 @@ public protocol CollectionViewModel {
func select(indexPath: IndexPath) func select(indexPath: IndexPath)
func canSelect(indexPath: IndexPath) -> Bool func canSelect(indexPath: IndexPath) -> Bool
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
func toggleShowMoreForAll() func toggleExpandAll()
} }

View File

@ -29,17 +29,3 @@ public extension CollectionItemIdentifier {
} }
} }
} }
extension CollectionItemIdentifier {
public static func isSameExceptShowMoreToggled(lhs: Self, rhs: Self) -> Bool {
guard case let .status(lhsStatus, lhsConfiguration) = lhs.item,
case let .status(rhsStatus, rhsConfiguration) = rhs.item,
lhsStatus == rhsStatus
else { return false }
return lhsConfiguration.isContextParent == rhsConfiguration.isContextParent
&& lhsConfiguration.isPinned == rhsConfiguration.isPinned
&& lhsConfiguration.isReplyInContext == rhsConfiguration.isReplyInContext
&& lhsConfiguration.hasReplyFollowing == rhsConfiguration.hasReplyFollowing
}
}

View File

@ -1,7 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
public enum ShowMoreForAllState { public enum ExpandAllState {
case hidden case hidden
case showMore case expand
case showLess case collapse
} }

View File

@ -49,8 +49,8 @@ extension ProfileViewModel: CollectionViewModel {
$accountViewModel.compactMap { $0?.accountName }.eraseToAnyPublisher() $accountViewModel.compactMap { $0?.accountName }.eraseToAnyPublisher()
} }
public var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> { public var expandAll: AnyPublisher<ExpandAllState, Never> {
collectionViewModel.flatMap(\.showMoreForAll).eraseToAnyPublisher() collectionViewModel.flatMap(\.expandAll).eraseToAnyPublisher()
} }
public var alertItems: AnyPublisher<AlertItem, Never> { public var alertItems: AnyPublisher<AlertItem, Never> {
@ -101,7 +101,7 @@ extension ProfileViewModel: CollectionViewModel {
collectionViewModel.value.viewModel(indexPath: indexPath) collectionViewModel.value.viewModel(indexPath: indexPath)
} }
public func toggleShowMoreForAll() { public func toggleExpandAll() {
collectionViewModel.value.toggleShowMoreForAll() collectionViewModel.value.toggleExpandAll()
} }
} }

View File

@ -48,16 +48,31 @@ public struct StatusViewModel: CollectionItemViewModel {
} }
public extension StatusViewModel { public extension StatusViewModel {
var shouldShowMore: Bool { var shouldShowContent: Bool {
guard spoilerText != "" else { return true } guard spoilerText != "" else { return true }
if identification.identity.preferences.readingExpandSpoilers { if identification.identity.preferences.readingExpandSpoilers {
return !configuration.showMoreToggled return !configuration.showContentToggled
} else { } else {
return configuration.showMoreToggled return configuration.showContentToggled
} }
} }
var shouldShowAttachments: Bool {
switch identification.identity.preferences.readingExpandMedia {
case .default, .unknown:
return !sensitive || configuration.showAttachmentsToggled
case .showAll:
return !configuration.showAttachmentsToggled
case .hideAll:
return configuration.showAttachmentsToggled
}
}
var shouldShowHideAttachmentsButton: Bool {
sensitive || identification.identity.preferences.readingExpandMedia == .hideAll
}
var accountName: String { "@" + statusService.status.displayStatus.account.acct } var accountName: String { "@" + statusService.status.displayStatus.account.acct }
var avatarURL: URL { statusService.status.displayStatus.account.avatar } var avatarURL: URL { statusService.status.displayStatus.account.avatar }
@ -107,9 +122,16 @@ public extension StatusViewModel {
} }
} }
func toggleShowMore() { func toggleShowContent() {
eventsSubject.send( eventsSubject.send(
statusService.toggleShowMore() statusService.toggleShowContent()
.map { _ in CollectionItemEvent.ignorableOutput }
.eraseToAnyPublisher())
}
func toggleShowAttachments() {
eventsSubject.send(
statusService.toggleShowAttachments()
.map { _ in CollectionItemEvent.ignorableOutput } .map { _ in CollectionItemEvent.ignorableOutput }
.eraseToAnyPublisher()) .eraseToAnyPublisher())
} }

View File

@ -7,6 +7,10 @@ final class StatusAttachmentsView: UIView {
private let containerStackView = UIStackView() private let containerStackView = UIStackView()
private let leftStackView = UIStackView() private let leftStackView = UIStackView()
private let rightStackView = UIStackView() private let rightStackView = UIStackView()
private let curtain = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
private let curtainButton = UIButton(type: .system)
private let hideButtonBackground = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
private let hideButton = UIButton()
private var aspectRatioConstraint: NSLayoutConstraint? private var aspectRatioConstraint: NSLayoutConstraint?
var viewModel: StatusViewModel? { var viewModel: StatusViewModel? {
@ -47,6 +51,15 @@ final class StatusAttachmentsView: UIView {
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: newAspectRatio) aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: newAspectRatio)
aspectRatioConstraint?.priority = .justBelowMax aspectRatioConstraint?.priority = .justBelowMax
aspectRatioConstraint?.isActive = true aspectRatioConstraint?.isActive = true
curtain.isHidden = viewModel?.shouldShowAttachments ?? false
curtainButton.setTitle(
NSLocalizedString((viewModel?.sensitive ?? false)
? "attachment.sensitive-content"
: "attachment.media-hidden",
comment: ""),
for: .normal)
hideButtonBackground.isHidden = !(viewModel?.shouldShowHideAttachmentsButton ?? false)
} }
} }
@ -63,6 +76,7 @@ final class StatusAttachmentsView: UIView {
} }
private extension StatusAttachmentsView { private extension StatusAttachmentsView {
// swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
backgroundColor = .clear backgroundColor = .clear
layoutMargins = .zero layoutMargins = .zero
@ -81,11 +95,57 @@ private extension StatusAttachmentsView {
containerStackView.addArrangedSubview(leftStackView) containerStackView.addArrangedSubview(leftStackView)
containerStackView.addArrangedSubview(rightStackView) containerStackView.addArrangedSubview(rightStackView)
let toggleShowAttachmentsAction = UIAction { [weak self] _ in
self?.viewModel?.toggleShowAttachments()
}
addSubview(hideButtonBackground)
hideButtonBackground.translatesAutoresizingMaskIntoConstraints = false
hideButtonBackground.clipsToBounds = true
hideButtonBackground.layer.cornerRadius = .defaultCornerRadius
hideButton.addAction(toggleShowAttachmentsAction, for: .touchUpInside)
hideButtonBackground.contentView.addSubview(hideButton)
hideButton.translatesAutoresizingMaskIntoConstraints = false
hideButton.setImage(
UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal)
addSubview(curtain)
curtain.translatesAutoresizingMaskIntoConstraints = false
curtain.contentView.addSubview(curtainButton)
curtainButton.addAction(toggleShowAttachmentsAction, for: .touchUpInside)
curtainButton.translatesAutoresizingMaskIntoConstraints = false
curtainButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
curtainButton.titleLabel?.adjustsFontForContentSizeCategory = true
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
containerStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
containerStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
containerStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), containerStackView.topAnchor.constraint(equalTo: topAnchor),
containerStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
hideButtonBackground.topAnchor.constraint(equalTo: topAnchor, constant: .defaultSpacing),
hideButtonBackground.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .defaultSpacing),
hideButton.topAnchor.constraint(
equalTo: hideButtonBackground.contentView.topAnchor,
constant: .compactSpacing),
hideButton.leadingAnchor.constraint(
equalTo: hideButtonBackground.contentView.leadingAnchor,
constant: .compactSpacing),
hideButtonBackground.contentView.trailingAnchor.constraint(
equalTo: hideButton.trailingAnchor,
constant: .compactSpacing),
hideButtonBackground.contentView.bottomAnchor.constraint(
equalTo: hideButton.bottomAnchor,
constant: .compactSpacing),
curtain.topAnchor.constraint(equalTo: topAnchor),
curtain.leadingAnchor.constraint(equalTo: leadingAnchor),
curtain.trailingAnchor.constraint(equalTo: trailingAnchor),
curtain.bottomAnchor.constraint(equalTo: bottomAnchor),
curtainButton.topAnchor.constraint(equalTo: curtain.contentView.topAnchor),
curtainButton.leadingAnchor.constraint(equalTo: curtain.contentView.leadingAnchor),
curtainButton.trailingAnchor.constraint(equalTo: curtain.contentView.trailingAnchor),
curtainButton.bottomAnchor.constraint(equalTo: curtain.contentView.bottomAnchor)
]) ])
} }
} }

View File

@ -13,7 +13,7 @@ final class StatusView: UIView {
let accountLabel = UILabel() let accountLabel = UILabel()
let timeLabel = UILabel() let timeLabel = UILabel()
let spoilerTextLabel = UILabel() let spoilerTextLabel = UILabel()
let toggleShowMoreButton = UIButton(type: .system) let toggleShowContentButton = UIButton(type: .system)
let contentTextView = TouchFallthroughTextView() let contentTextView = TouchFallthroughTextView()
let attachmentsView = StatusAttachmentsView() let attachmentsView = StatusAttachmentsView()
let cardView = CardView() let cardView = CardView()
@ -148,12 +148,12 @@ private extension StatusView {
spoilerTextLabel.adjustsFontForContentSizeCategory = true spoilerTextLabel.adjustsFontForContentSizeCategory = true
mainStackView.addArrangedSubview(spoilerTextLabel) mainStackView.addArrangedSubview(spoilerTextLabel)
toggleShowMoreButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) toggleShowContentButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
toggleShowMoreButton.titleLabel?.adjustsFontForContentSizeCategory = true toggleShowContentButton.titleLabel?.adjustsFontForContentSizeCategory = true
toggleShowMoreButton.addAction( toggleShowContentButton.addAction(
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowMore() }, UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowContent() },
for: .touchUpInside) for: .touchUpInside)
mainStackView.addArrangedSubview(toggleShowMoreButton) mainStackView.addArrangedSubview(toggleShowContentButton)
contentTextView.adjustsFontForContentSizeCategory = true contentTextView.adjustsFontForContentSizeCategory = true
contentTextView.isScrollEnabled = false contentTextView.isScrollEnabled = false
@ -365,14 +365,14 @@ private extension StatusView {
spoilerTextLabel.font = contentFont spoilerTextLabel.font = contentFont
spoilerTextLabel.attributedText = mutableSpoilerText spoilerTextLabel.attributedText = mutableSpoilerText
spoilerTextLabel.isHidden = spoilerTextLabel.text == "" spoilerTextLabel.isHidden = spoilerTextLabel.text == ""
toggleShowMoreButton.setTitle( toggleShowContentButton.setTitle(
viewModel.shouldShowMore viewModel.shouldShowContent
? NSLocalizedString("status.show-less", comment: "") ? NSLocalizedString("status.show-less", comment: "")
: NSLocalizedString("status.show-more", comment: ""), : NSLocalizedString("status.show-more", comment: ""),
for: .normal) for: .normal)
toggleShowMoreButton.isHidden = viewModel.spoilerText == "" toggleShowContentButton.isHidden = viewModel.spoilerText == ""
contentTextView.isHidden = !viewModel.shouldShowMore contentTextView.isHidden = !viewModel.shouldShowContent
nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal
nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill