mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-01 09:57:21 +01:00
feat: add content warning for post spoiler
This commit is contained in:
parent
12b73f5a10
commit
caaf66286f
@ -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": {
|
||||
|
@ -1315,6 +1315,7 @@
|
||||
DBDC1CFD272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ku-TR"; path = "ku-TR.lproj/Intents.stringsdict"; sourceTree = "<group>"; };
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||
DBE3CA7127A3F23D00AFE27B /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = "<group>"; };
|
||||
DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||
DBE3CDEB261C6B2900430CC6 /* FavoriteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewController.swift; sourceTree = "<group>"; };
|
||||
@ -2111,6 +2112,7 @@
|
||||
children = (
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
|
||||
DBE3CA7127A3F23D00AFE27B /* MetaTextKit */,
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */,
|
||||
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
||||
|
@ -7,7 +7,7 @@
|
||||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>19</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -97,7 +97,7 @@
|
||||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>18</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -257,3 +257,36 @@ extension DataSourceFacade {
|
||||
}
|
||||
} // end func
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToToggleSensitiveAction(
|
||||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) 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<Status>
|
||||
) 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,18 +19,6 @@ final class NotificationViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||
// let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.everyThing)
|
||||
// let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
// let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
|
||||
// let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
|
||||
// let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||
// let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
// var needsScrollToTopAfterDataSourceUpdate = false
|
||||
// let dataSourceDidUpdated = PassthroughSubject<Void, Never>()
|
||||
// let isFetchingLatestNotification = CurrentValueSubject<Bool, Never>(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ extension ThreadViewModel {
|
||||
} else {
|
||||
|
||||
}
|
||||
diffableDataSource?.apply(snapshot)
|
||||
diffableDataSource?.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
$threadContext
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -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",
|
||||
|
@ -199,6 +199,8 @@
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||
<attribute name="isContentSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isMediaSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="language" optional="YES" attributeType="String"/>
|
||||
<attribute name="mentions" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
@ -275,7 +277,7 @@
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="629"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="164"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="149"/>
|
||||
|
@ -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<Feed>
|
||||
|
||||
@NSManaged public private(set) var reblogFrom: Set<Status>
|
||||
// @NSManaged public private(set) var mentions: Set<Mention>?
|
||||
// @NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
||||
// @NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
||||
@NSManaged public private(set) var replyFrom: Set<Status>
|
||||
@NSManaged public private(set) var notifications: Set<Notification>
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
@ -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
|
||||
|
15
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json
vendored
Normal file
15
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "eye.circle.fill.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
125
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf
vendored
Normal file
125
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Human/eye.circle.fill.imageset/eye.circle.fill.pdf
vendored
Normal file
@ -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
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "eye.slash.circle.fill.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
@ -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
|
@ -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 {
|
||||
|
4
MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift
Normal file
4
MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift
Normal file
@ -0,0 +1,4 @@
|
||||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
// No fonts found
|
@ -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
|
||||
|
@ -380,7 +380,9 @@ extension NotificationView: StatusViewDelegate {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
|
||||
public func statusView(_ statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user