From caaf66286f17595802eabf00be8eef2ce38f844e Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 29 Jan 2022 17:02:30 +0800 Subject: [PATCH] feat: add content warning for post spoiler --- Localization/app.json | 6 + Mastodon.xcodeproj/project.pbxproj | 2 + .../xcschemes/xcschememanagement.plist | 4 +- .../Provider/DataSourceFacade+Status.swift | 33 ++++ ...Provider+StatusTableViewCellDelegate.swift | 27 +++ .../Notification/NotificationViewModel.swift | 64 ------- .../Search/Search/SearchViewController.swift | 1 + .../Content/StatusView+Configuration.swift | 49 ++++-- .../StatusTableViewCell+ViewModel.swift | 18 +- .../StatusTableViewCellDelegate.swift | 5 + ...tusThreadRootTableViewCell+ViewModel.swift | 17 +- .../Thread/ThreadViewModel+Diffable.swift | 2 +- MastodonSDK/Package.swift | 8 +- .../CoreData 3.xcdatamodel/contents | 4 +- .../Entity/Mastodon/Status.swift | 18 +- .../eye.circle.fill.imageset/Contents.json | 15 ++ .../eye.circle.fill.pdf | 125 +++++++++++++ .../Contents.json | 15 ++ .../eye.slash.circle.fill.pdf | 139 +++++++++++++++ .../MastodonAsset/Generated/Assets.swift | 2 + .../MastodonAsset/Generated/Fonts.swift | 4 + .../MastodonUI/Extension/MetaLabel.swift | 7 + .../View/Content/NotificationView.swift | 4 +- .../View/Content/StatusView+ViewModel.swift | 164 ++++++++++++++---- .../MastodonUI/View/Content/StatusView.swift | 69 +++++++- .../View/Control/SpoilerOverlayView.swift | 90 ++++++++++ .../View/Control/StatusVisibilityView.swift | 74 ++++++++ .../View/{ => Menu}/MastodonMenu.swift | 0 swiftgen.yml | 8 + 29 files changed, 842 insertions(+), 132 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/eye.slash.circle.fill.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Control/StatusVisibilityView.swift rename MastodonSDK/Sources/MastodonUI/View/{ => Menu}/MastodonMenu.swift (100%) diff --git a/Localization/app.json b/Localization/app.json index b6da7c4e..2e948f3e 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -149,6 +149,12 @@ "hashtag": "Hashtag", "email": "Email", "emoji": "Emoji" + }, + "visibility": { + "unlisted": "Everyone can see this post but not display in the public timeline.", + "private": "Only their followers can see this post.", + "private_from_me": "Only my followers can see this post.", + "direct": "Only mentioned user can see this post." } }, "friendship": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d0439c81..9bc2abe2 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -1315,6 +1315,7 @@ DBDC1CFD272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ku-TR"; path = "ku-TR.lproj/Intents.stringsdict"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = ""; }; + DBE3CA7127A3F23D00AFE27B /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderTableViewCell.swift; sourceTree = ""; }; DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = ""; }; DBE3CDEB261C6B2900430CC6 /* FavoriteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewController.swift; sourceTree = ""; }; @@ -2111,6 +2112,7 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, + DBE3CA7127A3F23D00AFE27B /* MetaTextKit */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index bf3ad9bf..6d24f2a7 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ AppShared.xcscheme_^#shared#^_ orderHint - 19 + 21 CoreDataStack.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 18 + 19 MastodonIntents.xcscheme_^#shared#^_ diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 5d3b0695..2861730a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -257,3 +257,36 @@ extension DataSourceFacade { } } // end func } + +extension DataSourceFacade { + + static func responseToToggleSensitiveAction( + dependency: NeedsDependency, + status: ManagedObjectRecord + ) async throws { + let managedObjectContext = dependency.context.managedObjectContext + try await managedObjectContext.performChanges { + guard let _status = status.object(in: managedObjectContext) else { return } + let status = _status.reblog ?? _status + + let isToggled = status.isContentSensitiveToggled || status.isMediaSensitiveToggled + + status.update(isContentSensitiveToggled: !isToggled) + status.update(isMediaSensitiveToggled: !isToggled) + } + } + + static func responseToToggleMediaSensitiveAction( + dependency: NeedsDependency, + status: ManagedObjectRecord + ) async throws { + let managedObjectContext = dependency.context.managedObjectContext + try await managedObjectContext.performChanges { + guard let _status = status.object(in: managedObjectContext) else { return } + let status = _status.reblog ?? _status + + status.update(isMediaSensitiveToggled: !status.isMediaSensitiveToggled) + } + } + +} diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 9c5d3912..cd167cb9 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -299,6 +299,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } +// MARK: - menu button extension StatusTableViewCellDelegate where Self: DataSourceProvider { func tableViewCell( _ cell: UITableViewCell, @@ -342,3 +343,29 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } } + +// MARK: - content warning +extension StatusTableViewCellDelegate where Self: DataSourceProvider { + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + contentWarningToggleButtonDidPressed button: UIButton + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + try await DataSourceFacade.responseToToggleSensitiveAction( + dependency: self, + status: status + ) + } // end Task + } +} + diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 46891ef8..3d4fa604 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -19,18 +19,6 @@ final class NotificationViewModel { // input let context: AppContext let viewDidLoad = PassthroughSubject() -// let selectedIndex = CurrentValueSubject(.everyThing) -// let noMoreNotification = CurrentValueSubject(false) - -// let activeMastodonAuthenticationBox: CurrentValueSubject -// let fetchedResultsController: NSFetchedResultsController! -// let notificationPredicate = CurrentValueSubject(nil) -// let cellFrameCache = NSCache() - -// var needsScrollToTopAfterDataSourceUpdate = false -// let dataSourceDidUpdated = PassthroughSubject() -// let isFetchingLatestNotification = CurrentValueSubject(false) - // output let scopes = NotificationTimelineViewModel.Scope.allCases @@ -40,59 +28,7 @@ final class NotificationViewModel { init(context: AppContext) { self.context = context -// self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) -// self.fetchedResultsController = { -// let fetchRequest = MastodonNotification.sortedFetchRequest -// fetchRequest.returnsObjectsAsFaults = false -// fetchRequest.fetchBatchSize = 10 -// fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)] -// let controller = NSFetchedResultsController( -// fetchRequest: fetchRequest, -// managedObjectContext: context.managedObjectContext, -// sectionNameKeyPath: nil, -// cacheName: nil -// ) -// -// return controller -// }() // end init - -// fetchedResultsController.delegate = self -// context.authenticationService.activeMastodonAuthenticationBox -// .sink(receiveValue: { [weak self] box in -// guard let self = self else { return } -// self.activeMastodonAuthenticationBox.value = box -// if let domain = box?.domain, let userID = box?.userID { -// self.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) -// } -// }) -// .store(in: &disposeBag) - -// notificationPredicate -// .compactMap { $0 } -// .sink { [weak self] predicate in -// guard let self = self else { return } -// self.fetchedResultsController.fetchRequest.predicate = predicate -// do { -// self.diffableDataSource?.defaultRowAnimation = .fade -// try self.fetchedResultsController.performFetch() -// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in -// guard let self = self else { return } -// self.diffableDataSource?.defaultRowAnimation = .automatic -// } -// } catch { -// assertionFailure(error.localizedDescription) -// } -// } -// .store(in: &disposeBag) - -// viewDidLoad -// .sink { [weak self] in -// -// guard let domain = self?.activeMastodonAuthenticationBox.value?.domain, let userID = self?.activeMastodonAuthenticationBox.value?.userID else { return } -// self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) -// } -// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index ebfa1584..d1bed948 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -42,6 +42,7 @@ final class SearchViewController: UIViewController, NeedsDependency { configuration.headerMode = .supplementary let layout = UICollectionViewCompositionalLayout.list(using: configuration) let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear return collectionView }() diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift index 1605a993..8530fe24 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift @@ -48,7 +48,7 @@ extension StatusView { configureContent(status: status) configureMedia(status: status) configurePoll(status: status) - configureToolbar(status: status) + configureToolbar(status: status) } } @@ -235,33 +235,42 @@ extension StatusView { private func configureContent(status: Status) { let status = status.reblog ?? status + + // spoilerText + if let spoilerText = status.spoilerText, !spoilerText.isEmpty { + do { + let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + viewModel.spoilerContent = metaContent + } catch { + assertionFailure(error.localizedDescription) + viewModel.spoilerContent = PlaintextMetaContent(string: "") + } + } else { + viewModel.spoilerContent = nil + } + // content do { let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent - // viewModel.sharePlaintextContent = metaContent.original } catch { assertionFailure(error.localizedDescription) viewModel.content = PlaintextMetaContent(string: "") } + // visibility + status.publisher(for: \.visibilityRaw) + .compactMap { MastodonVisibility(rawValue: $0) } + .assign(to: \.visibility, on: viewModel) + .store(in: &disposeBag) + // sensitive + status.publisher(for: \.isContentSensitiveToggled) + .assign(to: \.isContentSensitiveToggled, on: viewModel) + .store(in: &disposeBag) + status.publisher(for: \.isMediaSensitiveToggled) + .assign(to: \.isMediaSensitiveToggled, on: viewModel) + .store(in: &disposeBag) -// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { -// do { -// let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: content) -// viewModel.spoilerContent = metaContent -// } catch { -// assertionFailure() -// viewModel.spoilerContent = nil -// } -// } else { -// viewModel.spoilerContent = nil -// } - -// status.publisher(for: \.isContentReveal) -// .assign(to: \.isContentReveal, on: viewModel) -// .store(in: &disposeBag) -// // viewModel.source = status.source } @@ -271,6 +280,8 @@ extension StatusView { // mediaGridContainerView.viewModel.resetContentWarningOverlay() // viewModel.isMediaSensitiveSwitchable = true + viewModel.isMediaSensitive = status.sensitive + MediaView.configuration(status: status) .assign(to: \.mediaViewConfigurations, on: viewModel) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index 87c01b18..9a535e9b 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift @@ -35,7 +35,7 @@ extension StatusTableViewCell { statusView.frame.size.width = tableView.frame.width logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") } - + switch viewModel.value { case .feed(let feed): statusView.configure(feed: feed) @@ -51,7 +51,21 @@ extension StatusTableViewCell { statusView.configure(status: status) } - self.delegate = delegate + self.delegate = delegate + + + statusView.viewModel.$isContentReveal + .removeDuplicates() + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak tableView, weak self] isContentReveal in + guard let tableView = tableView else { return } + guard let self = self else { return } + + tableView.beginUpdates() + tableView.endUpdates() + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift index dcbf41f4..322441ba 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift @@ -31,6 +31,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) // sourcery:end } @@ -70,5 +71,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { func statusView(_ statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) { delegate?.tableViewCell(self, statusView: statusView, menuButton: button, didSelectAction: action) } + + func statusView(_ statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) { + delegate?.tableViewCell(self, statusView: statusView, contentWarningToggleButtonDidPressed: button) + } // sourcery:end } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift index d549ba1f..240736fd 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -40,7 +40,22 @@ extension StatusThreadRootTableViewCell { statusView.configure(status: status) } - self.delegate = delegate + self.delegate = delegate + + statusView.viewModel.$isContentReveal + .removeDuplicates() + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak tableView, weak self] isContentReveal in + guard let tableView = tableView else { return } + guard let self = self else { return } + + guard self.contentView.window != nil else { return } + + tableView.beginUpdates() + tableView.endUpdates() + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 71dd003f..07966713 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -42,7 +42,7 @@ extension ThreadViewModel { } else { } - diffableDataSource?.apply(snapshot) + diffableDataSource?.apply(snapshot, animatingDifferences: false) $threadContext .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 093f592f..aba25501 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -46,11 +46,17 @@ let package = Package( name: "CoreDataStack", dependencies: [ "MastodonCommon", + ], + exclude: [ + "Template/Stencil" ] ), .target( name: "MastodonAsset", - dependencies: [] + dependencies: [], + resources: [ + .process("Font"), + ] ), .target( name: "MastodonCommon", diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 3.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 3.xcdatamodel/contents index 6cb8e91b..9f6f3ce1 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 3.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 3.xcdatamodel/contents @@ -199,6 +199,8 @@ + + @@ -275,7 +277,7 @@ - + diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift index c506536d..c9946b3f 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift @@ -41,6 +41,11 @@ public final class Status: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var spoilerText: String? + // sourcery: autoUpdatableObject + @NSManaged public private(set) var isContentSensitiveToggled: Bool + // sourcery: autoUpdatableObject + @NSManaged public private(set) var isMediaSensitiveToggled: Bool + @NSManaged public private(set) var application: Application? // Informational @@ -86,9 +91,6 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var feeds: Set @NSManaged public private(set) var reblogFrom: Set -// @NSManaged public private(set) var mentions: Set? -// @NSManaged public private(set) var homeTimelineIndexes: Set? -// @NSManaged public private(set) var mediaAttachments: Set? @NSManaged public private(set) var replyFrom: Set @NSManaged public private(set) var notifications: Set @NSManaged public private(set) var searchHistories: Set @@ -590,6 +592,16 @@ extension Status: AutoUpdatableObject { self.spoilerText = spoilerText } } + public func update(isContentSensitiveToggled: Bool) { + if self.isContentSensitiveToggled != isContentSensitiveToggled { + self.isContentSensitiveToggled = isContentSensitiveToggled + } + } + public func update(isMediaSensitiveToggled: Bool) { + if self.isMediaSensitiveToggled != isMediaSensitiveToggled { + self.isMediaSensitiveToggled = isMediaSensitiveToggled + } + } public func update(reblogsCount: Int64) { if self.reblogsCount != reblogsCount { self.reblogsCount = reblogsCount diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json new file mode 100644 index 00000000..35dacadd --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "eye.circle.fill.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf new file mode 100644 index 00000000..bbc4a3ac --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf @@ -0,0 +1,125 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +16.989307 0.000000 m +19.322350 0.000000 21.512966 0.441750 23.561153 1.325245 c +25.609318 2.208746 27.416132 3.436619 28.981590 5.008865 c +30.547117 6.581110 31.774899 8.391962 32.664940 10.441422 c +33.554981 12.490898 34.000000 14.677086 34.000000 16.999983 c +34.000000 19.322903 33.554981 21.509090 32.664940 23.558544 c +31.774899 25.608021 30.544210 27.418886 28.972881 28.991137 c +27.401642 30.563387 25.591934 31.791258 23.543745 32.674755 c +21.495579 33.558250 19.304962 34.000000 16.971897 34.000000 c +14.653032 34.000000 12.468869 33.558250 10.419407 32.674755 c +8.369945 31.791258 6.566022 30.563387 5.007641 28.991137 c +3.449283 27.418886 2.225084 25.608021 1.335042 23.558544 c +0.445014 21.509090 0.000000 19.322903 0.000000 16.999983 c +0.000000 14.677086 0.445014 12.490898 1.335042 10.441422 c +2.225084 8.391962 3.452185 6.581110 5.016346 5.008865 c +6.580508 3.436619 8.387330 2.208746 10.436815 1.325245 c +12.486278 0.441750 14.670443 0.000000 16.989307 0.000000 c +h +16.993164 9.949825 m +15.357431 9.949825 13.855102 10.229713 12.486176 10.789488 c +11.117251 11.349285 9.930407 12.031605 8.925645 12.836449 c +7.920860 13.641315 7.142945 14.430695 6.591897 15.204592 c +6.040850 15.978466 5.765326 16.572420 5.765326 16.986456 c +5.765326 17.400492 6.038916 17.994457 6.586094 18.768354 c +7.133273 19.542252 7.906352 20.331621 8.905334 21.136465 c +9.904292 21.941330 11.091135 22.623650 12.465864 23.183426 c +13.840593 23.743221 15.349693 24.023121 16.993164 24.023121 c +18.640503 24.023121 20.147987 23.743221 21.515615 23.183426 c +22.883266 22.623650 24.067867 21.941330 25.069420 21.136465 c +26.070972 20.331621 26.843737 19.542252 27.387707 18.768354 c +27.931677 17.994457 28.203663 17.400492 28.203663 16.986456 c +28.203663 16.572420 27.931677 15.978466 27.387707 15.204592 c +26.843737 14.430695 26.071623 13.641315 25.071367 12.836449 c +24.071089 12.031605 22.887135 11.349285 21.519506 10.789488 c +20.151878 10.229713 18.643097 9.949825 16.993164 9.949825 c +h +16.993164 12.377918 m +17.840057 12.377918 18.611862 12.589771 19.308580 13.013485 c +20.005299 13.437176 20.562477 13.996964 20.980122 14.692844 c +21.397766 15.388702 21.606586 16.153238 21.606586 16.986456 c +21.606586 17.848070 21.397766 18.626804 20.980122 19.322662 c +20.562477 20.018520 20.005299 20.571213 19.308580 20.980740 c +18.611862 21.390266 17.840057 21.595030 16.993164 21.595030 c +16.134687 21.595030 15.357088 21.390266 14.660371 20.980740 c +13.963676 20.571213 13.407145 20.018520 12.990775 19.322662 c +12.574429 18.626804 12.366257 17.848070 12.366257 16.986456 c +12.368829 16.153238 12.578299 15.388702 12.994668 14.692844 c +13.411015 13.996964 13.966898 13.437176 14.662318 13.013485 c +15.357738 12.589771 16.134687 12.377918 16.993164 12.377918 c +h +17.014465 14.958965 m +16.460209 14.958965 15.980392 15.161781 15.575014 15.567413 c +15.169660 15.973045 14.966982 16.446060 14.966982 16.986456 c +14.966982 17.526875 15.169660 17.999889 15.575014 18.405499 c +15.980392 18.811131 16.460209 19.013947 17.014465 19.013947 c +17.554522 19.013947 18.024336 18.811131 18.423910 18.405499 c +18.823484 17.999889 19.023272 17.526875 19.023272 16.986456 c +19.023272 16.446060 18.823484 15.973045 18.423910 15.567413 c +18.024336 15.161781 17.554522 14.958965 17.014465 14.958965 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 3391 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 34.000000 34.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003481 00000 n +0000003504 00000 n +0000003677 00000 n +0000003751 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3810 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/Contents.json new file mode 100644 index 00000000..cc18167f --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "eye.slash.circle.fill.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/eye.slash.circle.fill.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/eye.slash.circle.fill.pdf new file mode 100644 index 00000000..3912ea6d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.slash.circle.fill.imageset/eye.slash.circle.fill.pdf @@ -0,0 +1,139 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +16.989307 0.000000 m +19.322350 0.000000 21.512964 0.441746 23.561153 1.325245 c +25.609318 2.208746 27.416132 3.436619 28.981590 5.008865 c +30.547117 6.581110 31.774899 8.391962 32.664940 10.441422 c +33.554981 12.490898 34.000000 14.677086 34.000000 16.999983 c +34.000000 19.322903 33.554981 21.509090 32.664940 23.558544 c +31.774899 25.608021 30.544210 27.418886 28.972881 28.991137 c +27.401642 30.563387 25.591932 31.791258 23.543745 32.674755 c +21.495579 33.558250 19.304962 34.000000 16.971897 34.000000 c +14.653033 34.000000 12.468869 33.558250 10.419407 32.674755 c +8.369944 31.791258 6.566022 30.563387 5.007641 28.991137 c +3.449283 27.418886 2.225084 25.608021 1.335042 23.558544 c +0.445014 21.509090 0.000000 19.322903 0.000000 16.999983 c +0.000000 14.677086 0.445014 12.490898 1.335042 10.441422 c +2.225084 8.391962 3.452185 6.581110 5.016346 5.008865 c +6.580508 3.436619 8.387330 2.208746 10.436815 1.325245 c +12.486278 0.441746 14.670442 0.000000 16.989307 0.000000 c +h +20.951313 10.079727 m +20.344168 9.895254 19.708973 9.742058 19.045732 9.620138 c +18.382490 9.498241 17.697016 9.437292 16.989307 9.437292 c +15.235007 9.437292 13.624114 9.738777 12.156626 10.341749 c +10.689138 10.944744 9.420459 11.680578 8.350589 12.549252 c +7.280741 13.417925 6.449994 14.262413 5.858347 15.082718 c +5.266723 15.903046 4.970911 16.537626 4.970911 16.986456 c +4.970911 17.488209 5.335063 18.222446 6.063368 19.189175 c +6.791673 20.155926 7.797766 21.079124 9.081647 21.958775 c +12.354888 18.669945 l +12.148547 18.150091 12.045376 17.588926 12.045376 16.986456 c +12.047971 16.095217 12.269036 15.274912 12.708573 14.525541 c +13.148132 13.776169 13.745263 13.175121 14.499967 12.722397 c +15.254647 12.269672 16.084427 12.043310 16.989307 12.043310 c +17.572033 12.043310 18.128338 12.153637 18.658220 12.374296 c +20.951313 10.079727 l +h +16.610533 14.419378 m +15.995673 14.406466 15.470092 14.628626 15.033787 15.085859 c +14.597482 15.543070 14.387078 16.056072 14.402575 16.624865 c +16.610533 14.419378 l +h +21.635332 15.318474 m +21.833935 15.861555 21.933235 16.417551 21.933235 16.986456 c +21.933235 17.903496 21.712809 18.733810 21.271954 19.477398 c +20.831120 20.220963 20.236252 20.815556 19.487350 21.261173 c +18.738451 21.706795 17.905769 21.929604 16.989307 21.929604 c +16.416889 21.929604 15.868335 21.831537 15.343640 21.635405 c +13.067924 23.908693 l +13.675068 24.090595 14.308327 24.240559 14.967699 24.358583 c +15.627072 24.476631 16.300941 24.535656 16.989307 24.535656 c +18.766796 24.535656 20.389282 24.234158 21.856770 23.631165 c +23.324280 23.028191 24.593609 22.292368 25.664755 21.423695 c +26.735899 20.555000 27.561493 19.710501 28.141533 18.890194 c +28.721573 18.069889 29.011593 17.435308 29.011593 16.986456 c +29.011593 16.487301 28.650341 15.753389 27.927839 14.784727 c +27.205315 13.816063 26.204697 12.890612 24.925982 12.008366 c +21.635332 15.318474 l +h +17.387436 19.586462 m +17.990688 19.576170 18.504028 19.351105 18.927452 18.911270 c +19.350876 18.471457 19.560640 17.965868 19.556749 17.394503 c +17.387436 19.586462 l +h +25.073380 7.792742 m +7.762697 25.118214 l +7.622216 25.261356 7.551338 25.442539 7.550064 25.661762 c +7.548766 25.881008 7.619644 26.069296 7.762697 26.226625 c +7.917356 26.381382 8.105525 26.458761 8.327205 26.458761 c +8.548884 26.458761 8.737055 26.381382 8.891714 26.226625 c +26.181129 8.901154 l +26.335789 8.748993 26.413122 8.565552 26.413122 8.350830 c +26.413122 8.136106 26.335789 7.950079 26.181129 7.792742 c +26.040648 7.647011 25.859568 7.574789 25.637888 7.576080 c +25.416208 7.577374 25.228039 7.649595 25.073380 7.792742 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 3709 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 34.000000 34.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003799 00000 n +0000003822 00000 n +0000003995 00000 n +0000004069 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4128 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 9524153a..f7b676d0 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -86,6 +86,8 @@ public enum Asset { public static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split") } public enum Human { + public static let eyeCircleFill = ImageAsset(name: "Human/eye.circle.fill") + public static let eyeSlashCircleFill = ImageAsset(name: "Human/eye.slash.circle.fill") public static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive") } public enum Scene { diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift new file mode 100644 index 00000000..600fcb5b --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift @@ -0,0 +1,4 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +// No fonts found diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift b/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift index dbee8e9b..ee9bc73e 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift @@ -15,6 +15,7 @@ extension MetaLabel { case statusHeader case statusName case statusUsername + case statusSpoiler case notificationTitle case profileFieldName case profileFieldValue @@ -56,6 +57,12 @@ extension MetaLabel { font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) textColor = Asset.Colors.Label.secondary.color + case .statusSpoiler: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) + textColor = Asset.Colors.Label.secondary.color + textAlignment = .center + paragraphStyle.alignment = .center + case .notificationTitle: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .regular)) textColor = Asset.Colors.Label.secondary.color diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index a3f367e4..74fcb943 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -380,7 +380,9 @@ extension NotificationView: StatusViewDelegate { assertionFailure() } - + public func statusView(_ statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) { + assertionFailure() + } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index b711b3ae..2b1098d1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -14,6 +14,7 @@ import MastodonSDK import MastodonAsset import MastodonLocalization import MastodonExtension +import CoreDataStack extension StatusView { public final class ViewModel: ObservableObject { @@ -41,6 +42,9 @@ extension StatusView { @Published public var timestamp: Date? public var timestampFormatter: ((_ date: Date) -> String)? + // Spoiler + @Published public var spoilerContent: MetaContent? + // Status @Published public var content: MetaContent? @@ -57,6 +61,19 @@ extension StatusView { @Published public var expireAt: Date? @Published public var expired: Bool = false + // Visibility + @Published public var visibility: MastodonVisibility = .public + + // Sensitive + @Published public var isContentSensitive: Bool = false + @Published public var isContentSensitiveToggled: Bool = false + @Published public var isMediaSensitive: Bool = false + @Published public var isMediaSensitiveToggled: Bool = false + + @Published public var isSensitive: Bool = false // isContentSensitive || isMediaSensitive + @Published public var isContentReveal: Bool = true + @Published public var isMediaReveal: Bool = true + // Toolbar @Published public var isReblog: Bool = false @Published public var isReblogEnabled: Bool = true @@ -93,6 +110,47 @@ extension StatusView { } } } + + public func prepareForReuse() { + authorAvatarImageURL = nil + + isContentSensitive = false + isContentSensitiveToggled = false + isMediaSensitive = false + isMediaSensitiveToggled = false + + isSensitive = false + isContentReveal = false + isMediaReveal = false + } + + init() { + // isContentSensitive + $spoilerContent + .map { $0 != nil } + .assign(to: &$isContentSensitive) + // isSensitive + Publishers.CombineLatest( + $isContentSensitive, + $isMediaSensitive + ) + .map { $0 || $1 } + .assign(to: &$isSensitive) + // $isContentReveal + Publishers.CombineLatest( + $isContentSensitive, + $isContentSensitiveToggled + ) + .map { $1 ? $0 : !$0 } + .assign(to: &$isContentReveal) + // $isMediaReveal + Publishers.CombineLatest( + $isMediaSensitive, + $isMediaSensitiveToggled + ) + .map { $1 ? !$0 : $0} + .assign(to: &$isMediaReveal) + } } } @@ -163,52 +221,98 @@ extension StatusView.ViewModel { statusView.authorUsernameLabel.configure(content: metaContent) } .store(in: &disposeBag) -// // visibility -// $visibility -// .sink { visibility in -// guard let visibility = visibility, -// let image = visibility.inlineImage -// else { return } -// -// statusView.visibilityImageView.image = image -// statusView.setVisibilityDisplay() -// } -// .store(in: &disposeBag) - // timestamp Publishers.CombineLatest( $timestamp, timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() ) - .sink { [weak self] timestamp, _ in - guard let self = self else { return } + .compactMap { [weak self] timestamp, _ -> String? in + guard let self = self else { return nil } guard let timestamp = timestamp, - let text = self.timestampFormatter?(timestamp) else { - statusView.dateLabel.configure(content: PlaintextMetaContent(string: "")) - return - } - + let text = self.timestampFormatter?(timestamp) + else { return "" } + return text + } + .removeDuplicates() + .sink { [weak self] text in + guard let _ = self else { return } statusView.dateLabel.configure(content: PlaintextMetaContent(string: text)) } .store(in: &disposeBag) + $isSensitive + .sink { isSensitive in + if !isSensitive { + statusView.setMenuButtonDisplay() + } + } + .store(in: &disposeBag) } private func bindContent(statusView: StatusView) { - $content - .sink { content in - guard let content = content else { - statusView.contentMetaText.reset() - statusView.contentMetaText.textView.accessibilityLabel = "" - return - } - - statusView.contentMetaText.configure(content: content) + Publishers.CombineLatest3( + $spoilerContent, + $content, + $isContentReveal.removeDuplicates() + ) + .sink { spoilerContent, content, isContentReveal in + if let spoilerContent = spoilerContent { + statusView.spoilerOverlayView.spoilerMetaLabel.configure(content: spoilerContent) + } else { + statusView.spoilerOverlayView.spoilerMetaLabel.reset() + } + + if let content = content { + statusView.contentMetaText.configure( + content: content, + isRedactedModeEnabled: !isContentReveal + ) statusView.contentMetaText.textView.accessibilityLabel = content.string statusView.contentMetaText.textView.accessibilityTraits = [.staticText] statusView.contentMetaText.textView.accessibilityElementsHidden = false - + } else { + statusView.contentMetaText.reset() + statusView.contentMetaText.textView.accessibilityLabel = "" } - .store(in: &disposeBag) + + statusView.setSpoilerOverlayViewHidden(isContentReveal) + } + .store(in: &disposeBag) + // visibility + Publishers.CombineLatest( + $visibility, + $isMyself + ) + .sink { visibility, isMyself in + switch visibility { + case .public: + break + case .unlisted: + statusView.statusVisibilityView.label.text = "Everyone can see this post but not display in the public timeline." + statusView.setVisibilityDisplay() + case .private: + statusView.statusVisibilityView.label.text = isMyself ? "Only my followers can see this post." : "Only their followers can see this post." + statusView.setVisibilityDisplay() + case .direct: + statusView.statusVisibilityView.label.text = "Only mentioned user can see this post." + statusView.setVisibilityDisplay() + case ._other: + break + } + } + .store(in: &disposeBag) + Publishers.CombineLatest( + $isContentSensitive, + $isMediaSensitive + ) + .sink { isContentSensitive, isMediaSensitive in + if isContentSensitive || isMediaSensitive { + let image = Asset.Human.eyeCircleFill.image + statusView.contentWarningToggleButton.setImage(image, for: .normal) + statusView.contentWarningToggleButton.tintColor = .systemGray + statusView.setContentWarningToggleButtonDisplay() + } + } + .store(in: &disposeBag) // $spoilerContent // .sink { metaContent in // guard let metaContent = metaContent else { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index d6f9106b..0b8cae96 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -22,7 +22,7 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) func statusView(_ statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) func statusView(_ statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) -// func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) + func statusView(_ statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) // func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) // func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) } @@ -100,6 +100,9 @@ public final class StatusView: UIView { return button }() + // contentWarningToggleButton + public let contentWarningToggleButton = UIButton(type: .system) + // content let contentContainer = UIStackView() public let contentMetaText: MetaText = { @@ -130,6 +133,8 @@ public final class StatusView: UIView { ] return metaText }() + + let spoilerOverlayView = SpoilerOverlayView() // media public let mediaContainerView = UIView() @@ -189,6 +194,9 @@ public final class StatusView: UIView { return indicatorView }() + // visibility + public let statusVisibilityView = StatusVisibilityView() + // toolbar public let actionToolbarContainer = ActionToolbarContainer() @@ -199,7 +207,7 @@ public final class StatusView: UIView { disposeBag.removeAll() viewModel.objects.removeAll() - viewModel.authorAvatarImageURL = nil + viewModel.prepareForReuse() avatarButton.avatarImageView.cancelTask() mediaGridContainerView.prepareForReuse() @@ -214,8 +222,12 @@ public final class StatusView: UIView { } headerContainerView.isHidden = true + menuButton.isHidden = true + contentWarningToggleButton.isHidden = true + setSpoilerOverlayViewHidden(true) mediaContainerView.isHidden = true pollContainerView.isHidden = true + statusVisibilityView.isHidden = true } public override init(frame: CGRect) { @@ -254,6 +266,9 @@ extension StatusView { authorNameLabel.isUserInteractionEnabled = false authorUsernameLabel.isUserInteractionEnabled = false + // contentWarningToggleButton + contentWarningToggleButton.addTarget(self, action: #selector(StatusView.contentWarningToggleButtonDidPressed(_:)), for: .touchUpInside) + // dateLabel dateLabel.isUserInteractionEnabled = false @@ -291,6 +306,11 @@ extension StatusView { delegate?.statusView(self, authorAvatarButtonDidPressed: avatarButton) } + @objc private func contentWarningToggleButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.statusView(self, contentWarningToggleButtonDidPressed: contentWarningToggleButton) + } + @objc private func pollVoteButtonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") delegate?.statusView(self, pollVoteButtonPressed: pollVoteButton) @@ -360,7 +380,7 @@ extension StatusView.Style { statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - // author container: H - [ avatarButton | author meta container ] + // author container: H - [ avatarButton | author meta container | contentWarningToggleButton ] statusView.authorContainerView.preservesSuperviewLayoutMargins = true statusView.authorContainerView.isLayoutMarginsRelativeArrangement = true statusView.containerStackView.addArrangedSubview(statusView.authorContainerView) @@ -418,7 +438,12 @@ extension StatusView.Style { statusView.dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) authorSecondaryMetaContainer.addArrangedSubview(UIView()) - // content container: V - [ contentMetaText | ] + // contentWarningToggleButton + statusView.authorContainerView.addArrangedSubview(statusView.contentWarningToggleButton) + statusView.contentWarningToggleButton.setContentHuggingPriority(.required - 2, for: .horizontal) + statusView.contentWarningToggleButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal) + + // content container: V - [ contentMetaText ] statusView.contentContainer.axis = .vertical statusView.contentContainer.spacing = 12 statusView.contentContainer.distribution = .fill @@ -430,10 +455,17 @@ extension StatusView.Style { statusView.contentContainer.setContentHuggingPriority(.required - 1, for: .vertical) statusView.contentContainer.setContentCompressionResistancePriority(.required - 1, for: .vertical) - // status + // status content statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView) - statusView.contentMetaText.textView.setContentHuggingPriority(.required - 1, for: .vertical) - statusView.contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical) + + statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false + statusView.containerStackView.addSubview(statusView.spoilerOverlayView) + NSLayoutConstraint.activate([ + statusView.contentContainer.topAnchor.constraint(equalTo: statusView.spoilerOverlayView.topAnchor), + statusView.contentContainer.leadingAnchor.constraint(equalTo: statusView.spoilerOverlayView.leadingAnchor), + statusView.contentContainer.trailingAnchor.constraint(equalTo: statusView.spoilerOverlayView.trailingAnchor), + statusView.contentContainer.bottomAnchor.constraint(equalTo: statusView.spoilerOverlayView.bottomAnchor), + ]) // media container: V - [ mediaGridContainerView ] statusView.containerStackView.addArrangedSubview(statusView.mediaContainerView) @@ -470,6 +502,10 @@ extension StatusView.Style { statusView.pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) statusView.pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal) + // statusVisibilityView + statusView.statusVisibilityView.preservesSuperviewLayoutMargins = true + statusView.containerStackView.addArrangedSubview(statusView.statusVisibilityView) + // action toolbar statusView.actionToolbarContainer.configure(for: .inline) statusView.actionToolbarContainer.preservesSuperviewLayoutMargins = true @@ -503,6 +539,7 @@ extension StatusView.Style { statusView.contentContainer.layoutMargins.bottom = 16 // fix contentText align to edge issue statusView.menuButton.removeFromSuperview() + statusView.statusVisibilityView.removeFromSuperview() statusView.actionToolbarContainer.removeFromSuperview() } @@ -524,6 +561,7 @@ extension StatusView.Style { statusView.contentContainer.removeFromSuperview() statusView.mediaContainerView.removeFromSuperview() statusView.pollContainerView.removeFromSuperview() + statusView.statusVisibilityView.removeFromSuperview() statusView.actionToolbarContainer.removeFromSuperview() } @@ -534,6 +572,19 @@ extension StatusView { headerContainerView.isHidden = false } + func setMenuButtonDisplay() { + menuButton.isHidden = false + } + + func setContentWarningToggleButtonDisplay() { + contentWarningToggleButton.isHidden = false + } + + func setSpoilerOverlayViewHidden(_ isHidden: Bool) { + spoilerOverlayView.isHidden = isHidden + spoilerOverlayView.setComponentHidden(isHidden) + } + func setMediaDisplay() { mediaContainerView.isHidden = false } @@ -542,6 +593,10 @@ extension StatusView { pollContainerView.isHidden = false } + func setVisibilityDisplay() { + statusVisibilityView.isHidden = false + } + // content text Width public var contentMaxLayoutWidth: CGFloat { let inset = contentLayoutInset diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift new file mode 100644 index 00000000..d6f3900a --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift @@ -0,0 +1,90 @@ +// +// SpoilerOverlayView.swift +// +// +// Created by MainasuK on 2022-1-29. +// + +import UIKit +import MastodonLocalization +import MastodonAsset +import MetaTextKit + +final class SpoilerOverlayView: UIView { + + let containerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + // stackView.spacing = 8 + stackView.alignment = .center + return stackView + }() + + let iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "eye", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 34, weight: .light))) + imageView.tintColor = Asset.Colors.Label.secondary.color + return imageView + }() + + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + label.textAlignment = .center + label.textColor = Asset.Colors.Label.primary.color + label.text = L10n.Common.Controls.Status.contentWarning + return label + }() + + let spoilerMetaLabel = MetaLabel(style: .statusSpoiler) + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension SpoilerOverlayView { + private func _init() { + containerStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + + let topPaddingView = UIView() + topPaddingView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(topPaddingView) + iconImageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(iconImageView) + NSLayoutConstraint.activate([ + iconImageView.widthAnchor.constraint(equalToConstant: 52.0).priority(.required - 1), + iconImageView.heightAnchor.constraint(equalToConstant: 32.0).priority(.required - 1), + ]) + iconImageView.setContentCompressionResistancePriority(.required, for: .vertical) + containerStackView.addArrangedSubview(titleLabel) + containerStackView.addArrangedSubview(spoilerMetaLabel) + let bottomPaddingView = UIView() + bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(bottomPaddingView) + NSLayoutConstraint.activate([ + topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor).priority(.required - 1), + ]) + topPaddingView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical) + bottomPaddingView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical) + } + + public func setComponentHidden(_ isHidden: Bool) { + containerStackView.arrangedSubviews.forEach { $0.isHidden = isHidden } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/StatusVisibilityView.swift b/MastodonSDK/Sources/MastodonUI/View/Control/StatusVisibilityView.swift new file mode 100644 index 00000000..1866c7e1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Control/StatusVisibilityView.swift @@ -0,0 +1,74 @@ +// +// StatusVisibilityView.swift +// +// +// Created by MainasuK on 2022-1-28. +// + +import UIKit + +public final class StatusVisibilityView: UIView { + + static let cornerRadius: CGFloat = 8 + static let containerMargin: CGFloat = 14 + + public let containerView = UIView() + + public let label: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + label.numberOfLines = 0 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension StatusVisibilityView { + + private func _init() { + containerView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerView) + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: topAnchor), + containerView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + containerView.backgroundColor = .secondarySystemBackground + + containerView.layoutMargins = UIEdgeInsets( + top: StatusVisibilityView.containerMargin, + left: StatusVisibilityView.containerMargin, + bottom: StatusVisibilityView.containerMargin, + right: StatusVisibilityView.containerMargin + ) + label.translatesAutoresizingMaskIntoConstraints = false + addSubview(label) + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor), + label.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), + label.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor), + label.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor), + ]) + } + + public override func layoutSubviews() { + super.layoutSubviews() + + containerView.layer.masksToBounds = false + containerView.layer.cornerCurve = .continuous + containerView.layer.cornerRadius = StatusVisibilityView.cornerRadius + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/View/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/View/MastodonMenu.swift rename to MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift diff --git a/swiftgen.yml b/swiftgen.yml index fa8189cf..e9c21260 100644 --- a/swiftgen.yml +++ b/swiftgen.yml @@ -16,3 +16,11 @@ xcassets: params: bundle: Bundle.module publicAccess: true +fonts: + inputs: MastodonSDK/Sources/MastodonAsset/Font + outputs: + templateName: swift5 + output: MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift + params: + bundle: Bundle.module + publicAccess: true \ No newline at end of file