Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reader: Add announcement card #23197

Merged
merged 13 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum RemoteFeatureFlag: Int, CaseIterable {
case siteSwitcherRedesign
case readingPreferences
case readingPreferencesFeedback
case readerAnnouncementCard

var defaultValue: Bool {
switch self {
Expand Down Expand Up @@ -92,6 +93,8 @@ enum RemoteFeatureFlag: Int, CaseIterable {
return true
case .readingPreferencesFeedback:
return true
case .readerAnnouncementCard:
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest]
}
}

Expand Down Expand Up @@ -156,6 +159,8 @@ enum RemoteFeatureFlag: Int, CaseIterable {
return "reading_preferences"
case .readingPreferencesFeedback:
return "reading_preferences_feedback"
case .readerAnnouncementCard:
return "reader_announcement_card"
}
}

Expand Down Expand Up @@ -219,6 +224,8 @@ enum RemoteFeatureFlag: Int, CaseIterable {
return "Reading Preferences"
case .readingPreferencesFeedback:
return "Reading Preferences Feedback"
case .readerAnnouncementCard:
return "Reader Announcement Card"
}
}

Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this file needs to be added to the WordPress target.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I've added it in e2a312a.

Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import SwiftUI
import DesignSystem

class ReaderAnnouncementHeaderView: UITableViewHeaderFooterView, ReaderStreamHeader {

weak var delegate: ReaderStreamHeaderDelegate?

private let header: ReaderAnnouncementHeader

init(doneButtonTapped: (() -> Void)? = nil) {
self.header = ReaderAnnouncementHeader { [doneButtonTapped] in
doneButtonTapped?()
}

super.init(reuseIdentifier: ReaderSiteHeaderView.classNameWithoutNamespaces())
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
let view = UIView.embedSwiftUIView(self.header)
addSubview(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: topAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
view.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor)
])

applyBackgroundColor(Constants.backgroundColor)
addBottomBorder(withColor: .separator)
}

private func applyBackgroundColor(_ color: UIColor) {
let backgroundView = UIView(frame: bounds)
backgroundView.backgroundColor = color
self.backgroundView = backgroundView
}

// MARK: ReaderStreamHeader

func enableLoggedInFeatures(_ enable: Bool) {
// no-op
}

func configureHeader(_ topic: ReaderAbstractTopic) {
// no-op; this header doesn't rely on the supplied topic.
}

fileprivate struct Constants {
static let backgroundColor = UIColor.systemBackground
}
}

// TODO: ReaderAnnouncementItem / Models

// MARK: - SwiftUI View

fileprivate struct ReaderAnnouncementHeader: View {

// Determines what features should be listed (and its order).
let entries: [Entry] = [.tagsStream, .readingPreferences]

var onButtonTap: (() -> Void)? = nil

var body: some View {
VStack(alignment: .leading, spacing: .DS.Padding.double) {
Text(Strings.title)
.font(.callout)
.fontWeight(.semibold)

ForEach(entries, id: \.title) { entry in
announcementEntryView(entry)
}

DSButton(title: Strings.buttonTitle,
style: DSButtonStyle(emphasis: .primary, size: .large)) {
onButtonTap?()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.vertical, .DS.Padding.medium)
.background(Color(ReaderAnnouncementHeaderView.Constants.backgroundColor))
}

// MARK: Constants

private struct Strings {
static let title = NSLocalizedString(
"reader.announcement.title",
value: "New in Reader",
comment: "Title text for the announcement card component in the Reader."
)
static let buttonTitle = NSLocalizedString(
"reader.announcement.button",
value: "Done",
comment: "Text for a button that dismisses the announcement card in the Reader."
)
}
}

// MARK: - Announcement Item

fileprivate extension ReaderAnnouncementHeader {

struct Entry {
static let tagsStream = Entry(
imageName: "reader-menu-tags",
title: NSLocalizedString(
"reader.announcement.entry.tagsStream.title",
value: "Tags Stream",
comment: "The title part of the feature announcement content for Tags Stream."
),
description: NSLocalizedString(
"reader.announcement.entry.tagsStream.description",
value: "Tap the dropdown at the top and select Tags to access streams from your followed tags.",
comment: "The description part of the feature announcement content for Tags Stream."
)
)

static let readingPreferences = Entry(
imageName: "reader-reading-preferences",
title: NSLocalizedString(
"reader.announcement.entry.readingPreferences.title",
value: "Reading Preferences",
comment: "The title part of the feature announcement content for Reading Preferences."
),
description: NSLocalizedString(
"reader.announcement.entry.readingPreferences.description",
value: "Choose colors and fonts that suit you. When you’re reading a post tap the AA icon at the top of the screen.",
comment: "The description part of the feature announcement content for Reading Preferences."
)
)

let imageName: String
let title: String
let description: String
}

@ViewBuilder
func announcementEntryView(_ entry: Entry) -> some View {
HStack(spacing: .DS.Padding.double) {
Image(entry.imageName, bundle: nil)
.renderingMode(.template)
.resizable()
.frame(width: 24, height: 24)
.padding(12)
.foregroundColor(Color(.systemBackground))
.background(.primary)
.clipShape(Circle())
.accessibilityHidden(true)

VStack(alignment: .leading, spacing: 1) {
Text(entry.title)
.font(.callout)
.fontWeight(.semibold)
Text(entry.description)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
}

// MARK: - Reader Announcement Coordinator

class ReaderAnnouncementCoordinator {

let repository: UserPersistentRepository = UserPersistentStoreFactory.instance()

lazy var canShowAnnouncement: Bool = {
return !isDismissed && RemoteFeatureFlag.readerAnnouncementCard.enabled()
}()

var isDismissed: Bool {
get {
repository.bool(forKey: Constants.key)
}
set {
repository.set(newValue, forKey: Constants.key)
}
}

private struct Constants {
static let key = "readerAnnouncementCardDismissedKey"
}
}
27 changes: 23 additions & 4 deletions WordPress/Classes/ViewRelated/Reader/ReaderSiteHeaderView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

class ReaderSiteHeaderView: UIView, ReaderStreamHeader {
class ReaderSiteHeaderView: UITableViewHeaderFooterView, ReaderStreamHeader {

weak var delegate: ReaderStreamHeaderDelegate?

Expand All @@ -14,8 +14,8 @@ class ReaderSiteHeaderView: UIView, ReaderStreamHeader {
}()

init() {
super.init(frame: .zero)
backgroundColor = .secondarySystemGroupedBackground
super.init(reuseIdentifier: ReaderSiteHeaderView.classNameWithoutNamespaces())
applyBackgroundColor(.secondarySystemGroupedBackground)
setupHeader()
}

Expand Down Expand Up @@ -46,9 +46,28 @@ class ReaderSiteHeaderView: UIView, ReaderStreamHeader {
let header = ReaderSiteHeader(viewModel: weakSelf?.headerViewModel ?? ReaderSiteHeaderViewModel())
let view = UIView.embedSwiftUIView(header)
addSubview(view)
pinSubviewToAllEdges(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: topAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
view.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor)
])

addBottomBorder(withColor: .separator)
}

/// Sets the background color of the container view.
///
/// For `UITableViewHeaderFooterView`, we'll need to assign a `UIView` with the desired
/// background color to the `backgroundView` property. Setting the `backgroundColor`
/// directly will pop a warning in the console.
///
/// - Parameter color: The background color
private func applyBackgroundColor(_ color: UIColor) {
let backgroundView = UIView(frame: bounds)
backgroundView.backgroundColor = color
self.backgroundView = backgroundView
}
}

// MARK: - ReaderSiteHeader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ extension ReaderStreamViewController {
///
/// - Returns: A configured instance of UIView.
///
func headerForStream(_ topic: ReaderAbstractTopic, isLoggedIn: Bool, container: UITableViewController) -> UIView? {
func headerForStream(_ topic: ReaderAbstractTopic?, isLoggedIn: Bool, container: UITableViewController) -> UIView? {
if let topic,
let header = headerForStream(topic) {
configure(header, topic: topic, isLoggedIn: isLoggedIn, delegate: self)
return header
}

let header = headerForStream(topic)
configure(header, topic: topic, isLoggedIn: isLoggedIn, delegate: self)
return header
// The announcement header should have the lowest display priority.
// Only return the announcement when there's no other header.
return makeAnnouncementHeader()
}

func configure(_ header: ReaderHeader?, topic: ReaderAbstractTopic, isLoggedIn: Bool, delegate: ReaderStreamHeaderDelegate) {
Expand Down Expand Up @@ -154,6 +159,50 @@ extension ReaderStreamViewController {
}
}

// MARK: - Reader Announcement Header

extension ReaderStreamViewController {
/// Returns a header view for Reader-related announcements.
/// Note that the announcement can also be shown on topicless streams (e.g., Saved, Tags).
///
/// - Returns: A configured UIView, or nil if the conditions are not met.
func makeAnnouncementHeader() -> UIView? {
guard readerAnnouncementCoordinator.canShowAnnouncement,
tableView.tableHeaderView == nil,
!isContentFiltered,
!contentIsEmpty() else {
return nil
}

return ReaderAnnouncementHeaderView(doneButtonTapped: { [weak self] in
// Set the card as dismissed.
self?.readerAnnouncementCoordinator.isDismissed = true

// Animate the header removal so it feels less jarring.
UIView.animate(withDuration: 0.3) {
self?.tableView.tableHeaderView?.layer.opacity = 0.0
} completion: { _ in
self?.tableView.performBatchUpdates({
self?.tableView.tableHeaderView = nil
})
}
})
}

// The header may be configured when the content is still empty (i.e., Discover stream).
// This method is added to provide a way to inject the announcement card outside of
// `configureStreamHeader()`. For example, after syncing completes.
func showAnnouncementHeaderIfNeeded(completion: (() -> Void)? = nil) {
guard let headerView = makeAnnouncementHeader() else {
return
}

tableView.tableHeaderView = headerView
completion?()
}

}

// MARK: - Undo cell for saved posts
extension ReaderStreamViewController {

Expand Down
Loading