feat: add content warning for post spoiler

This commit is contained in:
CMK 2022-01-29 17:02:30 +08:00
parent 12b73f5a10
commit caaf66286f
29 changed files with 842 additions and 132 deletions

View File

@ -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": {

View File

@ -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 */,

View File

@ -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>

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}()

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -42,7 +42,7 @@ extension ThreadViewModel {
} else {
}
diffableDataSource?.apply(snapshot)
diffableDataSource?.apply(snapshot, animatingDifferences: false)
$threadContext
.receive(on: DispatchQueue.main)

View File

@ -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",

View File

@ -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"/>

View File

@ -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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "eye.circle.fill.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View 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

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "eye.slash.circle.fill.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1,4 @@
// swiftlint:disable all
// Generated using SwiftGen https://github.com/SwiftGen/SwiftGen
// No fonts found

View File

@ -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

View File

@ -380,7 +380,9 @@ extension NotificationView: StatusViewDelegate {
assertionFailure()
}
public func statusView(_ statusView: StatusView, contentWarningToggleButtonDidPressed button: UIButton) {
assertionFailure()
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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 }
}
}

View File

@ -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
}
}

View File

@ -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