feat: add content warning (CW) for status text.
This commit is contained in:
parent
98ebddc438
commit
f455faa273
|
@ -20,7 +20,9 @@
|
|||
"open_in_safari": "Open in Safari"
|
||||
},
|
||||
"status": {
|
||||
"userBoosted": "%s boosted"
|
||||
"user_boosted": "%s boosted",
|
||||
"content_warning": "content warning",
|
||||
"show_post": "Show Post"
|
||||
},
|
||||
"timeline": {
|
||||
"load_more": "Load More"
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
"Common.Controls.Actions.SignIn" = "Sign in";
|
||||
"Common.Controls.Actions.SignUp" = "Sign up";
|
||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"Common.Controls.Status.Userboosted" = "%@ boosted";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
"open_in_safari": "Open in Safari"
|
||||
},
|
||||
"status": {
|
||||
"userBoosted": "%s boosted"
|
||||
"user_boosted": "%s boosted",
|
||||
"content_warning": "content warning",
|
||||
"show_post": "Show Post"
|
||||
},
|
||||
"timeline": {
|
||||
"load_more": "Load More"
|
||||
|
|
|
@ -103,6 +103,7 @@
|
|||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||
|
@ -302,6 +303,7 @@
|
|||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -895,6 +897,7 @@
|
|||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
|
||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
|
||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */,
|
||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||
|
@ -1338,6 +1341,7 @@
|
|||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
0FAA102725E1126A0017CCDE /* PickServerViewController.swift in Sources */,
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
|
|
|
@ -79,7 +79,7 @@ extension TimelineSection {
|
|||
cell.statusView.headerInfoLabel.text = {
|
||||
let author = toot.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userboosted(name)
|
||||
return L10n.Common.Controls.Status.userBoosted(name)
|
||||
}()
|
||||
|
||||
// set name username avatar
|
||||
|
@ -93,6 +93,12 @@ extension TimelineSection {
|
|||
// set text
|
||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
||||
|
||||
// set content warning
|
||||
cell.statusView.updateContentWarningDisplay(isHidden: !(toot.reblog ?? toot).sensitive)
|
||||
cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in
|
||||
return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)"
|
||||
} ?? L10n.Common.Controls.Status.contentWarning
|
||||
|
||||
// prepare media attachments
|
||||
let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// NSKeyValueObservation.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-2-24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension NSKeyValueObservation {
|
||||
func store(in set: inout Set<NSKeyValueObservation>) {
|
||||
set.insert(self)
|
||||
}
|
||||
}
|
|
@ -40,3 +40,16 @@ extension UIImage {
|
|||
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
func blur(radius: CGFloat) -> UIImage? {
|
||||
guard let inputImage = CIImage(image: self) else { return nil }
|
||||
let blurFilter = CIFilter.gaussianBlur()
|
||||
blurFilter.inputImage = inputImage
|
||||
blurFilter.radius = Float(radius)
|
||||
guard let outputImage = blurFilter.outputImage else { return nil }
|
||||
guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { return nil }
|
||||
let image = UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation)
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,6 @@ import Foundation
|
|||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
internal enum L10n {
|
||||
|
||||
internal enum Button {
|
||||
/// Sign In
|
||||
internal static let signIn = L10n.tr("Localizable", "Button.SignIn")
|
||||
/// Sign Up
|
||||
internal static let signUp = L10n.tr("Localizable", "Button.SignUp")
|
||||
}
|
||||
|
||||
internal enum Common {
|
||||
internal enum Controls {
|
||||
internal enum Actions {
|
||||
|
@ -53,9 +46,13 @@ internal enum L10n {
|
|||
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
||||
}
|
||||
internal enum Status {
|
||||
/// content warning
|
||||
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
|
||||
/// Show Post
|
||||
internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost")
|
||||
/// %@ boosted
|
||||
internal static func userboosted(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Userboosted", String(describing: p1))
|
||||
internal static func userBoosted(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Timeline {
|
||||
|
|
|
@ -13,11 +13,10 @@
|
|||
"Common.Controls.Actions.SignIn" = "Sign in";
|
||||
"Common.Controls.Actions.SignUp" = "Sign up";
|
||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"Common.Controls.Status.Userboosted" = "%@ boosted";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||
|
||||
"Button.SignUp" = "Sign Up";
|
||||
"Button.SignIn" = "Sign In";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.HomeTimeline.Title" = "Home";
|
||||
|
@ -38,4 +37,4 @@ any server.";
|
|||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Welcome.Slogan" = "Social networking
|
||||
back in your hands.";
|
||||
back in your hands.";
|
|
@ -206,6 +206,14 @@ extension HomeTimelineViewController: UITableViewDelegate {
|
|||
|
||||
return ceil(frame.height)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
if let cell = cell as? StatusTableViewCell {
|
||||
DispatchQueue.main.async {
|
||||
cell.statusView.drawContentWarningImageView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
|
|
|
@ -14,6 +14,7 @@ final class StatusView: UIView {
|
|||
|
||||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
static let contentWarningBlurRadius: CGFloat = 12
|
||||
|
||||
let headerContainerStackView = UIStackView()
|
||||
|
||||
|
@ -72,8 +73,33 @@ final class StatusView: UIView {
|
|||
}()
|
||||
|
||||
let statusContainerStackView = UIStackView()
|
||||
let statusTextContainerView = UIView()
|
||||
let statusContentWarningContainerStackView = UIStackView()
|
||||
var statusContentWarningContainerStackViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let contentWarningTitle: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Common.Controls.Status.contentWarning
|
||||
return label
|
||||
}()
|
||||
let contentWarningActionButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .medium))
|
||||
button.setTitleColor(Asset.Colors.Label.highlight.color, for: .normal)
|
||||
button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal)
|
||||
return button
|
||||
}()
|
||||
let mosaicImageView = MosaicImageView()
|
||||
|
||||
// do not use visual effect view due to we blur text only without background
|
||||
let contentWarningBlurContentImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.backgroundColor = .secondarySystemGroupedBackground
|
||||
imageView.layer.masksToBounds = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let actionToolbarContainer: ActionToolbarContainer = {
|
||||
let actionToolbarContainer = ActionToolbarContainer()
|
||||
|
@ -183,16 +209,77 @@ extension StatusView {
|
|||
containerStackView.addArrangedSubview(statusContainerStackView)
|
||||
statusContainerStackView.axis = .vertical
|
||||
statusContainerStackView.spacing = 10
|
||||
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
||||
activeTextLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
statusContainerStackView.addArrangedSubview(statusTextContainerView)
|
||||
statusTextContainerView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
activeTextLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusTextContainerView.addSubview(activeTextLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
activeTextLabel.topAnchor.constraint(equalTo: statusTextContainerView.topAnchor),
|
||||
activeTextLabel.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
|
||||
activeTextLabel.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
||||
statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: activeTextLabel.bottomAnchor),
|
||||
])
|
||||
contentWarningBlurContentImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusTextContainerView.addSubview(contentWarningBlurContentImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
activeTextLabel.topAnchor.constraint(equalTo: contentWarningBlurContentImageView.topAnchor, constant: StatusView.contentWarningBlurRadius),
|
||||
activeTextLabel.leadingAnchor.constraint(equalTo: contentWarningBlurContentImageView.leadingAnchor, constant: StatusView.contentWarningBlurRadius),
|
||||
|
||||
])
|
||||
statusContentWarningContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusContentWarningContainerStackView.axis = .vertical
|
||||
statusContentWarningContainerStackView.distribution = .fill
|
||||
statusContentWarningContainerStackView.alignment = .center
|
||||
statusTextContainerView.addSubview(statusContentWarningContainerStackView)
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint = statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: statusContentWarningContainerStackView.bottomAnchor, constant: 8)
|
||||
NSLayoutConstraint.activate([
|
||||
statusContentWarningContainerStackView.topAnchor.constraint(equalTo: statusTextContainerView.topAnchor),
|
||||
statusContentWarningContainerStackView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
|
||||
statusContentWarningContainerStackView.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint,
|
||||
])
|
||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
|
||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
|
||||
statusContainerStackView.addArrangedSubview(mosaicImageView)
|
||||
|
||||
|
||||
// action toolbar container
|
||||
containerStackView.addArrangedSubview(actionToolbarContainer)
|
||||
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
|
||||
headerContainerStackView.isHidden = true
|
||||
mosaicImageView.isHidden = true
|
||||
contentWarningBlurContentImageView.isHidden = true
|
||||
statusContentWarningContainerStackView.isHidden = true
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusView {
|
||||
|
||||
func cleanUpContentWarning() {
|
||||
contentWarningBlurContentImageView.image = nil
|
||||
}
|
||||
|
||||
func drawContentWarningImageView() {
|
||||
guard activeTextLabel.frame != .zero, let text = activeTextLabel.text, !text.isEmpty else {
|
||||
cleanUpContentWarning()
|
||||
return
|
||||
}
|
||||
|
||||
let image = UIGraphicsImageRenderer(size: activeTextLabel.frame.size).image { context in
|
||||
activeTextLabel.draw(activeTextLabel.bounds)
|
||||
}
|
||||
.blur(radius: StatusView.contentWarningBlurRadius)
|
||||
contentWarningBlurContentImageView.contentScaleFactor = traitCollection.displayScale
|
||||
contentWarningBlurContentImageView.image = image
|
||||
}
|
||||
|
||||
func updateContentWarningDisplay(isHidden: Bool) {
|
||||
contentWarningBlurContentImageView.isHidden = isHidden
|
||||
statusContentWarningContainerStackView.isHidden = isHidden
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = !isHidden
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ final class StatusTableViewCell: UITableViewCell {
|
|||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
statusView.cleanUpContentWarning()
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
let signUpButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton(type: .system)
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||
button.setTitle(L10n.Button.signUp, for: .normal)
|
||||
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
@ -45,7 +45,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
let signInButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||
button.setTitle(L10n.Button.signIn, for: .normal)
|
||||
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
||||
button.setInsets(forContentPadding: UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0), imageTitlePadding: 0)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
Loading…
Reference in New Issue