From cc370af8812395784592c9790d25706fc39500c1 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Tue, 13 Oct 2020 17:03:01 -0700 Subject: [PATCH] Sensitive attachments --- .../Content/ContentDatabase+Migration.swift | 6 +- DB/Sources/DB/Content/ContentDatabase.swift | 45 ++++++++---- DB/Sources/DB/Content/ContextItemsInfo.swift | 3 +- DB/Sources/DB/Content/StatusInfo.swift | 22 ++++-- DB/Sources/DB/Content/StatusRecord.swift | 13 ++-- .../Content/StatusShowAttachmentsToggle.swift | 25 +++++++ ...le.swift => StatusShowContentToggle.swift} | 8 +-- DB/Sources/DB/Content/TimelineItemsInfo.swift | 7 +- DB/Sources/DB/Entities/CollectionItem.swift | 11 +-- Data Sources/TableViewDataSource.swift | 15 ---- Localizations/Localizable.strings | 2 + .../Services/AccountListService.swift | 4 -- .../Services/CollectionService.swift | 1 - .../Services/ContextService.swift | 12 ++-- .../ServiceLayer/Services/StatusService.swift | 8 ++- .../Services/TimelineService.swift | 4 -- View Controllers/TableViewController.swift | 22 +++--- .../ViewModels/CollectionItemsViewModel.swift | 26 +++---- .../ViewModels/CollectionViewModel.swift | 4 +- .../Entities/CollectionItemIdentifier.swift | 14 ---- ...ForAllState.swift => ExpandAllState.swift} | 6 +- .../Sources/ViewModels/ProfileViewModel.swift | 8 +-- .../Sources/ViewModels/StatusViewModel.swift | 32 +++++++-- Views/Status/StatusAttachmentsView.swift | 68 +++++++++++++++++-- Views/Status/StatusView.swift | 20 +++--- 25 files changed, 250 insertions(+), 136 deletions(-) create mode 100644 DB/Sources/DB/Content/StatusShowAttachmentsToggle.swift rename DB/Sources/DB/Content/{StatusShowMoreToggle.swift => StatusShowContentToggle.swift} (60%) rename ViewModels/Sources/ViewModels/Entities/{ShowMoreForAllState.swift => ExpandAllState.swift} (51%) diff --git a/DB/Sources/DB/Content/ContentDatabase+Migration.swift b/DB/Sources/DB/Content/ContentDatabase+Migration.swift index 8a1ed72..588ae40 100644 --- a/DB/Sources/DB/Content/ContentDatabase+Migration.swift +++ b/DB/Sources/DB/Content/ContentDatabase+Migration.swift @@ -62,7 +62,11 @@ extension ContentDatabase { 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) } diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 05a825b..9082f1b 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -149,37 +149,56 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func toggleShowMore(id: Status.Id) -> AnyPublisher { + func toggleShowContent(id: Status.Id) -> AnyPublisher { databaseWriter.writePublisher { - if let toggle = try StatusShowMoreToggle - .filter(StatusShowMoreToggle.Columns.statusId == id) + if let toggle = try StatusShowContentToggle + .filter(StatusShowContentToggle.Columns.statusId == id) .fetchOne($0) { try toggle.delete($0) } else { - try StatusShowMoreToggle(statusId: id).save($0) + try StatusShowContentToggle(statusId: id).save($0) } } .ignoreOutput() .eraseToAnyPublisher() } - func showMore(ids: Set) -> AnyPublisher { + func toggleShowAttachments(id: Status.Id) -> AnyPublisher { + 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) -> AnyPublisher { databaseWriter.writePublisher { for id in ids { - try StatusShowMoreToggle(statusId: id).save($0) + try StatusShowContentToggle(statusId: id).save($0) + try StatusShowAttachmentsToggle(statusId: id).save($0) } } .ignoreOutput() .eraseToAnyPublisher() } - func showLess(ids: Set) -> AnyPublisher { - databaseWriter.writePublisher( - updates: StatusShowMoreToggle - .filter(ids.contains(StatusShowMoreToggle.Columns.statusId)) - .deleteAll) - .ignoreOutput() - .eraseToAnyPublisher() + func collapse(ids: Set) -> AnyPublisher { + databaseWriter.writePublisher { + try StatusShowContentToggle + .filter(ids.contains(StatusShowContentToggle.Columns.statusId)) + .deleteAll($0) + try StatusShowAttachmentsToggle + .filter(ids.contains(StatusShowContentToggle.Columns.statusId)) + .deleteAll($0) + } + .ignoreOutput() + .eraseToAnyPublisher() } func append(accounts: [Account], toList list: AccountList) -> AnyPublisher { diff --git a/DB/Sources/DB/Content/ContextItemsInfo.swift b/DB/Sources/DB/Content/ContextItemsInfo.swift index fe0d9f2..9ec2517 100644 --- a/DB/Sources/DB/Content/ContextItemsInfo.swift +++ b/DB/Sources/DB/Content/ContextItemsInfo.swift @@ -35,7 +35,8 @@ extension ContextItemsInfo { return .status( .init(info: statusInfo), - .init(showMoreToggled: statusInfo.showMoreToggled, + .init(showContentToggled: statusInfo.showContentToggled, + showAttachmentsToggled: statusInfo.showAttachmentsToggled, isContextParent: statusInfo.record.id == parent.record.id, isReplyInContext: isReplyInContext, hasReplyFollowing: hasReplyFollowing)) diff --git a/DB/Sources/DB/Content/StatusInfo.swift b/DB/Sources/DB/Content/StatusInfo.swift index 651f506..b2fdc2f 100644 --- a/DB/Sources/DB/Content/StatusInfo.swift +++ b/DB/Sources/DB/Content/StatusInfo.swift @@ -8,8 +8,10 @@ struct StatusInfo: Codable, Hashable, FetchableRecord { let accountInfo: AccountInfo let reblogAccountInfo: AccountInfo? let reblogRecord: StatusRecord? - let showMoreToggle: StatusShowMoreToggle? - let reblogShowMoreToggle: StatusShowMoreToggle? + let showContentToggle: StatusShowContentToggle? + let reblogShowContentToggle: StatusShowContentToggle? + let showAttachmentsToggle: StatusShowAttachmentsToggle? + let reblogShowAttachmentsToggle: StatusShowAttachmentsToggle? } extension StatusInfo { @@ -18,9 +20,11 @@ extension StatusInfo { .including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount) .forKey(CodingKeys.reblogAccountInfo)) .including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord)) - .including(optional: StatusRecord.showMoreToggle.forKey(CodingKeys.showMoreToggle)) - .including(optional: StatusRecord.reblogShowMoreToggle - .forKey(CodingKeys.reblogShowMoreToggle)) + .including(optional: StatusRecord.showContentToggle.forKey(CodingKeys.showContentToggle)) + .including(optional: StatusRecord.reblogShowContentToggle.forKey(CodingKeys.reblogShowContentToggle)) + .including(optional: StatusRecord.showAttachmentsToggle.forKey(CodingKeys.showAttachmentsToggle)) + .including(optional: StatusRecord.reblogShowAttachmentsToggle + .forKey(CodingKeys.reblogShowAttachmentsToggle)) } static func request(_ request: QueryInterfaceRequest) -> QueryInterfaceRequest { @@ -31,7 +35,11 @@ extension StatusInfo { (record.filterableContent + (reblogRecord?.filterableContent ?? [])).joined(separator: " ") } - var showMoreToggled: Bool { - showMoreToggle != nil || reblogShowMoreToggle != nil + var showContentToggled: Bool { + showContentToggle != nil || reblogShowContentToggle != nil + } + + var showAttachmentsToggled: Bool { + showAttachmentsToggle != nil || reblogShowAttachmentsToggle != nil } } diff --git a/DB/Sources/DB/Content/StatusRecord.swift b/DB/Sources/DB/Content/StatusRecord.swift index 97d023f..9d71421 100644 --- a/DB/Sources/DB/Content/StatusRecord.swift +++ b/DB/Sources/DB/Content/StatusRecord.swift @@ -92,11 +92,16 @@ extension StatusRecord { through: Self.reblogAccount, using: AccountRecord.moved) static let reblog = belongsTo(StatusRecord.self) - static let showMoreToggle = hasOne(StatusShowMoreToggle.self) - static let reblogShowMoreToggle = hasOne( - StatusShowMoreToggle.self, + static let showContentToggle = hasOne(StatusShowContentToggle.self) + static let reblogShowContentToggle = hasOne( + StatusShowContentToggle.self, 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( StatusAncestorJoin.self, using: ForeignKey([StatusAncestorJoin.Columns.parentId])) diff --git a/DB/Sources/DB/Content/StatusShowAttachmentsToggle.swift b/DB/Sources/DB/Content/StatusShowAttachmentsToggle.swift new file mode 100644 index 0000000..b5d562f --- /dev/null +++ b/DB/Sources/DB/Content/StatusShowAttachmentsToggle.swift @@ -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() + } +} diff --git a/DB/Sources/DB/Content/StatusShowMoreToggle.swift b/DB/Sources/DB/Content/StatusShowContentToggle.swift similarity index 60% rename from DB/Sources/DB/Content/StatusShowMoreToggle.swift rename to DB/Sources/DB/Content/StatusShowContentToggle.swift index 2db6b18..b9277a2 100644 --- a/DB/Sources/DB/Content/StatusShowMoreToggle.swift +++ b/DB/Sources/DB/Content/StatusShowContentToggle.swift @@ -4,17 +4,17 @@ import Foundation import GRDB import Mastodon -struct StatusShowMoreToggle: Codable, Hashable { +struct StatusShowContentToggle: Codable, Hashable { let statusId: Status.Id } -extension StatusShowMoreToggle { +extension StatusShowContentToggle { 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 { MastodonDecoder() } diff --git a/DB/Sources/DB/Content/TimelineItemsInfo.swift b/DB/Sources/DB/Content/TimelineItemsInfo.swift index bcbbc81..b0c054a 100644 --- a/DB/Sources/DB/Content/TimelineItemsInfo.swift +++ b/DB/Sources/DB/Content/TimelineItemsInfo.swift @@ -35,7 +35,8 @@ extension TimelineItemsInfo { .map { CollectionItem.status( .init(info: $0), - .init(showMoreToggled: $0.showMoreToggled)) + .init(showContentToggled: $0.showContentToggled, + showAttachmentsToggled: $0.showAttachmentsToggled)) } for loadMoreRecord in loadMoreRecords { @@ -58,7 +59,9 @@ extension TimelineItemsInfo { .map { CollectionItem.status( .init(info: $0), - .init(showMoreToggled: $0.showMoreToggled, isPinned: true)) + .init(showContentToggled: $0.showContentToggled, + showAttachmentsToggled: $0.showAttachmentsToggled, + isPinned: true)) }, timelineItems] } else { diff --git a/DB/Sources/DB/Entities/CollectionItem.swift b/DB/Sources/DB/Entities/CollectionItem.swift index e00c840..ecd4aa3 100644 --- a/DB/Sources/DB/Entities/CollectionItem.swift +++ b/DB/Sources/DB/Entities/CollectionItem.swift @@ -10,18 +10,21 @@ public enum CollectionItem: Hashable { public extension CollectionItem { struct StatusConfiguration: Hashable { - public let showMoreToggled: Bool + public let showContentToggled: Bool + public let showAttachmentsToggled: Bool public let isContextParent: Bool public let isPinned: Bool public let isReplyInContext: Bool public let hasReplyFollowing: Bool - init(showMoreToggled: Bool, + init(showContentToggled: Bool, + showAttachmentsToggled: Bool, isContextParent: Bool = false, isPinned: Bool = false, isReplyInContext: Bool = false, hasReplyFollowing: Bool = false) { - self.showMoreToggled = showMoreToggled + self.showContentToggled = showContentToggled + self.showAttachmentsToggled = showAttachmentsToggled self.isContextParent = isContextParent self.isPinned = isPinned self.isReplyInContext = isReplyInContext @@ -31,5 +34,5 @@ public extension CollectionItem { } public extension CollectionItem.StatusConfiguration { - static let `default` = Self(showMoreToggled: false) + static let `default` = Self(showContentToggled: false, showAttachmentsToggled: false) } diff --git a/Data Sources/TableViewDataSource.swift b/Data Sources/TableViewDataSource.swift index f5fcfd1..76a45f4 100644 --- a/Data Sources/TableViewDataSource.swift +++ b/Data Sources/TableViewDataSource.swift @@ -30,20 +30,5 @@ class TableViewDataSource: UITableViewDiffableDataSource, - 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) - } } } diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 7f73efa..8c693f2 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -11,6 +11,8 @@ "add-identity.join" = "Join"; "add-identity.request-invite" = "Request an invite"; "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.username" = "Username"; "registration.email" = "Email"; diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 1c7415a..56f6471 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -41,8 +41,4 @@ extension AccountListService: CollectionService { .flatMap { contentDatabase.append(accounts: $0.result, toList: list) } .eraseToAnyPublisher() } - - public func toggleShowMore(id: Status.Id) -> AnyPublisher { - contentDatabase.toggleShowMore(id: id) - } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift index 76eb92a..4e4e2ff 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift @@ -9,7 +9,6 @@ public protocol CollectionService { var title: AnyPublisher { get } var navigationService: NavigationService { get } func request(maxId: String?, minId: String?) -> AnyPublisher - func toggleShowMore(id: Status.Id) -> AnyPublisher } extension CollectionService { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift index 24ec9dc..5b5eb30 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift @@ -32,15 +32,11 @@ extension ContextService: CollectionService { .eraseToAnyPublisher() } - public func toggleShowMore(id: Status.Id) -> AnyPublisher { - contentDatabase.toggleShowMore(id: id) + public func expand(ids: Set) -> AnyPublisher { + contentDatabase.expand(ids: ids) } - public func showMore(ids: Set) -> AnyPublisher { - contentDatabase.showMore(ids: ids) - } - - public func showLess(ids: Set) -> AnyPublisher { - contentDatabase.showLess(ids: ids) + public func collapse(ids: Set) -> AnyPublisher { + contentDatabase.collapse(ids: ids) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift b/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift index 872d3a9..133f7b2 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift @@ -24,8 +24,12 @@ public struct StatusService { } public extension StatusService { - func toggleShowMore() -> AnyPublisher { - contentDatabase.toggleShowMore(id: status.displayStatus.id) + func toggleShowContent() -> AnyPublisher { + contentDatabase.toggleShowContent(id: status.displayStatus.id) + } + + func toggleShowAttachments() -> AnyPublisher { + contentDatabase.toggleShowAttachments(id: status.displayStatus.id) } func toggleFavorited() -> AnyPublisher { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift index ca97727..e1cc3bb 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift @@ -44,8 +44,4 @@ extension TimelineService: CollectionService { .flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) } .eraseToAnyPublisher() } - - public func toggleShowMore(id: Status.Id) -> AnyPublisher { - contentDatabase.toggleShowMore(id: id) - } } diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 1f6ffae..836eb97 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -164,8 +164,8 @@ private extension TableViewController { .sink { [weak self] in self?.handle(event: $0) } .store(in: &cancellables) - viewModel.showMoreForAll.receive(on: DispatchQueue.main) - .sink { [weak self] in self?.set(showMoreForAllState: $0) } + viewModel.expandAll.receive(on: DispatchQueue.main) + .sink { [weak self] in self?.set(expandAllState: $0) } .store(in: &cancellables) 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 } - 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 } if @@ -241,20 +241,20 @@ private extension TableViewController { } } - func set(showMoreForAllState: ShowMoreForAllState) { - switch showMoreForAllState { + func set(expandAllState: ExpandAllState) { + switch expandAllState { case .hidden: navigationItem.rightBarButtonItem = nil - case .showMore: + case .expand: navigationItem.rightBarButtonItem = UIBarButtonItem( title: NSLocalizedString("status.show-more", comment: ""), - image: UIImage(systemName: "eye.slash"), - primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() }) - case .showLess: + image: UIImage(systemName: "eye"), + primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() }) + case .collapse: navigationItem.rightBarButtonItem = UIBarButtonItem( title: NSLocalizedString("status.show-less", comment: ""), - image: UIImage(systemName: "eye"), - primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() }) + image: UIImage(systemName: "eye.slash"), + primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleExpandAll() }) } } diff --git a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift index bed5e98..1931913 100644 --- a/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/CollectionItemsViewModel.swift @@ -15,7 +15,7 @@ final public class CollectionItemsViewModel: ObservableObject { private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]() private let eventsSubject = PassthroughSubject() private let loadingSubject = PassthroughSubject() - private let showMoreForAllSubject: CurrentValueSubject + private let expandAllSubject: CurrentValueSubject private var maintainScrollPosition: CollectionItemIdentifier? private var topVisibleIndexPath = IndexPath(item: 0, section: 0) private var lastSelectedLoadMore: LoadMore? @@ -24,9 +24,9 @@ final public class CollectionItemsViewModel: ObservableObject { public init(collectionService: CollectionService, identification: Identification) { self.collectionService = collectionService self.identification = identification - showMoreForAllSubject = CurrentValueSubject( + expandAllSubject = CurrentValueSubject( collectionService is ContextService && !identification.identity.preferences.readingExpandSpoilers - ? .showMore : .hidden) + ? .expand : .hidden) collectionService.sections .handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) }) @@ -52,8 +52,8 @@ extension CollectionItemsViewModel: CollectionViewModel { public var title: AnyPublisher { collectionService.title } - public var showMoreForAll: AnyPublisher { - showMoreForAllSubject.eraseToAnyPublisher() + public var expandAll: AnyPublisher { + expandAllSubject.eraseToAnyPublisher() } public var alertItems: AnyPublisher { $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 guard case let .status(status, _) = item else { return nil } return status.id }) - switch showMoreForAllSubject.value { + switch expandAllSubject.value { case .hidden: break - case .showMore: - (collectionService as? ContextService)?.showMore(ids: statusIds) + case .expand: + (collectionService as? ContextService)?.expand(ids: statusIds) .assignErrorsToAlertItem(to: \.alertItem, on: self) .collect() - .sink { [weak self] _ in self?.showMoreForAllSubject.send(.showLess) } + .sink { [weak self] _ in self?.expandAllSubject.send(.collapse) } .store(in: &cancellables) - case .showLess: - (collectionService as? ContextService)?.showLess(ids: statusIds) + case .collapse: + (collectionService as? ContextService)?.collapse(ids: statusIds) .assignErrorsToAlertItem(to: \.alertItem, on: self) .collect() - .sink { [weak self] _ in self?.showMoreForAllSubject.send(.showMore) } + .sink { [weak self] _ in self?.expandAllSubject.send(.expand) } .store(in: &cancellables) } } diff --git a/ViewModels/Sources/ViewModels/CollectionViewModel.swift b/ViewModels/Sources/ViewModels/CollectionViewModel.swift index 4e793e4..4ce86b0 100644 --- a/ViewModels/Sources/ViewModels/CollectionViewModel.swift +++ b/ViewModels/Sources/ViewModels/CollectionViewModel.swift @@ -6,7 +6,7 @@ import Foundation public protocol CollectionViewModel { var updates: AnyPublisher { get } var title: AnyPublisher { get } - var showMoreForAll: AnyPublisher { get } + var expandAll: AnyPublisher { get } var alertItems: AnyPublisher { get } var loading: AnyPublisher { get } var events: AnyPublisher { get } @@ -16,5 +16,5 @@ public protocol CollectionViewModel { func select(indexPath: IndexPath) func canSelect(indexPath: IndexPath) -> Bool func viewModel(indexPath: IndexPath) -> CollectionItemViewModel - func toggleShowMoreForAll() + func toggleExpandAll() } diff --git a/ViewModels/Sources/ViewModels/Entities/CollectionItemIdentifier.swift b/ViewModels/Sources/ViewModels/Entities/CollectionItemIdentifier.swift index 42e0d19..719f84c 100644 --- a/ViewModels/Sources/ViewModels/Entities/CollectionItemIdentifier.swift +++ b/ViewModels/Sources/ViewModels/Entities/CollectionItemIdentifier.swift @@ -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 - } -} diff --git a/ViewModels/Sources/ViewModels/Entities/ShowMoreForAllState.swift b/ViewModels/Sources/ViewModels/Entities/ExpandAllState.swift similarity index 51% rename from ViewModels/Sources/ViewModels/Entities/ShowMoreForAllState.swift rename to ViewModels/Sources/ViewModels/Entities/ExpandAllState.swift index c26fae7..d3c0d1a 100644 --- a/ViewModels/Sources/ViewModels/Entities/ShowMoreForAllState.swift +++ b/ViewModels/Sources/ViewModels/Entities/ExpandAllState.swift @@ -1,7 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. -public enum ShowMoreForAllState { +public enum ExpandAllState { case hidden - case showMore - case showLess + case expand + case collapse } diff --git a/ViewModels/Sources/ViewModels/ProfileViewModel.swift b/ViewModels/Sources/ViewModels/ProfileViewModel.swift index 8476976..7dd9600 100644 --- a/ViewModels/Sources/ViewModels/ProfileViewModel.swift +++ b/ViewModels/Sources/ViewModels/ProfileViewModel.swift @@ -49,8 +49,8 @@ extension ProfileViewModel: CollectionViewModel { $accountViewModel.compactMap { $0?.accountName }.eraseToAnyPublisher() } - public var showMoreForAll: AnyPublisher { - collectionViewModel.flatMap(\.showMoreForAll).eraseToAnyPublisher() + public var expandAll: AnyPublisher { + collectionViewModel.flatMap(\.expandAll).eraseToAnyPublisher() } public var alertItems: AnyPublisher { @@ -101,7 +101,7 @@ extension ProfileViewModel: CollectionViewModel { collectionViewModel.value.viewModel(indexPath: indexPath) } - public func toggleShowMoreForAll() { - collectionViewModel.value.toggleShowMoreForAll() + public func toggleExpandAll() { + collectionViewModel.value.toggleExpandAll() } } diff --git a/ViewModels/Sources/ViewModels/StatusViewModel.swift b/ViewModels/Sources/ViewModels/StatusViewModel.swift index 70def88..5c64b83 100644 --- a/ViewModels/Sources/ViewModels/StatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/StatusViewModel.swift @@ -48,16 +48,31 @@ public struct StatusViewModel: CollectionItemViewModel { } public extension StatusViewModel { - var shouldShowMore: Bool { + var shouldShowContent: Bool { guard spoilerText != "" else { return true } if identification.identity.preferences.readingExpandSpoilers { - return !configuration.showMoreToggled + return !configuration.showContentToggled } 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 avatarURL: URL { statusService.status.displayStatus.account.avatar } @@ -107,9 +122,16 @@ public extension StatusViewModel { } } - func toggleShowMore() { + func toggleShowContent() { eventsSubject.send( - statusService.toggleShowMore() + statusService.toggleShowContent() + .map { _ in CollectionItemEvent.ignorableOutput } + .eraseToAnyPublisher()) + } + + func toggleShowAttachments() { + eventsSubject.send( + statusService.toggleShowAttachments() .map { _ in CollectionItemEvent.ignorableOutput } .eraseToAnyPublisher()) } diff --git a/Views/Status/StatusAttachmentsView.swift b/Views/Status/StatusAttachmentsView.swift index d038868..ab3a96d 100644 --- a/Views/Status/StatusAttachmentsView.swift +++ b/Views/Status/StatusAttachmentsView.swift @@ -7,6 +7,10 @@ final class StatusAttachmentsView: UIView { private let containerStackView = UIStackView() private let leftStackView = 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? var viewModel: StatusViewModel? { @@ -47,6 +51,15 @@ final class StatusAttachmentsView: UIView { aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: newAspectRatio) aspectRatioConstraint?.priority = .justBelowMax 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 { + // swiftlint:disable:next function_body_length func initialSetup() { backgroundColor = .clear layoutMargins = .zero @@ -81,11 +95,57 @@ private extension StatusAttachmentsView { containerStackView.addArrangedSubview(leftStackView) 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([ - containerStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - containerStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), - containerStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + containerStackView.topAnchor.constraint(equalTo: topAnchor), + 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) ]) } } diff --git a/Views/Status/StatusView.swift b/Views/Status/StatusView.swift index 0c9b5d1..c0ae5c6 100644 --- a/Views/Status/StatusView.swift +++ b/Views/Status/StatusView.swift @@ -13,7 +13,7 @@ final class StatusView: UIView { let accountLabel = UILabel() let timeLabel = UILabel() let spoilerTextLabel = UILabel() - let toggleShowMoreButton = UIButton(type: .system) + let toggleShowContentButton = UIButton(type: .system) let contentTextView = TouchFallthroughTextView() let attachmentsView = StatusAttachmentsView() let cardView = CardView() @@ -148,12 +148,12 @@ private extension StatusView { spoilerTextLabel.adjustsFontForContentSizeCategory = true mainStackView.addArrangedSubview(spoilerTextLabel) - toggleShowMoreButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) - toggleShowMoreButton.titleLabel?.adjustsFontForContentSizeCategory = true - toggleShowMoreButton.addAction( - UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowMore() }, + toggleShowContentButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + toggleShowContentButton.titleLabel?.adjustsFontForContentSizeCategory = true + toggleShowContentButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowContent() }, for: .touchUpInside) - mainStackView.addArrangedSubview(toggleShowMoreButton) + mainStackView.addArrangedSubview(toggleShowContentButton) contentTextView.adjustsFontForContentSizeCategory = true contentTextView.isScrollEnabled = false @@ -365,14 +365,14 @@ private extension StatusView { spoilerTextLabel.font = contentFont spoilerTextLabel.attributedText = mutableSpoilerText spoilerTextLabel.isHidden = spoilerTextLabel.text == "" - toggleShowMoreButton.setTitle( - viewModel.shouldShowMore + toggleShowContentButton.setTitle( + viewModel.shouldShowContent ? NSLocalizedString("status.show-less", comment: "") : NSLocalizedString("status.show-more", comment: ""), for: .normal) - toggleShowMoreButton.isHidden = viewModel.spoilerText == "" + toggleShowContentButton.isHidden = viewModel.spoilerText == "" - contentTextView.isHidden = !viewModel.shouldShowMore + contentTextView.isHidden = !viewModel.shouldShowContent nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill