diff --git a/Modules/Package.swift b/Modules/Package.swift
index 1b42f405890f..faa4a994f838 100644
--- a/Modules/Package.swift
+++ b/Modules/Package.swift
@@ -14,6 +14,7 @@ let package = Package(
.library(name: "WordPressFlux", targets: ["WordPressFlux"]),
.library(name: "WordPressShared", targets: ["WordPressShared"]),
.library(name: "WordPressUI", targets: ["WordPressUI"]),
+ .library(name: "WordPressReader", targets: ["WordPressReader"]),
],
dependencies: [
.package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"),
@@ -69,13 +70,15 @@ let package = Package(
.target(name: "WordPressTesting", resources: [.process("Resources")]),
.target(
name: "WordPressUI",
- dependencies: [
- "AsyncImageKit",
- .target(name: "WordPressShared")
- ],
+ dependencies: ["AsyncImageKit", "WordPressShared"],
resources: [.process("Resources")],
swiftSettings: [.swiftLanguageMode(.v5)]
),
+ .target(
+ name: "WordPressReader",
+ dependencies: ["AsyncImageKit", "WordPressUI", "WordPressShared"],
+ resources: [.process("Resources")]
+ ),
.testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]),
@@ -156,6 +159,7 @@ enum XcodeSupport {
"JetpackStatsWidgetsCore",
"WordPressFlux",
"WordPressShared",
+ "WordPressReader",
"AsyncImageKit",
"WordPressUI",
"WordPressCore",
diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/CommentContentRenderer.swift b/Modules/Sources/WordPressReader/Comments/Views/CommentContentRenderer.swift
similarity index 66%
rename from WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/CommentContentRenderer.swift
rename to Modules/Sources/WordPressReader/Comments/Views/CommentContentRenderer.swift
index 6f4c138a2590..b198771587b5 100644
--- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/CommentContentRenderer.swift
+++ b/Modules/Sources/WordPressReader/Comments/Views/CommentContentRenderer.swift
@@ -1,22 +1,21 @@
+import UIKit
+
/// Defines methods related to Comment content rendering.
-///
-protocol CommentContentRenderer {
+@MainActor
+public protocol CommentContentRenderer: AnyObject {
var delegate: CommentContentRendererDelegate? { get set }
- init(comment: Comment)
+ init()
/// Returns a view component that's configured to display the formatted content of the comment.
///
/// Note that the renderer *might* return a view with the wrong sizing at first, but it should update its delegate with the correct height
/// through the `renderer(_:asyncRenderCompletedWithHeight:)` method.
- func render() -> UIView
-
- /// Checks if the provided comment contains the same content as the current one that's processed by the renderer.
- /// This is used as an optimization strategy by the caller to skip view creations if the rendered content matches the one provided in the parameter.
- func matchesContent(from comment: Comment) -> Bool
+ func render(comment: String) -> UIView
}
-protocol CommentContentRendererDelegate: AnyObject {
+@MainActor
+public protocol CommentContentRendererDelegate: AnyObject {
/// Called when the rendering process completes. Note that this method is only called when using complex rendering methods that involves
/// asynchronous operations, so the container can readjust its size at a later time.
func renderer(_ renderer: CommentContentRenderer, asyncRenderCompletedWithHeight height: CGFloat)
diff --git a/Modules/Sources/WordPressReader/Comments/Views/CommentWebView.swift b/Modules/Sources/WordPressReader/Comments/Views/CommentWebView.swift
new file mode 100644
index 000000000000..1ae5833650de
--- /dev/null
+++ b/Modules/Sources/WordPressReader/Comments/Views/CommentWebView.swift
@@ -0,0 +1,62 @@
+import UIKit
+
+/// -warning: It's not designed to be used publically yet.
+@MainActor
+final class CommentWebView: UIView, CommentContentRendererDelegate {
+ let renderer = WebCommentContentRenderer()
+ let webView: UIView
+ lazy var heightConstraint = webView.heightAnchor.constraint(equalToConstant: 20)
+
+ init(comment: String) {
+ let webView = renderer.render(comment: comment)
+ self.webView = webView
+
+ super.init(frame: .zero)
+
+ renderer.delegate = self
+
+ addSubview(webView)
+ webView.pinEdges()
+
+ heightConstraint.isActive = true
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: CommentContentRendererDelegate
+
+ func renderer(_ renderer: any CommentContentRenderer, interactedWithURL url: URL) {
+ print("interact:", url)
+ }
+
+ func renderer(_ renderer: any CommentContentRenderer, asyncRenderCompletedWithHeight height: CGFloat) {
+ heightConstraint.constant = height
+ }
+}
+
+@available(iOS 17, *)
+#Preview("Plain Text") {
+ makeView(comment: "
Thank you so much! You should see it now – people are losing their minds!
\n")
+}
+
+@available(iOS 17, *)
+#Preview("Gutenberg") {
+ makeView(comment: """
+ Thank you for putting this together, I’m in support of all proposed improvements, we know that the current experience is less-than-ideal.
Get rid of This. We’re moving everything to That anyway, and this is our last remaining This instance in Jetpack. It’s not performing great, so let’s remove it.
@tester‘s most recent review found it failed to provide a valid response in more than half of interactions.
+ """)
+}
+
+@MainActor
+private func makeView(comment: String) -> UIView {
+ let webView = CommentWebView(comment: comment)
+ webView.layer.borderColor = UIColor.opaqueSeparator.withAlphaComponent(0.66).cgColor
+ webView.layer.borderWidth = 0.5
+
+ let container = UIView()
+ container.addSubview(webView)
+ webView.pinEdges(insets: UIEdgeInsets(.all, 16))
+
+ return container
+}
diff --git a/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift b/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift
new file mode 100644
index 000000000000..d0820ec6f876
--- /dev/null
+++ b/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift
@@ -0,0 +1,158 @@
+import WebKit
+import WordPressShared
+import WordPressUI
+
+/// Renders the comment body with a web view. Provides the best visual experience but has the highest performance cost.
+@MainActor
+public final class WebCommentContentRenderer: NSObject, CommentContentRenderer {
+ // MARK: Properties
+
+ public weak var delegate: CommentContentRendererDelegate?
+
+ private let webView = WKWebView(frame: .zero, configuration: {
+ let configuration = WKWebViewConfiguration()
+ configuration.allowsInlineMediaPlayback = true
+ configuration.defaultWebpagePreferences.allowsContentJavaScript = false
+ return configuration
+ }())
+
+ private var comment: String?
+
+ /// It can't be changed at the moment, but this capability was included from the
+ /// start, and this implementation continues supporting it.
+ private var displaySetting = ReaderDisplaySettings.standard
+
+ /// - warning: This has to be configured _before_ you render.
+ public var tintColor: UIColor {
+ get { webView.tintColor }
+ set {
+ webView.tintColor = newValue
+ cachedHead = nil
+ }
+ }
+
+ private var cachedHead: String?
+
+ // MARK: Methods
+
+ public required override init() {
+ super.init()
+
+ if #available(iOS 16.4, *) {
+ webView.isInspectable = true
+ }
+ webView.backgroundColor = .clear
+ webView.isOpaque = false // gets rid of the white flash upon content load in dark mode.
+ webView.translatesAutoresizingMaskIntoConstraints = false
+ webView.navigationDelegate = self
+ webView.scrollView.bounces = false
+ webView.scrollView.showsVerticalScrollIndicator = false
+ webView.scrollView.backgroundColor = .clear
+ }
+
+ public func render(comment: String) -> UIView {
+ guard self.comment != comment else {
+ return webView // Already rendering this comment
+ }
+ self.comment = comment
+
+ // - important: `wordPressSharedBundle` contains custom fonts
+ webView.loadHTMLString(formattedHTMLString(for: comment), baseURL: Bundle.wordPressSharedBundle.bundleURL)
+ return webView
+ }
+}
+
+// MARK: - WKNavigationDelegate
+
+extension WebCommentContentRenderer: WKNavigationDelegate {
+ public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ // Wait until the HTML document finished loading.
+ // This also waits for all of resources within the HTML (images, video thumbnail images) to be fully loaded.
+ webView.evaluateJavaScript("document.readyState") { complete, _ in
+ guard complete != nil else {
+ return
+ }
+
+ // To capture the content height, the methods to use is either `document.body.scrollHeight` or `document.documentElement.scrollHeight`.
+ // `document.body` does not capture margins on tag, so we'll use `document.documentElement` instead.
+ webView.evaluateJavaScript("document.documentElement.scrollHeight") { [weak self] height, _ in
+ guard let self, let height = height as? CGFloat else {
+ return
+ }
+
+ /// The display setting's custom size is applied through the HTML's initial-scale property
+ /// in the meta tag. The `scrollHeight` value seems to return the height as if it's at 1.0 scale,
+ /// so we'll need to add the custom scale into account.
+ let actualHeight = round(height * self.displaySetting.size.scale)
+ self.delegate?.renderer(self, asyncRenderCompletedWithHeight: actualHeight)
+ }
+ }
+ }
+
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
+ switch navigationAction.navigationType {
+ case .other:
+ // allow local file requests.
+ return .allow
+ default:
+ guard let destinationURL = navigationAction.request.url else {
+ return .allow
+ }
+ self.delegate?.renderer(self, interactedWithURL: destinationURL)
+ return .cancel
+ }
+ }
+}
+
+private extension WebCommentContentRenderer {
+ /// Returns a formatted HTML string by loading the template for rich comment.
+ ///
+ /// The method will try to return cached content if possible, by detecting whether the content matches the previous content.
+ /// If it's different (e.g. due to edits), it will reprocess the HTML string.
+ ///
+ /// - Parameter content: The content value from the `Comment` object.
+ /// - Returns: Formatted HTML string to be displayed in the web view.
+ ///
+ func formattedHTMLString(for comment: String) -> String {
+ // remove empty HTML elements from the `content`, as the content often contains empty paragraph elements which adds unnecessary padding/margin.
+ // `rawContent` does not have this problem, but it's not used because `rawContent` gets rid of links ( tags) for mentions.
+ let comment = comment
+ .replacingOccurrences(of: Self.emptyElementRegexPattern, with: "", options: [.regularExpression])
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ return """
+
+ \(makeHead())
+
+ \(comment)
+
+
+ """
+ }
+
+ static let emptyElementRegexPattern = "<[a-z]+>()+<\\/[a-z]+>"
+
+ /// Returns HTML page with the preconfigured styles and scripts.
+ private func makeHead() -> String {
+ if let cachedHead {
+ return cachedHead
+ }
+ let head = actuallyMakeHead()
+ cachedHead = head
+ return head
+ }
+
+ private func actuallyMakeHead() -> String {
+ let meta = "width=device-width,initial-scale=\(displaySetting.size.scale),maximum-scale=\(displaySetting.size.scale),user-scalable=no,shrink-to-fit=no"
+ let styles = displaySetting.makeStyles(tintColor: webView.tintColor)
+ return String(format: Self.headTemplate, meta, styles)
+ }
+
+ private static let headTemplate: String = {
+ guard let fileURL = Bundle.module.url(forResource: "gutenbergCommentHeadTemplate", withExtension: "html"),
+ let string = try? String(contentsOf: fileURL) else {
+ assertionFailure("template missing")
+ return ""
+ }
+ return string
+ }()
+}
diff --git a/WordPress/Resources/HTML/richCommentTemplate.html b/Modules/Sources/WordPressReader/Resources/gutenbergCommentHeadTemplate.html
similarity index 94%
rename from WordPress/Resources/HTML/richCommentTemplate.html
rename to Modules/Sources/WordPressReader/Resources/gutenbergCommentHeadTemplate.html
index 46427605d94c..d73c5f3a3962 100644
--- a/WordPress/Resources/HTML/richCommentTemplate.html
+++ b/Modules/Sources/WordPressReader/Resources/gutenbergCommentHeadTemplate.html
@@ -1,4 +1,3 @@
-
Comment
@@ -27,7 +26,3 @@
document.addEventListener('copy', event => postEvent("commentTextCopied"));
-
- %3$@
-
-
diff --git a/WordPress/Resources/HTML/richCommentStyle.css b/Modules/Sources/WordPressReader/Resources/gutenbergContentStyles.css
similarity index 100%
rename from WordPress/Resources/HTML/richCommentStyle.css
rename to Modules/Sources/WordPressReader/Resources/gutenbergContentStyles.css
diff --git a/Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings+WebKit.swift b/Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings+WebKit.swift
new file mode 100644
index 000000000000..124b07f5fa38
--- /dev/null
+++ b/Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings+WebKit.swift
@@ -0,0 +1,64 @@
+import UIKit
+
+// MARK: - ReaderDisplaySetting (CSS)
+
+extension ReaderDisplaySettings {
+ /// Creates a set of CSS styles that could be applied to a HTML file with
+ /// Gutenberg blocks to render them in a nice app that fits with the design
+ /// of the app.
+ @MainActor
+ func makeStyles(tintColor: UIColor) -> String {
+ Self.baseStylesheet.appending(makeStyleOverrides(tintColor: tintColor))
+ }
+
+ private static let baseStylesheet: String = {
+ guard let fileURL = Bundle.module.url(forResource: "gutenbergContentStyles", withExtension: "css"),
+ let string = try? String(contentsOf: fileURL) else {
+ assertionFailure("css missing")
+ return ""
+ }
+ return string
+ }()
+
+ /// Additional styles based on system or custom theme.
+ private func makeStyleOverrides(tintColor: UIColor) -> String {
+ """
+ :root {
+ --text-font: \(font.cssString);
+ --link-font-weight: \(color == .system ? "inherit" : "600");
+ --link-text-decoration: \(color == .system ? "inherit" : "underline");
+ }
+
+ @media(prefers-color-scheme: light) {
+ \(makeColors(interfaceStyle: .light, tintColor: tintColor))
+ }
+
+ @media(prefers-color-scheme: dark) {
+ \(makeColors(interfaceStyle: .dark, tintColor: tintColor))
+ }
+ """
+ }
+
+ /// CSS color definitions that matches the current color theme.
+ ///
+ /// - parameter interfaceStyle: The current `UIUserInterfaceStyle` value.
+ private func makeColors(interfaceStyle: UIUserInterfaceStyle, tintColor: UIColor) -> String {
+ let trait = UITraitCollection(userInterfaceStyle: interfaceStyle)
+ return """
+ :root {
+ --text-color: \(color.foreground.color(for: trait).cssHex);
+ --text-secondary-color: \(color.secondaryForeground.color(for: trait).cssHex);
+ --link-color: \(tintColor.color(for: trait).cssHex);
+ --mention-background-color: \(tintColor.withAlphaComponent(0.1).color(for: trait).cssHex);
+ --background-secondary-color: \( color.secondaryBackground.color(for: trait).cssHex);
+ --border-color: \(color.border.color(for: trait).cssHex);
+ }
+ """
+ }
+}
+
+private extension UIColor {
+ var cssHex: String {
+ "#\(hexStringWithAlpha)"
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift b/Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings.swift
similarity index 68%
rename from WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift
rename to Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings.swift
index fdcc176662ce..28bbccf27346 100644
--- a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift
+++ b/Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings.swift
@@ -1,26 +1,24 @@
-import WordPressUI
+import UIKit
import WordPressShared
-struct ReaderDisplaySetting: Codable, Equatable {
+public struct ReaderDisplaySettings: Codable, Equatable, Hashable, Sendable {
- static var customizationEnabled: Bool {
- AppConfiguration.isJetpack
- }
+ public static var customizationEnabled: Bool { true }
// MARK: Properties
// The default display setting.
- static let standard = ReaderDisplaySetting(color: .system, font: .sans, size: .normal)
+ public static let standard = ReaderDisplaySettings(color: .system, font: .sans, size: .normal)
- var color: Color
- var font: Font
- var size: Size
+ public var color: Color
+ public var font: Font
+ public var size: Size
- var hasLightBackground: Bool {
+ public var hasLightBackground: Bool {
color.background.brighterThan(0.5)
}
- var isDefaultSetting: Bool {
+ public var isDefaultSetting: Bool {
return self == .standard
}
@@ -34,10 +32,12 @@ struct ReaderDisplaySetting: Codable, Equatable {
/// - textStyle: The preferred text style.
/// - weight: The preferred weight. Defaults to nil, which falls back to the inherent weight from the `UITextStyle`.
/// - Returns: A `UIFont` instance with the specified configuration.
- static func font(with font: Font,
- size: Size = .normal,
- textStyle: UIFont.TextStyle,
- weight: UIFont.Weight? = nil) -> UIFont {
+ public static func font(
+ with font: Font,
+ size: Size = .normal,
+ textStyle: UIFont.TextStyle,
+ weight: UIFont.Weight? = nil
+ ) -> UIFont {
let descriptor: UIFontDescriptor = {
let defaultDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
@@ -66,18 +66,18 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- func font(with textStyle: UIFont.TextStyle, weight: UIFont.Weight = .regular) -> UIFont {
+ public func font(with textStyle: UIFont.TextStyle, weight: UIFont.Weight = .regular) -> UIFont {
return Self.font(with: font, size: size, textStyle: textStyle, weight: weight)
}
- func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) throws -> NSDictionary? {
+ public func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) throws -> NSDictionary? {
let data = try encoder.encode(self)
return try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary
}
// MARK: Types
- enum Color: String, Codable, CaseIterable {
+ public enum Color: String, Codable, CaseIterable, Sendable, Hashable {
case system
case soft
case sepia
@@ -86,7 +86,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
case hacker
case candy
- var label: String {
+ public var label: String {
switch self {
case .system:
return NSLocalizedString(
@@ -133,7 +133,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var foreground: UIColor {
+ public var foreground: UIColor {
switch self {
case .system:
return .label
@@ -152,7 +152,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var secondaryForeground: UIColor {
+ public var secondaryForeground: UIColor {
switch self {
case .system:
return .secondaryLabel
@@ -161,7 +161,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var background: UIColor {
+ public var background: UIColor {
switch self {
case .system:
return .systemBackground
@@ -180,7 +180,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var secondaryBackground: UIColor {
+ public var secondaryBackground: UIColor {
switch self {
case .system:
return .secondarySystemBackground
@@ -191,7 +191,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var border: UIColor {
+ public var border: UIColor {
switch self {
case .system:
return .separator
@@ -201,7 +201,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
/// Whether the color adjusts between light and dark mode.
- var adaptsToInterfaceStyle: Bool {
+ public var adaptsToInterfaceStyle: Bool {
switch self {
case .system:
return true
@@ -210,7 +210,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var valueForTracks: String {
+ public var valueForTracks: String {
switch self {
case .system:
return "default"
@@ -222,12 +222,12 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- enum Font: String, Codable, CaseIterable {
+ public enum Font: String, Codable, CaseIterable, Sendable {
case sans
case serif
case mono
- var cssString: String {
+ public var cssString: String {
switch self {
case .sans:
return "-apple-system, sans-serif"
@@ -238,19 +238,19 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var valueForTracks: String {
+ public var valueForTracks: String {
rawValue
}
}
- enum Size: Int, Codable, CaseIterable {
+ public enum Size: Int, Codable, CaseIterable, Sendable {
case extraSmall = -2
case small
case normal
case large
case extraLarge
- var scale: Double {
+ public var scale: Double {
switch self {
case .extraSmall:
return 0.75
@@ -265,7 +265,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var accessibilityLabel: String {
+ public var accessibilityLabel: String {
switch self {
case .extraSmall:
return NSLocalizedString(
@@ -300,7 +300,7 @@ struct ReaderDisplaySetting: Codable, Equatable {
}
}
- var valueForTracks: String {
+ public var valueForTracks: String {
switch self {
case .extraSmall:
return "extra_small"
@@ -318,123 +318,13 @@ struct ReaderDisplaySetting: Codable, Equatable {
// MARK: Codable
- private enum CodingKeys: String, CodingKey {
+ private enum CodingKeys: String, CodingKey, Sendable {
case color
case font
case size
}
}
-// MARK: - Controller
-
-protocol ReaderDisplaySettingStoreDelegate: NSObjectProtocol {
- func displaySettingDidChange()
-}
-
-/// This should be the object to be strongly retained. Keeps the store up-to-date.
-class ReaderDisplaySettingStore: NSObject {
-
- private let repository: UserPersistentRepository
-
- private let notificationCenter: NotificationCenter
-
- weak var delegate: ReaderDisplaySettingStoreDelegate?
-
- /// A public facade to simplify the flag checking dance for the `ReaderDisplaySetting` object.
- /// When the flag is disabled, this will always return the `standard` object, and the setter does nothing.
- var setting: ReaderDisplaySetting {
- get {
- return ReaderDisplaySetting.customizationEnabled ? _setting : .standard
- }
- set {
- guard ReaderDisplaySetting.customizationEnabled,
- newValue != _setting else {
- return
- }
- _setting = newValue
- broadcastChangeNotification()
- }
- }
-
- /// The actual instance variable that holds the setting object.
- /// This is intentionally set to private so that it's only controllable by `ReaderDisplaySettingStore`.
- private var _setting: ReaderDisplaySetting = .standard {
- didSet {
- guard oldValue != _setting,
- let dictionary = try? setting.toDictionary() else {
- return
- }
- repository.set(dictionary, forKey: Constants.key)
- }
- }
-
- init(repository: UserPersistentRepository = UserPersistentStoreFactory.instance(),
- notificationCenter: NotificationCenter = .default) {
- self.repository = repository
- self.notificationCenter = notificationCenter
- self._setting = {
- guard let dictionary = repository.dictionary(forKey: Constants.key),
- let data = try? JSONSerialization.data(withJSONObject: dictionary),
- let setting = try? JSONDecoder().decode(ReaderDisplaySetting.self, from: data) else {
- return .standard
- }
- return setting
- }()
- super.init()
- registerNotifications()
- }
-
- private func registerNotifications() {
- notificationCenter.addObserver(self,
- selector: #selector(handleChangeNotification),
- name: .readerDisplaySettingStoreDidChange,
- object: nil)
- }
-
- private func broadcastChangeNotification() {
- notificationCenter.post(name: .readerDisplaySettingStoreDidChange, object: self)
- }
-
- @objc
- private func handleChangeNotification(_ notification: NSNotification) {
- // ignore self broadcasts.
- if let broadcaster = notification.object as? ReaderDisplaySettingStore,
- broadcaster == self {
- return
- }
-
- // since we're handling change notifications, a stored setting object *should* exist.
- guard let updatedSetting = try? fetchSetting() else {
- DDLogError("ReaderDisplaySettingStore: Received a didChange notification with a nil stored value")
- return
- }
-
- _setting = updatedSetting
- delegate?.displaySettingDidChange()
- }
-
- /// Fetches the stored value of `ReaderDisplaySetting`.
- ///
- /// - Returns: `ReaderDisplaySetting`
- private func fetchSetting() throws -> ReaderDisplaySetting? {
- guard let dictionary = repository.dictionary(forKey: Constants.key) else {
- return nil
- }
-
- let data = try JSONSerialization.data(withJSONObject: dictionary)
- let setting = try JSONDecoder().decode(ReaderDisplaySetting.self, from: data)
- return setting
- }
-
- private struct Constants {
- static let key = "readerDisplaySettingKey"
- }
-}
-
-fileprivate extension NSNotification.Name {
- static let readerDisplaySettingStoreDidChange = NSNotification.Name("ReaderDisplaySettingDidChange")
-}
-
private extension UIColor {
/**
Whether or not the color brightness is higher than a provided brightness value.
diff --git a/Modules/Sources/WordPressUI/Extensions/UIColor+Helpers.swift b/Modules/Sources/WordPressUI/Extensions/UIColor+Helpers.swift
index 9eb1e0677888..258cf2f80b2f 100644
--- a/Modules/Sources/WordPressUI/Extensions/UIColor+Helpers.swift
+++ b/Modules/Sources/WordPressUI/Extensions/UIColor+Helpers.swift
@@ -14,6 +14,16 @@ public extension UIColor {
}
}
+ convenience init(color: UIColor) {
+ var r: CGFloat = 0
+ var g: CGFloat = 0
+ var b: CGFloat = 0
+ var a: CGFloat = 0
+
+ color.getRed(&r, green: &g, blue: &b, alpha: &a)
+ self.init(red: r, green: g, blue: b, alpha: a)
+ }
+
/// Creates a color based on a hexString. If the string is not a valid hexColor it return nil
/// Example of colors: #FF0000, #00FF0000
///
@@ -117,4 +127,26 @@ public extension UIColor {
preconditionFailure("Color can't be represented in hexadecimal form")
}
+
+ // MARK: - Traits
+
+ func color(for trait: UITraitCollection?) -> UIColor {
+ if let trait {
+ return resolvedColor(with: trait)
+ }
+ return self
+ }
+
+ func lightVariant() -> UIColor {
+ return color(for: UITraitCollection(userInterfaceStyle: .light))
+ }
+
+ func darkVariant() -> UIColor {
+ return color(for: UITraitCollection(userInterfaceStyle: .dark))
+ }
+
+ /// The same color with the dark and light variants swapped
+ var variantInverted: UIColor {
+ UIColor(light: darkVariant(), dark: lightVariant())
+ }
}
diff --git a/WordPress/Classes/Extensions/Colors and Styles/UIColor+Extensions.swift b/WordPress/Classes/Extensions/Colors and Styles/UIColor+Extensions.swift
deleted file mode 100644
index 6da499433fa0..000000000000
--- a/WordPress/Classes/Extensions/Colors and Styles/UIColor+Extensions.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-import Foundation
-import WordPressUI
-
-@objc
-extension UIColor {
-
- convenience init(color: UIColor) {
- var r: CGFloat = 0
- var g: CGFloat = 0
- var b: CGFloat = 0
- var a: CGFloat = 0
-
- color.getRed(&r, green: &g, blue: &b, alpha: &a)
- self.init(red: r, green: g, blue: b, alpha: a)
- }
-
- func color(for trait: UITraitCollection?) -> UIColor {
- if let trait {
- return resolvedColor(with: trait)
- }
- return self
- }
-
- func lightVariant() -> UIColor {
- return color(for: UITraitCollection(userInterfaceStyle: .light))
- }
-
- func darkVariant() -> UIColor {
- return color(for: UITraitCollection(userInterfaceStyle: .dark))
- }
-
- /// The same color with the dark and light variants swapped
- var variantInverted: UIColor {
- UIColor(light: self.darkVariant(), dark: self.lightVariant())
- }
-}
diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
index ce04de452338..91ea86528dfd 100644
--- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
+++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
@@ -15,6 +15,7 @@ enum FeatureFlag: Int, CaseIterable {
case newGutenbergThemeStyles
case newGutenbergPlugins
case selfHostedSiteUserManagement
+ case readerCommentsWebKit
/// Returns a boolean indicating if the feature is enabled
var enabled: Bool {
@@ -49,6 +50,8 @@ enum FeatureFlag: Int, CaseIterable {
return false
case .selfHostedSiteUserManagement:
return false
+ case .readerCommentsWebKit:
+ return false
}
}
@@ -84,6 +87,7 @@ extension FeatureFlag {
case .newGutenbergThemeStyles: "Experimental Block Editor Styles"
case .newGutenbergPlugins: "Experimental Block Editor Plugins"
case .selfHostedSiteUserManagement: "Self-hosted Site User Management"
+ case .readerCommentsWebKit: "Render Comments using WebKit"
}
}
}
diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift
index 679cfb7551e6..87aa040221b9 100644
--- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift
+++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift
@@ -51,6 +51,7 @@ class CommentDetailViewController: UIViewController, NoResultsViewHost {
}
}
private var notification: Notification?
+ private let helper = ReaderCommentsHelper()
private var isNotificationComment: Bool {
notification != nil
@@ -441,7 +442,7 @@ private extension CommentDetailViewController {
}
func configureContentCell(_ cell: CommentContentTableViewCell, comment: Comment) {
- cell.configure(with: comment) { [weak self] _ in
+ cell.configure(with: comment, helper: helper) { [weak self] _ in
self?.tableView.performBatchUpdates({})
}
diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift
index 89acdcacf975..fa3c82d5ee7f 100644
--- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift
+++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift
@@ -1,5 +1,6 @@
import UIKit
import WordPressUI
+import WordPressReader
import Gravatar
class CommentContentTableViewCell: UITableViewCell, NibReusable {
@@ -119,9 +120,9 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
/// Called when the cell has finished loading and calculating the height of the HTML content. Passes the new content height as parameter.
private var onContentLoaded: ((CGFloat) -> Void)? = nil
- private var renderer: CommentContentRenderer? = nil
-
+ private var comment: Comment?
private var renderMethod: RenderMethod?
+ private var helper: ReaderCommentsHelper?
// MARK: Like Button State
@@ -133,10 +134,10 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
/// can be scoped by using the "legacy" style when the passed parameter is nil.
private var style: CellStyle = .init(displaySetting: nil)
- var displaySetting: ReaderDisplaySetting? = nil {
+ // TODO: (kean) remove
+ var displaySetting: ReaderDisplaySettings? = nil {
didSet {
style = CellStyle(displaySetting: displaySetting)
- resetRenderedContents()
applyStyles()
}
}
@@ -198,7 +199,15 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
/// - comment: The `Comment` object to display.
/// - renderMethod: Specifies how to display the comment body. See `RenderMethod`.
/// - onContentLoaded: Callback to be called once the content has been loaded. Provides the new content height as parameter.
- func configure(with comment: Comment, renderMethod: RenderMethod = .web, onContentLoaded: ((CGFloat) -> Void)?) {
+ func configure(
+ with comment: Comment,
+ renderMethod: RenderMethod = .web,
+ helper: ReaderCommentsHelper,
+ onContentLoaded: ((CGFloat) -> Void)?
+ ) {
+ self.comment = comment
+ self.helper = helper
+
nameLabel?.setText(comment.authorForDisplay())
dateLabel?.setText(comment.dateForDisplay()?.toMediumString() ?? String())
@@ -229,7 +238,7 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
// Configure content renderer.
self.onContentLoaded = onContentLoaded
- configureRendererIfNeeded(for: comment, renderMethod: renderMethod)
+ configureRendererIfNeeded(for: comment, renderMethod: renderMethod, helper: helper)
}
/// Configures the cell with a `Comment` object, to be displayed in the post details view.
@@ -237,8 +246,8 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
/// - Parameters:
/// - comment: The `Comment` object to display.
/// - onContentLoaded: Callback to be called once the content has been loaded. Provides the new content height as parameter.
- func configureForPostDetails(with comment: Comment, onContentLoaded: ((CGFloat) -> Void)?) {
- configure(with: comment, onContentLoaded: onContentLoaded)
+ func configureForPostDetails(with comment: Comment, helper: ReaderCommentsHelper, onContentLoaded: ((CGFloat) -> Void)?) {
+ configure(with: comment, helper: helper, onContentLoaded: onContentLoaded)
isCommentLikesEnabled = false
isCommentReplyEnabled = false
@@ -267,9 +276,18 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
extension CommentContentTableViewCell: CommentContentRendererDelegate {
func renderer(_ renderer: CommentContentRenderer, asyncRenderCompletedWithHeight height: CGFloat) {
if renderMethod == .web {
- contentContainerHeightConstraint?.constant = height
+ if let constraint = contentContainerHeightConstraint, let comment {
+ if height != constraint.constant {
+ constraint.constant = height
+ helper?.setCachedContentHeight(height, for: .init(comment))
+ onContentLoaded?(height) // We had the right size from the get-go
+ }
+ } else {
+ wpAssertionFailure("constraint missing")
+ }
+ } else {
+ onContentLoaded?(height)
}
- onContentLoaded?(height)
}
func renderer(_ renderer: CommentContentRenderer, interactedWithURL url: URL) {
@@ -283,11 +301,11 @@ private extension CommentContentTableViewCell {
/// A structure to override the cell styling based on `ReaderDisplaySetting`.
/// This doesn't cover all aspects of the cell, and iks currently scoped only for Reader Detail.
struct CellStyle {
- let displaySetting: ReaderDisplaySetting?
+ let displaySetting: ReaderDisplaySettings?
/// NOTE: Remove when the `readerCustomization` flag is removed.
var customizationEnabled: Bool {
- ReaderDisplaySetting.customizationEnabled
+ ReaderDisplaySettings.customizationEnabled
}
// Name Label
@@ -470,52 +488,38 @@ private extension CommentContentTableViewCell {
// MARK: Content Rendering
- func resetRenderedContents() {
- renderer = nil
- contentContainerView.subviews.forEach { $0.removeFromSuperview() }
- }
-
- func configureRendererIfNeeded(for comment: Comment, renderMethod: RenderMethod) {
- // skip creating the renderer if the content does not change.
- // this prevents the cell to jump multiple times due to consecutive reloadData calls.
- //
- // note that this doesn't apply for `.richContent` method. Always reset the textView instead
- // of reusing it to prevent crash. Ref: http://git.io/Jtl2U
- if let renderer,
- renderer.matchesContent(from: comment),
- renderMethod == .web {
- return
- }
-
- // clean out any pre-existing renderer just to be sure.
- resetRenderedContents()
-
- var renderer: CommentContentRenderer = {
+ func configureRendererIfNeeded(for comment: Comment, renderMethod: RenderMethod, helper: ReaderCommentsHelper) {
+ let renderer: CommentContentRenderer = {
switch renderMethod {
case .web:
- return WebCommentContentRenderer(comment: comment, displaySetting: displaySetting ?? .standard)
+ return helper.getRenderer(for: comment)
case .richContent(let attributedText):
- let renderer = RichCommentContentRenderer(comment: comment)
+ let renderer = RichCommentContentRenderer()
renderer.richContentDelegate = self.richContentDelegate
renderer.attributedText = attributedText
+ renderer.comment = comment
return renderer
}
}()
renderer.delegate = self
- self.renderer = renderer
- self.renderMethod = renderMethod
+
+ self.renderMethod = renderMethod // we assume the render method can't change
if renderMethod == .web {
// reset height constraint to handle cases where the new content requires the webview to shrink.
contentContainerHeightConstraint?.isActive = true
- contentContainerHeightConstraint?.constant = 1
+ contentContainerHeightConstraint?.constant = helper.getCachedContentHeight(for: TaggedManagedObjectID(comment)) ?? 20
} else {
contentContainerHeightConstraint?.isActive = false
}
- let contentView = renderer.render()
- contentContainerView?.addSubview(contentView)
- contentContainerView?.pinSubviewToAllEdges(contentView)
+ let contentView = renderer.render(comment: comment.content)
+ if contentContainerView.subviews.first != contentView {
+ contentContainerView.subviews.forEach { $0.removeFromSuperview() }
+ contentView.removeFromSuperview()
+ contentContainerView?.addSubview(contentView)
+ contentView.pinEdges()
+ }
}
// MARK: Button Actions
diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift
index 1fb14ad14e7e..e638a3a71507 100644
--- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift
+++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift
@@ -1,5 +1,6 @@
import UIKit
import AsyncImageKit
+import WordPressReader
/// Renders the comment body through `WPRichContentView`.
///
@@ -8,24 +9,16 @@ class RichCommentContentRenderer: NSObject, CommentContentRenderer {
weak var richContentDelegate: WPRichContentViewDelegate? = nil
var attributedText: NSAttributedString?
+ var comment: Comment?
- private let comment: Comment
+ required override init() {}
- required init(comment: Comment) {
- self.comment = comment
- }
-
- func render() -> UIView {
+ func render(comment: String) -> UIView {
let textView = newRichContentView()
textView.attributedText = attributedText
textView.delegate = self
-
return textView
}
-
- func matchesContent(from comment: Comment) -> Bool {
- return self.comment.content == comment.content
- }
}
// MARK: - WPRichContentViewDelegate
@@ -73,6 +66,9 @@ private extension RichCommentContentRenderer {
}
var mediaHost: MediaHost {
+ guard let comment else {
+ return .publicSite
+ }
if let blog = comment.blog {
return MediaHost(blog)
} else if let post = comment.post as? ReaderPost, post.isBlogPrivate {
diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/WebCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/WebCommentContentRenderer.swift
deleted file mode 100644
index 123d8b910f89..000000000000
--- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/WebCommentContentRenderer.swift
+++ /dev/null
@@ -1,293 +0,0 @@
-@preconcurrency import WebKit
-import WordPressShared
-
-/// Renders the comment body with a web view. Provides the best visual experience but has the highest performance cost.
-///
-class WebCommentContentRenderer: NSObject, CommentContentRenderer {
-
- // MARK: Properties
-
- weak var delegate: CommentContentRendererDelegate?
-
- private let comment: Comment
-
- private let webView = WKWebView(frame: .zero)
-
- /// Used to determine whether the cache is still valid or not.
- private var commentContentCache: String? = nil
-
- /// Caches the HTML content, to be reused when the orientation changed.
- private var htmlContentCache: String? = nil
-
- private let displaySetting: ReaderDisplaySetting
-
- // MARK: Methods
-
- required convenience init(comment: Comment) {
- self.init(comment: comment, displaySetting: .standard)
- }
-
- required init(comment: Comment, displaySetting: ReaderDisplaySetting) {
- self.comment = comment
- self.displaySetting = displaySetting
- super.init()
-
- if #available(iOS 16.4, *) {
- webView.isInspectable = true
- }
-
- webView.backgroundColor = .clear
- webView.isOpaque = false // gets rid of the white flash upon content load in dark mode.
- webView.translatesAutoresizingMaskIntoConstraints = false
- webView.navigationDelegate = self
- webView.scrollView.bounces = false
- webView.scrollView.showsVerticalScrollIndicator = false
- webView.scrollView.backgroundColor = .clear
- webView.configuration.allowsInlineMediaPlayback = true
-
- // - warning: It retains the handler. It can't be `self`.
- webView.configuration.userContentController.add(ReaderWebViewMessageHandler(), name: "eventHandler")
- }
-
- func render() -> UIView {
- // Do not reload if the content doesn't change.
- if let contentCache = commentContentCache, contentCache == comment.content {
- return webView
- }
-
- webView.loadHTMLString(formattedHTMLString(for: comment.content), baseURL: Bundle.wordPressSharedBundle.bundleURL)
-
- return webView
- }
-
- func matchesContent(from comment: Comment) -> Bool {
- // if content cache is still nil, then the comment hasn't been rendered yet.
- guard let contentCache = commentContentCache else {
- return false
- }
-
- return contentCache == comment.content
- }
-}
-
-// MARK: - WKNavigationDelegate
-
-extension WebCommentContentRenderer: WKNavigationDelegate {
- func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
- // Wait until the HTML document finished loading.
- // This also waits for all of resources within the HTML (images, video thumbnail images) to be fully loaded.
- webView.evaluateJavaScript("document.readyState") { complete, _ in
- guard complete != nil else {
- return
- }
-
- // To capture the content height, the methods to use is either `document.body.scrollHeight` or `document.documentElement.scrollHeight`.
- // `document.body` does not capture margins on tag, so we'll use `document.documentElement` instead.
- webView.evaluateJavaScript("document.documentElement.scrollHeight") { [weak self] height, _ in
- guard let self,
- let height = height as? CGFloat else {
- return
- }
-
- /// The display setting's custom size is applied through the HTML's initial-scale property
- /// in the meta tag. The `scrollHeight` value seems to return the height as if it's at 1.0 scale,
- /// so we'll need to add the custom scale into account.
- let actualHeight = round(height * self.displaySetting.size.scale)
- self.delegate?.renderer(self, asyncRenderCompletedWithHeight: actualHeight)
- }
- }
- }
-
- func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
- switch navigationAction.navigationType {
- case .other:
- // allow local file requests.
- decisionHandler(.allow)
- default:
- decisionHandler(.cancel)
- guard let destinationURL = navigationAction.request.url else {
- return
- }
-
- self.delegate?.renderer(self, interactedWithURL: destinationURL)
- }
- }
-}
-
-// MARK: - Private Methods
-
-private extension WebCommentContentRenderer {
- struct Constants {
- static let emptyElementRegexPattern = "<[a-z]+>()+<\\/[a-z]+>"
-
- static let highlightColor = UIColor(light: UIAppColor.primary, dark: UIAppColor.primary(.shade30))
-
- static let mentionBackgroundColor: UIColor = {
- var darkColor = UIAppColor.primary(.shade90)
-
- if AppConfiguration.isWordPress {
- darkColor = darkColor.withAlphaComponent(0.5)
- }
-
- return UIColor(light: UIAppColor.primary(.shade0), dark: darkColor)
- }()
- }
-
- /// Used for the web view's `baseURL`, to reference any local files (i.e. CSS) linked from the HTML.
- static let resourceURL: URL? = {
- Bundle.wordPressSharedBundle.bundleURL
- }()
-
- var textColor: UIColor {
- ReaderDisplaySetting.customizationEnabled ? displaySetting.color.foreground : .label
- }
-
- var mentionBackgroundColor: UIColor {
- guard ReaderDisplaySetting.customizationEnabled else {
- return Constants.mentionBackgroundColor
- }
-
- return displaySetting.color == .system ? Constants.mentionBackgroundColor : displaySetting.color.secondaryBackground
- }
-
- var linkColor: UIColor {
- guard ReaderDisplaySetting.customizationEnabled else {
- return Constants.highlightColor
- }
-
- return displaySetting.color == .system ? Constants.highlightColor : displaySetting.color.foreground
- }
-
- var secondaryBackgroundColor: UIColor {
- guard ReaderDisplaySetting.customizationEnabled else {
- return .secondarySystemBackground
- }
- return displaySetting.color.secondaryBackground
- }
-
- /// Cache the HTML template format. We only need read the template once.
- var htmlTemplateFormat: String? {
- guard let templatePath = Bundle.main.path(forResource: "richCommentTemplate", ofType: "html"),
- let templateStringFormat = try? String(contentsOfFile: templatePath) else {
- return nil
- }
-
- return String(format: templateStringFormat,
- metaContents.joined(separator: ", "),
- cssStyles,
- "%@")
- }
-
- var metaContents: [String] {
- [
- "width=device-width",
- "initial-scale=\(displaySetting.size.scale)",
- "maximum-scale=\(displaySetting.size.scale)",
- "user-scalable=no",
- "shrink-to-fit=no"
- ]
- }
-
- /// We'll need to load `richCommentStyle.css` from the main bundle and inject it as a string,
- /// because the web view needs to be loaded with the WordPressShared bundle to gain access to custom fonts.
- var cssStyles: String {
- guard let cssURL = Bundle.main.url(forResource: "richCommentStyle", withExtension: "css"),
- let cssContent = try? String(contentsOf: cssURL) else {
- return String()
- }
- return cssContent.appending(overrideStyles)
- }
-
- /// Additional styles based on system or custom theme.
- var overrideStyles: String {
- """
- /* Basic style variables */
- :root {
- --text-font: \(displaySetting.font.cssString);
-
- /* link styling */
- --link-font-weight: \(displaySetting.color == .system ? "inherit" : "600");
- --link-text-decoration: \(displaySetting.color == .system ? "inherit" : "underline");
- }
-
- /* Color overrides for light mode */
- @media(prefers-color-scheme: light) {
- \(cssColors(interfaceStyle: .light))
- }
-
- /* Color overrides for dark mode */
- @media(prefers-color-scheme: dark) {
- \(cssColors(interfaceStyle: .dark))
- }
- """
- }
-
- /// CSS color definitions that matches the current color theme.
- /// - Parameter interfaceStyle: The current `UIUserInterfaceStyle` value.
- /// - Returns: A string of CSS colors to be injected.
- private func cssColors(interfaceStyle: UIUserInterfaceStyle) -> String {
- let trait = UITraitCollection(userInterfaceStyle: interfaceStyle)
-
- return """
- :root {
- --text-color: \(textColor.color(for: trait).cssRGBAString());
- --text-secondary-color: \(displaySetting.color.secondaryForeground.color(for: trait).cssRGBAString());
- --link-color: \(linkColor.color(for: trait).cssRGBAString());
- --mention-background-color: \(mentionBackgroundColor.color(for: trait).cssRGBAString());
- --background-secondary-color: \(secondaryBackgroundColor.color(for: trait).cssRGBAString());
- --border-color: \(displaySetting.color.border.color(for: trait).cssRGBAString());
- }
- """
- }
-
- /// Returns a formatted HTML string by loading the template for rich comment.
- ///
- /// The method will try to return cached content if possible, by detecting whether the content matches the previous content.
- /// If it's different (e.g. due to edits), it will reprocess the HTML string.
- ///
- /// - Parameter content: The content value from the `Comment` object.
- /// - Returns: Formatted HTML string to be displayed in the web view.
- ///
- func formattedHTMLString(for content: String) -> String {
- // return the previous HTML string if the comment content is unchanged.
- if let previousCommentContent = commentContentCache,
- let previousHTMLString = htmlContentCache,
- previousCommentContent == content {
- return previousHTMLString
- }
-
- // otherwise: sanitize the content, cache it, and then return it.
- guard let htmlTemplateFormat else {
- DDLogError("WebCommentContentRenderer: Failed to load HTML template format for comment content.")
- return String()
- }
-
- // remove empty HTML elements from the `content`, as the content often contains empty paragraph elements which adds unnecessary padding/margin.
- // `rawContent` does not have this problem, but it's not used because `rawContent` gets rid of links ( tags) for mentions.
- let htmlContent = String(format: htmlTemplateFormat, content
- .replacingOccurrences(of: Constants.emptyElementRegexPattern, with: String(), options: [.regularExpression])
- .trimmingCharacters(in: .whitespacesAndNewlines))
-
- // cache the contents.
- commentContentCache = content
- htmlContentCache = htmlContent
-
- return htmlContent
- }
-}
-
-private extension UIColor {
- func cssRGBAString(customAlpha: CGFloat? = nil) -> String {
- let red = Int(rgbaComponents.red * 255)
- let green = Int(rgbaComponents.green * 255)
- let blue = Int(rgbaComponents.blue * 255)
- let alpha = {
- guard let customAlpha, customAlpha <= 1.0 else {
- return rgbaComponents.alpha
- }
- return customAlpha
- }()
-
- return "rgba(\(red), \(green), \(blue), \(alpha))"
- }
-}
diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/Tags View/ReaderTopicCollectionViewCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Cards/Tags View/ReaderTopicCollectionViewCoordinator.swift
index 05772934527e..0b01f8c6c1f3 100644
--- a/WordPress/Classes/ViewRelated/Reader/Cards/Tags View/ReaderTopicCollectionViewCoordinator.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Cards/Tags View/ReaderTopicCollectionViewCoordinator.swift
@@ -1,4 +1,5 @@
import UIKit
+import WordPressReader
enum ReaderTopicCollectionViewState {
case collapsed
@@ -43,7 +44,7 @@ class ReaderTopicCollectionViewCoordinator: NSObject {
}
/// For custom styling. When nil, the cell will be configured with default styling.
- var displaySetting: ReaderDisplaySetting? = nil
+ var displaySetting: ReaderDisplaySettings? = nil
init(collectionView: UICollectionView, topics: [String]) {
self.collectionView = collectionView
diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift
new file mode 100644
index 000000000000..cd27d4d72283
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift
@@ -0,0 +1,31 @@
+import Foundation
+import WordPressReader
+
+/// A collection of utilities for managing rendering for comments.
+@MainActor
+@objc class ReaderCommentsHelper: NSObject {
+ private var contentHeights: [TaggedManagedObjectID: CGFloat] = [:]
+ private let renderers = NSCache()
+
+ override init() {
+ renderers.countLimit = 30
+ }
+
+ func getRenderer(for comment: Comment) -> WebCommentContentRenderer {
+ if let renderer = renderers.object(forKey: comment) {
+ return renderer
+ }
+ let renderer = WebCommentContentRenderer()
+ renderer.tintColor = UIAppColor.primary
+ renderers.setObject(renderer, forKey: comment)
+ return renderer
+ }
+
+ func getCachedContentHeight(for commentID: TaggedManagedObjectID) -> CGFloat? {
+ contentHeights[commentID]
+ }
+
+ func setCachedContentHeight(_ height: CGFloat, for commentID: TaggedManagedObjectID) {
+ contentHeights[commentID] = height
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h
index 65b4a6d3bfa7..95b5ed27a726 100644
--- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h
+++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.h
@@ -13,14 +13,16 @@ typedef NS_ENUM(NSUInteger, ReaderCommentsSource) {
ReaderCommentsSourcePostsList
};
-
+@class Comment;
@class ReaderPost;
+@class ReaderCommentsHelper;
@interface ReaderCommentsViewController : UIViewController
@property (nonatomic, strong, readonly) ReaderPost *post;
@property (nonatomic, assign, readwrite) BOOL allowsPushingPostDetails;
@property (nonatomic, assign, readwrite) ReaderCommentsSource source;
+@property (nonatomic, strong, readonly) ReaderCommentsHelper *helper;
- (void)setupWithPostID:(NSNumber *)postID siteID:(NSNumber *)siteID;
@@ -36,5 +38,6 @@ typedef NS_ENUM(NSUInteger, ReaderCommentsSource) {
// Comment moderation support.
@property (nonatomic, assign, readwrite) BOOL commentModified;
- (void)refreshAfterCommentModeration;
+- (NSAttributedString *)cacheContentForComment:(Comment *)comment;
@end
diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m
index e0610eaedf52..7723d3e922bd 100644
--- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m
+++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m
@@ -53,6 +53,7 @@ @interface ReaderCommentsViewController () Void)?
private var totalRows = 0
private var hideButton = true
- var displaySetting: ReaderDisplaySetting
+ var displaySetting: ReaderDisplaySettings
private var comments: [Comment] = [] {
didSet {
@@ -42,7 +45,7 @@ class ReaderDetailCommentsTableViewDelegate: NSObject, UITableViewDataSource, UI
// MARK: - Public Methods
- init(displaySetting: ReaderDisplaySetting = .standard) {
+ init(displaySetting: ReaderDisplaySettings = .standard) {
self.displaySetting = displaySetting
}
@@ -85,7 +88,7 @@ class ReaderDetailCommentsTableViewDelegate: NSObject, UITableViewDataSource, UI
}
cell.displaySetting = displaySetting
- cell.configureForPostDetails(with: comment) { _ in
+ cell.configureForPostDetails(with: comment, helper: helper) { _ in
do {
try WPException.objcTry {
tableView.performBatchUpdates({})
@@ -109,7 +112,7 @@ class ReaderDetailCommentsTableViewDelegate: NSObject, UITableViewDataSource, UI
cell.backgroundColor = .clear
cell.contentView.backgroundColor = .clear
- if ReaderDisplaySetting.customizationEnabled {
+ if ReaderDisplaySettings.customizationEnabled {
cell.titleLabel.font = displaySetting.font(with: .body)
cell.titleLabel.textColor = displaySetting.color.secondaryForeground
}
@@ -175,7 +178,7 @@ private extension ReaderDetailCommentsTableViewDelegate {
let cell = BorderedButtonTableViewCell()
let title = totalComments == 0 ? Constants.leaveCommentButtonTitle : Constants.viewAllButtonTitle
- if ReaderDisplaySetting.customizationEnabled {
+ if ReaderDisplaySettings.customizationEnabled {
cell.configure(buttonTitle: title,
titleFont: displaySetting.font(with: .body, weight: .semibold),
normalColor: displaySetting.color.foreground,
diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift
index 4d9b516a27e4..c959c5099c71 100644
--- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift
@@ -1,6 +1,7 @@
import UIKit
import AsyncImageKit
import WordPressUI
+import WordPressReader
protocol ReaderDetailFeaturedImageViewDelegate: AnyObject {
func didTapFeaturedImage(_ sender: AsyncImageView)
@@ -31,7 +32,7 @@ final class ReaderDetailFeaturedImageView: UIView {
self.endTintColor = endTintColor
}
- init(displaySetting: ReaderDisplaySetting) {
+ init(displaySetting: ReaderDisplaySettings) {
self.init(endTintColor: displaySetting.color.foreground)
}
}
@@ -60,7 +61,7 @@ final class ReaderDetailFeaturedImageView: UIView {
}
}
- var displaySetting: ReaderDisplaySetting = .standard {
+ var displaySetting: ReaderDisplaySettings = .standard {
didSet {
style = .init(displaySetting: displaySetting)
@@ -188,7 +189,7 @@ final class ReaderDetailFeaturedImageView: UIView {
// Re-apply the styles after a potential orientation change.
// This fixes a case where the navbar tint would revert after changing orientation.
- if ReaderDisplaySetting.customizationEnabled {
+ if ReaderDisplaySettings.customizationEnabled {
resetNavigationBarTintColor()
resetStatusBarStyle()
}
diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift
index bfc1b4b8fc3e..49608a8d4e7a 100644
--- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift
@@ -1,6 +1,7 @@
import SwiftUI
import AsyncImageKit
import WordPressUI
+import WordPressReader
protocol ReaderDetailHeaderViewDelegate: AnyObject {
func didTapBlogName()
@@ -18,7 +19,7 @@ final class ReaderDetailHeaderHostingView: UIView {
}
}
- var displaySetting: ReaderDisplaySetting = .standard {
+ var displaySetting: ReaderDisplaySettings = .standard {
didSet {
viewModel.displaySetting = displaySetting
Task { @MainActor in
@@ -118,7 +119,7 @@ class ReaderDetailHeaderViewModel: ObservableObject {
@Published var showsAuthorName: Bool = true
- @Published var displaySetting: ReaderDisplaySetting
+ @Published var displaySetting: ReaderDisplaySettings
var likeCountString: String? {
guard let count = likeCount, count > 0 else {
@@ -134,7 +135,7 @@ class ReaderDetailHeaderViewModel: ObservableObject {
return WPStyleGuide.commentCountForDisplay(count)
}
- init(displaySetting: ReaderDisplaySetting, coreDataStack: CoreDataStackSwift = ContextManager.shared) {
+ init(displaySetting: ReaderDisplaySettings, coreDataStack: CoreDataStackSwift = ContextManager.shared) {
self.displaySetting = displaySetting
self.coreDataStack = coreDataStack
}
@@ -444,10 +445,10 @@ fileprivate extension ReaderDetailHeaderView {
fileprivate struct ReaderDetailTagsWrapperView: UIViewRepresentable {
private let topics: [String]
- private let displaySetting: ReaderDisplaySetting
+ private let displaySetting: ReaderDisplaySettings
private weak var delegate: ReaderTopicCollectionViewCoordinatorDelegate?
- init(topics: [String], displaySetting: ReaderDisplaySetting, delegate: ReaderTopicCollectionViewCoordinatorDelegate?) {
+ init(topics: [String], displaySetting: ReaderDisplaySettings, delegate: ReaderTopicCollectionViewCoordinatorDelegate?) {
self.topics = topics
self.displaySetting = displaySetting
self.delegate = delegate
@@ -458,7 +459,7 @@ fileprivate struct ReaderDetailTagsWrapperView: UIViewRepresentable {
view.topics = topics
view.topicDelegate = delegate
- if ReaderDisplaySetting.customizationEnabled {
+ if ReaderDisplaySettings.customizationEnabled {
view.coordinator?.displaySetting = displaySetting
}
@@ -470,7 +471,7 @@ fileprivate struct ReaderDetailTagsWrapperView: UIViewRepresentable {
func updateUIView(_ uiView: UICollectionView, context: Context) {
if let view = uiView as? TopicsCollectionView,
- ReaderDisplaySetting.customizationEnabled {
+ ReaderDisplaySettings.customizationEnabled {
view.coordinator?.displaySetting = displaySetting
}
diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift
index 50c654170bcf..590db5e05664 100644
--- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift
@@ -1,4 +1,5 @@
import UIKit
+import WordPressReader
protocol ReaderDetailLikesViewDelegate: AnyObject {
func didTapLikesView()
@@ -9,7 +10,7 @@ final class ReaderDetailLikesView: UIView, NibLoadable {
@IBOutlet private weak var summaryLabel: UILabel!
@IBOutlet private weak var selfAvatarImageView: CircularImageView!
- var displaySetting: ReaderDisplaySetting = .standard {
+ var displaySetting: ReaderDisplaySettings = .standard {
didSet {
applyStyles()
if let viewModel {
@@ -96,7 +97,7 @@ private extension ReaderDetailLikesView {
}
}
-private func makeHighlightedText(_ text: String, displaySetting: ReaderDisplaySetting) -> NSAttributedString {
+private func makeHighlightedText(_ text: String, displaySetting: ReaderDisplaySettings) -> NSAttributedString {
let labelParts = text.components(separatedBy: "_")
let firstPart = labelParts.first ?? ""
diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift
index 1d2d814e6c6e..b193800d5183 100644
--- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailToolbar.swift
@@ -1,5 +1,6 @@
import UIKit
import WordPressUI
+import WordPressReader
protocol ReaderDetailToolbarDelegate: AnyObject {
var notificationID: String? { get }
@@ -29,7 +30,7 @@ class ReaderDetailToolbar: UIView, NibLoadable {
weak var delegate: ReaderDetailToolbarDelegate? = nil
- var displaySetting: ReaderDisplaySetting = .standard {
+ var displaySetting: ReaderDisplaySettings = .standard {
didSet {
applyStyles()
configureActionButtons()
diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/WebView/ReaderWebView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/WebView/ReaderWebView.swift
index 344e9e50d052..7a6faef75cec 100644
--- a/WordPress/Classes/ViewRelated/Reader/Detail/WebView/ReaderWebView.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Detail/WebView/ReaderWebView.swift
@@ -1,5 +1,6 @@
import UIKit
import ColorStudio
+import WordPressReader
/// A WKWebView that renders post content with styles applied
///
@@ -15,7 +16,11 @@ class ReaderWebView: WKWebView {
var isP2 = false
- var displaySetting: ReaderDisplaySetting = .standard
+ var displaySetting: ReaderDisplaySettings = .standard
+
+ deinit {
+ print("here")
+ }
/// Make the webview transparent
///
diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderFollowButton.swift b/WordPress/Classes/ViewRelated/Reader/ReaderFollowButton.swift
index 4827d34cfe3d..8a74d3e0ab09 100644
--- a/WordPress/Classes/ViewRelated/Reader/ReaderFollowButton.swift
+++ b/WordPress/Classes/ViewRelated/Reader/ReaderFollowButton.swift
@@ -1,4 +1,5 @@
import SwiftUI
+import WordPressReader
struct ReaderFollowButton: View {
@@ -6,7 +7,7 @@ struct ReaderFollowButton: View {
let isEnabled: Bool
let size: ButtonSize
var color: ButtonColor = .init()
- var displaySetting: ReaderDisplaySetting?
+ var displaySetting: ReaderDisplaySettings?
let action: () -> Void
@@ -30,7 +31,7 @@ struct ReaderFollowButton: View {
self.unfollowedBackground = unfollowedBackground
}
- init(displaySetting: ReaderDisplaySetting) {
+ init(displaySetting: ReaderDisplaySettings) {
followedText = Color(displaySetting.color.secondaryForeground)
followedBackground = .clear
followedStroke = followedText
@@ -48,7 +49,7 @@ struct ReaderFollowButton: View {
isEnabled: Bool,
size: ButtonSize,
color: ButtonColor? = nil,
- displaySetting: ReaderDisplaySetting? = nil,
+ displaySetting: ReaderDisplaySettings? = nil,
action: @escaping () -> Void) {
self.isFollowing = isFollowing
self.isEnabled = isEnabled
diff --git a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingStore.swift b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingStore.swift
new file mode 100644
index 000000000000..81c3057e368b
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingStore.swift
@@ -0,0 +1,113 @@
+import WordPressUI
+import WordPressShared
+import WordPressReader
+
+// MARK: - Controller
+
+protocol ReaderDisplaySettingStoreDelegate: NSObjectProtocol {
+ func displaySettingDidChange()
+}
+
+/// This should be the object to be strongly retained. Keeps the store up-to-date.
+class ReaderDisplaySettingStore: NSObject {
+
+ private let repository: UserPersistentRepository
+
+ private let notificationCenter: NotificationCenter
+
+ weak var delegate: ReaderDisplaySettingStoreDelegate?
+
+ /// A public facade to simplify the flag checking dance for the `ReaderDisplaySetting` object.
+ /// When the flag is disabled, this will always return the `standard` object, and the setter does nothing.
+ var setting: ReaderDisplaySettings {
+ get {
+ return ReaderDisplaySettings.customizationEnabled ? _setting : .standard
+ }
+ set {
+ guard ReaderDisplaySettings.customizationEnabled,
+ newValue != _setting else {
+ return
+ }
+ _setting = newValue
+ broadcastChangeNotification()
+ }
+ }
+
+ /// The actual instance variable that holds the setting object.
+ /// This is intentionally set to private so that it's only controllable by `ReaderDisplaySettingStore`.
+ private var _setting: ReaderDisplaySettings = .standard {
+ didSet {
+ guard oldValue != _setting,
+ let dictionary = try? setting.toDictionary() else {
+ return
+ }
+ repository.set(dictionary, forKey: Constants.key)
+ }
+ }
+
+ init(repository: UserPersistentRepository = UserPersistentStoreFactory.instance(),
+ notificationCenter: NotificationCenter = .default) {
+ self.repository = repository
+ self.notificationCenter = notificationCenter
+ self._setting = {
+ guard let dictionary = repository.dictionary(forKey: Constants.key),
+ let data = try? JSONSerialization.data(withJSONObject: dictionary),
+ let setting = try? JSONDecoder().decode(ReaderDisplaySettings.self, from: data) else {
+ return .standard
+ }
+ return setting
+ }()
+ super.init()
+ registerNotifications()
+ }
+
+ private func registerNotifications() {
+ notificationCenter.addObserver(self,
+ selector: #selector(handleChangeNotification),
+ name: .readerDisplaySettingStoreDidChange,
+ object: nil)
+ }
+
+ private func broadcastChangeNotification() {
+ notificationCenter.post(name: .readerDisplaySettingStoreDidChange, object: self)
+ }
+
+ @objc
+ private func handleChangeNotification(_ notification: NSNotification) {
+ // ignore self broadcasts.
+ if let broadcaster = notification.object as? ReaderDisplaySettingStore,
+ broadcaster == self {
+ return
+ }
+
+ // since we're handling change notifications, a stored setting object *should* exist.
+ guard let updatedSetting = try? fetchSetting() else {
+ DDLogError("ReaderDisplaySettingStore: Received a didChange notification with a nil stored value")
+ return
+ }
+
+ _setting = updatedSetting
+ delegate?.displaySettingDidChange()
+ }
+
+ /// Fetches the stored value of `ReaderDisplaySetting`.
+ ///
+ /// - Returns: `ReaderDisplaySetting`
+ private func fetchSetting() throws -> ReaderDisplaySettings? {
+ guard let dictionary = repository.dictionary(forKey: Constants.key) else {
+ return nil
+ }
+
+ let data = try JSONSerialization.data(withJSONObject: dictionary)
+ let setting = try JSONDecoder().decode(ReaderDisplaySettings.self, from: data)
+ return setting
+ }
+
+ private struct Constants {
+ static let key = "readerDisplaySettingKey"
+ }
+}
+
+fileprivate extension NSNotification.Name {
+ static let readerDisplaySettingStoreDidChange = NSNotification.Name("ReaderDisplaySettingDidChange")
+}
diff --git a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingViewController.swift b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingViewController.swift
index afe9a012e9ac..412e1f71ec94 100644
--- a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingViewController.swift
+++ b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySettingViewController.swift
@@ -1,5 +1,6 @@
import SwiftUI
import DesignSystem
+import WordPressReader
/// The tracking source values for the customization sheet.
/// The values are kept in sync with Android.
@@ -9,8 +10,8 @@ enum ReaderDisplaySettingViewSource: String {
}
class ReaderDisplaySettingViewController: UIViewController {
- private let initialSetting: ReaderDisplaySetting
- private let completion: ((ReaderDisplaySetting) -> Void)?
+ private let initialSetting: ReaderDisplaySettings
+ private let completion: ((ReaderDisplaySettings) -> Void)?
private let trackingSource: ReaderDisplaySettingViewSource
private var viewModel: ReaderDisplaySettingSelectionViewModel? = nil
@@ -18,9 +19,9 @@ class ReaderDisplaySettingViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
- init(initialSetting: ReaderDisplaySetting,
+ init(initialSetting: ReaderDisplaySettings,
source: ReaderDisplaySettingViewSource = .unspecified,
- completion: ((ReaderDisplaySetting) -> Void)?) {
+ completion: ((ReaderDisplaySettings) -> Void)?) {
self.initialSetting = initialSetting
self.completion = completion
self.trackingSource = source
@@ -83,7 +84,7 @@ class ReaderDisplaySettingViewController: UIViewController {
}
@MainActor
- private func updateNavigationBar(with setting: ReaderDisplaySetting) {
+ private func updateNavigationBar(with setting: ReaderDisplaySettings) {
navigationController?.navigationBar.overrideUserInterfaceStyle = setting.hasLightBackground ? .light : .dark
// update the experimental label style
@@ -118,14 +119,14 @@ class ReaderDisplaySettingViewController: UIViewController {
class ReaderDisplaySettingSelectionViewModel: NSObject, ObservableObject {
private typealias TrackingKeys = ReaderDisplaySettingSelectionView.TrackingKeys
- @Published var displaySetting: ReaderDisplaySetting
+ @Published var displaySetting: ReaderDisplaySettings
/// Called when the user selects a new option.
var didSelectItem: (() -> Void)? = nil
- private let completion: ((ReaderDisplaySetting) -> Void)?
+ private let completion: ((ReaderDisplaySettings) -> Void)?
- init(displaySetting: ReaderDisplaySetting, completion: ((ReaderDisplaySetting) -> Void)?) {
+ init(displaySetting: ReaderDisplaySettings, completion: ((ReaderDisplaySettings) -> Void)?) {
self.displaySetting = displaySetting
self.completion = completion
}
@@ -338,7 +339,7 @@ extension ReaderDisplaySettingSelectionView {
var colorSelectionView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: .DS.Padding.half) {
- ForEach(ReaderDisplaySetting.Color.allCases, id: \.rawValue) { color in
+ ForEach(ReaderDisplaySettings.Color.allCases, id: \.rawValue) { color in
Button {
viewModel.displaySetting.color = color
viewModel.didSelectItem?() // notify the view controller to update.
@@ -374,7 +375,7 @@ extension ReaderDisplaySettingSelectionView {
var fontSelectionView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: .DS.Padding.half) {
- ForEach(ReaderDisplaySetting.Font.allCases, id: \.rawValue) { font in
+ ForEach(ReaderDisplaySettings.Font.allCases, id: \.rawValue) { font in
Button {
viewModel.displaySetting.font = font
viewModel.didSelectItem?() // notify the view controller to update.
@@ -384,7 +385,7 @@ extension ReaderDisplaySettingSelectionView {
} label: {
VStack(spacing: .DS.Padding.half) {
Text("Aa")
- .font(Font(ReaderDisplaySetting.font(with: font, textStyle: .largeTitle)).bold())
+ .font(Font(ReaderDisplaySettings.font(with: font, textStyle: .largeTitle)).bold())
.foregroundStyle(Color(.label))
Text(font.rawValue.capitalized)
.font(.footnote)
@@ -409,19 +410,19 @@ extension ReaderDisplaySettingSelectionView {
var sizeSelectionView: some View {
Slider(value: $sliderValue,
- in: Double(ReaderDisplaySetting.Size.extraSmall.rawValue)...Double(ReaderDisplaySetting.Size.extraLarge.rawValue),
+ in: Double(ReaderDisplaySettings.Size.extraSmall.rawValue)...Double(ReaderDisplaySettings.Size.extraLarge.rawValue),
step: 1) {
Text(Strings.sizeSliderLabel)
} minimumValueLabel: {
Text("A")
- .font(Font(ReaderDisplaySetting.font(with: .sans, size: .extraSmall, textStyle: .body)))
+ .font(Font(ReaderDisplaySettings.font(with: .sans, size: .extraSmall, textStyle: .body)))
.accessibilityHidden(true)
} maximumValueLabel: {
Text("A")
- .font(Font(ReaderDisplaySetting.font(with: .sans, size: .extraLarge, textStyle: .body)))
+ .font(Font(ReaderDisplaySettings.font(with: .sans, size: .extraLarge, textStyle: .body)))
.accessibilityHidden(true)
} onEditingChanged: { _ in
- let size = ReaderDisplaySetting.Size(rawValue: Int(sliderValue)) ?? .normal
+ let size = ReaderDisplaySettings.Size(rawValue: Int(sliderValue)) ?? .normal
viewModel.displaySetting.size = size
viewModel.didSelectItem?() // notify the view controller to update.
WPAnalytics.track(.readingPreferencesItemTapped,
diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj
index 228f44b17036..66493c618a50 100644
--- a/WordPress/WordPress.xcodeproj/project.pbxproj
+++ b/WordPress/WordPress.xcodeproj/project.pbxproj
@@ -1529,10 +1529,6 @@
FD3D6D2C1349F5D30061136A /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD3D6D2B1349F5D30061136A /* ImageIO.framework */; };
FE003F62282E73E6006F8D1D /* blogging-prompts-fetch-success.json in Resources */ = {isa = PBXBuildFile; fileRef = FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */; };
FE1E201E2A49D59400CE7C90 /* JetpackSocialServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E201D2A49D59400CE7C90 /* JetpackSocialServiceTests.swift */; };
- FE23EB4926E7C91F005A1698 /* richCommentTemplate.html in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */; };
- FE23EB4A26E7C91F005A1698 /* richCommentTemplate.html in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */; };
- FE23EB4B26E7C91F005A1698 /* richCommentStyle.css in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4826E7C91F005A1698 /* richCommentStyle.css */; };
- FE23EB4C26E7C91F005A1698 /* richCommentStyle.css in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4826E7C91F005A1698 /* richCommentStyle.css */; };
FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */; };
FE320CC5294705990046899B /* ReaderPostBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE320CC4294705990046899B /* ReaderPostBackupTests.swift */; };
FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */; };
@@ -3312,8 +3308,6 @@
FDCB9A89134B75B900E5C776 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blogging-prompts-fetch-success.json"; sourceTree = ""; };
FE1E201D2A49D59400CE7C90 /* JetpackSocialServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSocialServiceTests.swift; sourceTree = ""; };
- FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = richCommentTemplate.html; path = Resources/HTML/richCommentTemplate.html; sourceTree = ""; };
- FE23EB4826E7C91F005A1698 /* richCommentStyle.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = richCommentStyle.css; path = Resources/HTML/richCommentStyle.css; sourceTree = ""; };
FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsServiceTests.swift; sourceTree = ""; };
FE320CC4294705990046899B /* ReaderPostBackupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderPostBackupTests.swift; sourceTree = ""; };
FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinatorTests.swift; sourceTree = ""; };
@@ -3388,7 +3382,6 @@
Utility/SFHFKeychainUtils.m = "-fno-objc-arc";
};
membershipExceptions = (
- "Extensions/Colors and Styles/UIColor+Extensions.swift",
"Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift",
"Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift",
"Extensions/NSAttributedStringKey+Conversion.swift",
@@ -3427,7 +3420,6 @@
Utility/SFHFKeychainUtils.m = "-fno-objc-arc";
};
membershipExceptions = (
- "Extensions/Colors and Styles/UIColor+Extensions.swift",
"Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift",
"Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift",
"Extensions/NSAttributedStringKey+Conversion.swift",
@@ -3466,7 +3458,6 @@
Utility/SFHFKeychainUtils.m = "-fno-objc-arc";
};
membershipExceptions = (
- "Extensions/Colors and Styles/UIColor+Extensions.swift",
Services/KeyValueDatabase.swift,
System/Constants/Constants.m,
"Utility/App Configuration/AppConfiguration.swift",
@@ -3559,7 +3550,6 @@
Utility/SFHFKeychainUtils.m = "-fno-objc-arc";
};
membershipExceptions = (
- "Extensions/Colors and Styles/UIColor+Extensions.swift",
"Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift",
"Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift",
"Extensions/NSAttributedStringKey+Conversion.swift",
@@ -3596,7 +3586,6 @@
Utility/SFHFKeychainUtils.m = "-fno-objc-arc";
};
membershipExceptions = (
- "Extensions/Colors and Styles/UIColor+Extensions.swift",
"Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift",
"Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift",
"Extensions/NSAttributedStringKey+Conversion.swift",
@@ -6724,8 +6713,6 @@
2FAE97080E33B21600CA8540 /* xhtmlValidatorTemplate.xhtml */,
E61507E12220A0FE00213D33 /* richEmbedTemplate.html */,
E61507E32220A13B00213D33 /* richEmbedScript.js */,
- FE23EB4826E7C91F005A1698 /* richCommentStyle.css */,
- FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */,
);
name = HTML;
sourceTree = "";
@@ -8400,7 +8387,6 @@
17222D81261DDDF90047B163 /* celadon-classic-icon-app-76x76@2x.png in Resources */,
931DF4D618D09A2F00540BDD /* InfoPlist.strings in Resources */,
801D9511291AB3CF0051993E /* JetpackStatsLogoAnimation_ltr.json in Resources */,
- FE23EB4926E7C91F005A1698 /* richCommentTemplate.html in Resources */,
1761F18826209AEE000815EF /* pride-icon-app-76x76.png in Resources */,
17222D8A261DDDF90047B163 /* black-classic-icon-app-76x76@2x.png in Resources */,
1761F17F26209AEE000815EF /* open-source-icon-app-76x76@2x.png in Resources */,
@@ -8475,7 +8461,6 @@
4A690C152BA791B100A8E0C5 /* PrivacyInfo.xcprivacy in Resources */,
1761F17526209AEE000815EF /* open-source-dark-icon-app-76x76@2x.png in Resources */,
1761F18A26209AEE000815EF /* hot-pink-icon-app-60x60@2x.png in Resources */,
- FE23EB4B26E7C91F005A1698 /* richCommentStyle.css in Resources */,
1761F18526209AEE000815EF /* pride-icon-app-60x60@3x.png in Resources */,
0CD542CA2CEFAE5F00666F44 /* wporg-blog-icon.png in Resources */,
17222DB0261DDDF90047B163 /* spectrum-classic-icon-app-83.5x83.5@2x.png in Resources */,
@@ -8764,7 +8749,6 @@
F465980828E66A5B00D5F49A /* white-on-blue-icon-app-76@2x.png in Resources */,
F46597BE28E6687800D5F49A /* neumorphic-dark-icon-app-60@3x.png in Resources */,
F41E4ECB28F23E00001880C6 /* green-on-white-icon-app-60@3x.png in Resources */,
- FE23EB4C26E7C91F005A1698 /* richCommentStyle.css in Resources */,
F41E4EE428F24623001880C6 /* 3d-icon-app-60@2x.png in Resources */,
F46597F328E669D400D5F49A /* spectrum-on-white-icon-app-83.5@2x.png in Resources */,
F41E4EEF28F247D3001880C6 /* white-on-green-icon-app-60@2x.png in Resources */,
@@ -8814,7 +8798,6 @@
FABB203D2602FC2C00C8785C /* Localizable.strings in Resources */,
FABB28472603067C00C8785C /* Launch Screen.storyboard in Resources */,
F465978F28E65F8A00D5F49A /* celadon-on-white-icon-app-60@2x.png in Resources */,
- FE23EB4A26E7C91F005A1698 /* richCommentTemplate.html in Resources */,
F41E4EAA28F20DF9001880C6 /* stroke-light-icon-app-76.png in Resources */,
F46597FE28E66A1100D5F49A /* white-on-black-icon-app-76.png in Resources */,
F41E4ED628F2424B001880C6 /* dark-glow-icon-app-83.5@2x.png in Resources */,