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

Stats: Subscribers List Card #23067

Merged
merged 13 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ end

def wordpress_kit
# pod 'WordPressKit', '~> 17.0.0'
pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '1a0955f6cbc20b258f82be7887d6e8e56a6fbce3'
pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '3291ab9b60e88b1bc78f50422efd3b38c30c0eb3'
# pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: ''
# pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: ''
# pod 'WordPressKit', path: '../WordPressKit-iOS'
Expand Down
8 changes: 4 additions & 4 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ DEPENDENCIES:
- SwiftLint (= 0.54.0)
- WordPress-Editor-iOS (~> 1.19.11)
- WordPressAuthenticator (>= 9.0.8, ~> 9.0)
- WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `1a0955f6cbc20b258f82be7887d6e8e56a6fbce3`)
- WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `3291ab9b60e88b1bc78f50422efd3b38c30c0eb3`)
- WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`)
- WordPressUI (~> 1.16)
- ZendeskSupportSDK (= 5.3.0)
Expand Down Expand Up @@ -177,7 +177,7 @@ EXTERNAL SOURCES:
Gutenberg:
:podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec
WordPressKit:
:commit: 1a0955f6cbc20b258f82be7887d6e8e56a6fbce3
:commit: 3291ab9b60e88b1bc78f50422efd3b38c30c0eb3
:git: https://github.com/wordpress-mobile/WordPressKit-iOS.git
WordPressShared:
:commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0
Expand All @@ -188,7 +188,7 @@ CHECKOUT OPTIONS:
:git: https://github.com/wordpress-mobile/FSInteractiveMap.git
:tag: 0.2.0
WordPressKit:
:commit: 1a0955f6cbc20b258f82be7887d6e8e56a6fbce3
:commit: 3291ab9b60e88b1bc78f50422efd3b38c30c0eb3
:git: https://github.com/wordpress-mobile/WordPressKit-iOS.git
WordPressShared:
:commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0
Expand Down Expand Up @@ -239,6 +239,6 @@ SPEC CHECKSUMS:
ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba
ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced

PODFILE CHECKSUM: 2c841e7c9f39bc169a4b75accb5076d2b31e5468
PODFILE CHECKSUM: d5e8587901ad6400bb90366661ff541a6f27f08a

COCOAPODS: 1.15.2
14 changes: 9 additions & 5 deletions WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
case postStatsAverageViews
case postStatsRecentWeeks
case subscribersEmailsSummary
case subscribersList

static let allInsights: [StatSection] = [
.insightsViewsVisitors,
Expand Down Expand Up @@ -108,7 +109,7 @@
return InsightsHeaders.comments
}
case .insightsFollowersWordPress, .insightsFollowersEmail:
return InsightsHeaders.followers
return InsightsHeaders.subscribers
case .insightsTodaysStats:
return InsightsHeaders.todaysStats
case .insightsPostingActivity:
Expand Down Expand Up @@ -145,6 +146,8 @@
return PostStatsHeaders.recentWeeks
case .subscribersEmailsSummary:
return SubscribersHeaders.emailsSummaryStats
case .subscribersList:
return SubscribersHeaders.subscribersList
default:
return ""
}
Expand All @@ -164,7 +167,7 @@
return ItemSubtitles.service
case .insightsFollowersWordPress,
.insightsFollowersEmail:
return ItemSubtitles.follower
return ItemSubtitles.subscriber
case .periodReferrers:
return ItemSubtitles.referrer
case .periodClicks:
Expand Down Expand Up @@ -402,7 +405,7 @@
static let posts = NSLocalizedString("Posts", comment: "Insights 'Posts' header")
static let comments = NSLocalizedString("Comments", comment: "Insights 'Comments' header")
static let topCommenters = NSLocalizedString("Top Commenters", comment: "Insights 'Top Commenters' header")
static let followers = NSLocalizedString("Followers", comment: "Insights 'Followers' header")
static let subscribers = NSLocalizedString("stats.insights.subscribers.title", value: "Subscribers", comment: "Insights 'Subscribers' header")
static let tagsAndCategories = NSLocalizedString("Tags and Categories", comment: "Insights 'Tags and Categories' header")
static let annualSiteStats = NSLocalizedString("This Year", comment: "Insights 'This Year' header")
static let addCard = NSLocalizedString("Add stats card", comment: "Label for action to add a new Insight.")
Expand Down Expand Up @@ -431,6 +434,7 @@

struct SubscribersHeaders {
static let emailsSummaryStats = NSLocalizedString("stats.subscribers.emailsSummaryCard.title", value: "Emails", comment: "Stats 'Emails' card header")
static let subscribersList = NSLocalizedString("stats.subscribers.subscribersListCard.title", value: "Subscribers", comment: "Stats 'Subscribers' card header")
}

struct PostStatsHeaders {
Expand All @@ -443,7 +447,7 @@
static let author = NSLocalizedString("Author", comment: "Label for list of stats by content author.")
static let title = NSLocalizedString("Title", comment: "Label for list of stats by content title.")
static let service = NSLocalizedString("Service", comment: "Label for connected service in Publicize stat.")
static let follower = NSLocalizedString("Follower", comment: "Label for list of followers.")
static let subscriber = NSLocalizedString("stats.section.itemSubtitles.subscriber", value: "Name", comment: "Table column title that shows the names of subscribers.")
static let referrer = NSLocalizedString("Referrer", comment: "Label for link title in Referrers stat.")
static let link = NSLocalizedString("Link", comment: "Label for link title in Clicks stat.")
static let country = NSLocalizedString("Country", comment: "Label for list of countries.")
Expand All @@ -456,7 +460,7 @@
static let comments = NSLocalizedString("Comments", comment: "Label for number of comments.")
static let views = NSLocalizedString("Views", comment: "Label for number of views.")
static let followers = NSLocalizedString("Followers", comment: "Label for number of followers.")
static let since = NSLocalizedString("Since", comment: "Label for time period in list of followers.")
static let since = NSLocalizedString("stats.section.dataSubtitles.subscriber since", value: "Subscriber since", comment: "Table column title that shows the date since the user became a subscriber.")
staskus marked this conversation as resolved.
Show resolved Hide resolved
static let clicks = NSLocalizedString("Clicks", comment: "Label for number of clicks.")
static let downloads = NSLocalizedString("Downloads", comment: "Label for number of file downloads.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ final class StatsSubscribersCache {
static func emailsSummary(quantity: Int, sortField: String, sortOrder: String, siteId: NSNumber) -> CacheKey {
return .init(record: .subscribersEmailsSummary, key: "\(quantity) \(sortField) \(sortOrder)", siteID: siteId)
}

static func subscribersList(quantity: Int, siteId: NSNumber) -> CacheKey {
return .init(record: .subscribersList, key: "\(quantity)", siteID: siteId)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import WordPressKit

protocol StatsSubscribersStoreProtocol {
var emailsSummary: CurrentValueSubject<StatsSubscribersStore.State<StatsEmailsSummaryData>, Never> { get }
var subscribersList: CurrentValueSubject<StatsSubscribersStore.State<[StatsFollower]>, Never> { get }

func updateEmailsSummary(quantity: Int, sortField: StatsEmailsSummaryData.SortField)
func updateSubscribersList(quantity: Int)
}

struct StatsSubscribersStore: StatsSubscribersStoreProtocol {
Expand All @@ -13,6 +16,7 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol {
private let statsService: StatsServiceRemoteV2

var emailsSummary: CurrentValueSubject<State<StatsEmailsSummaryData>, Never> = .init(.idle)
var subscribersList: CurrentValueSubject<State<[StatsFollower]>, Never> = .init(.idle)

init() {
self.siteID = SiteStatsInformation.sharedInstance.siteID ?? 0
Expand All @@ -21,6 +25,8 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol {
statsService = StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: siteID.intValue, siteTimezone: timeZone)
}

// MARK: - Emails Summary

func updateEmailsSummary(quantity: Int, sortField: StatsEmailsSummaryData.SortField) {
guard emailsSummary.value != .loading else { return }

Expand Down Expand Up @@ -48,6 +54,67 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol {
}
}
}

// MARK: - Subscribers List

func updateSubscribersList(quantity: Int) {
let cacheKey = StatsSubscribersCache.CacheKey.subscribersList(quantity: quantity, siteId: siteID)
let cachedData: [StatsFollower]? = cache.getValue(key: cacheKey)

if let cachedData = cachedData {
self.subscribersList.send(.success(cachedData))
} else {
subscribersList.send(.loading)
}

getSubscribers(quantity: quantity) { result in
DispatchQueue.main.async {
switch result {
case .success(let data):
cache.setValue(data, key: cacheKey)
self.subscribersList.send(.success(data))
case .failure:
if cachedData == nil {
self.subscribersList.send(.error)
}
}
}
}
}

private func getSubscribers(quantity: Int, completion: @escaping (Result<[StatsFollower], Error>) -> Void) {
let group = DispatchGroup()
var followers: [StatsFollower] = []
var requestError: Error?

group.enter()
statsService.getInsight(limit: quantity) { (wpComFollowers: StatsDotComFollowersInsight?, error) in
followers.append(contentsOf: wpComFollowers?.topDotComFollowers ?? [])
requestError = error
group.leave()
}

group.enter()
statsService.getInsight(limit: quantity) { (wpComFollowers: StatsEmailFollowersInsight?, error) in
followers.append(contentsOf: wpComFollowers?.topEmailFollowers ?? [])
requestError = error
group.leave()
}

group.notify(queue: .main) {
if let error = requestError {
completion(.failure(error))
} else {
// Combine both wpcom and email subscribers into a single list
let followers = Array(
followers
.sorted(by: { $0.subscribedDate > $1.subscribedDate })
.prefix(quantity)
)
completion(.success(followers))
}
}
}
}

extension StatsSubscribersStore {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ final class StatsSubscribersViewModel {

func refreshData() {
store.updateEmailsSummary(quantity: 10, sortField: .postId)
store.updateSubscribersList(quantity: 10)
}

// MARK: - Lifecycle

func addObservers() {
Publishers.MergeMany(store.emailsSummary)
.removeDuplicates()
.sink { [weak self] _ in
self?.updateTableViewSnapshot()
}
.store(in: &cancellables)
Publishers.CombineLatest(
store.emailsSummary.removeDuplicates(),
store.subscribersList.removeDuplicates()
)
.sink { [weak self] _ in
self?.updateTableViewSnapshot()
}
.store(in: &cancellables)
}

func removeObservers() {
Expand All @@ -37,9 +40,9 @@ final class StatsSubscribersViewModel {

private extension StatsSubscribersViewModel {
func updateTableViewSnapshot() {
let snapshot = ImmuTableDiffableDataSourceSnapshot.multiSectionSnapshot(
emailsSummaryRows()
)
var snapshot = ImmuTableDiffableDataSourceSnapshot()
snapshot.addSection(subscribersListRows())
snapshot.addSection(emailsSummaryRows())
tableViewSnapshot.send(snapshot)
}

Expand Down Expand Up @@ -88,6 +91,40 @@ private extension StatsSubscribersViewModel {
}
}

// MARK: - Subscribers List

private extension StatsSubscribersViewModel {
func subscribersListRows() -> [any StatsHashableImmuTableRow] {
switch store.subscribersList.value {
case .loading, .idle:
return loadingRows(.subscribersList)
case .success(let subscribers):
return [
TopTotalsPeriodStatsRow(
itemSubtitle: StatSection.ItemSubtitles.subscriber,
dataSubtitle: StatSection.DataSubtitles.since,
dataRows: subscribersListDataRows(subscribers),
statSection: .subscribersList,
siteStatsPeriodDelegate: viewMoreDelegate
)
]
case .error:
return errorRows(.subscribersList)
}
}

func subscribersListDataRows(_ subscribers: [StatsFollower]) -> [StatsTotalRowData] {
return subscribers.map {
return StatsTotalRowData(
name: $0.name,
data: $0.subscribedDate.relativeStringInPast(),
guarani marked this conversation as resolved.
Show resolved Hide resolved
userIconURL: $0.avatarURL,
statSection: .subscribersList
)
}
}
}

private extension StatsSubscribersViewModel {
struct Strings {
static let titleColumn = NSLocalizedString("stats.subscribers.emailsSummary.column.title", value: "Latest emails", comment: "A title for table's column that shows a name of an email")
Expand Down
35 changes: 33 additions & 2 deletions WordPress/WordPressTest/StatsSubscribersViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,37 @@ final class StatsSubscribersViewModelTests: XCTestCase {
wait(for: [expectation], timeout: 1)
}

func testTableViewSnapshot_subscribersListLoaded() throws {
let expectation = expectation(description: "First section should be TopTotalsPeriodStatsRow")
var subscribersListRow: TopTotalsPeriodStatsRow?
sut.tableViewSnapshot
.sink(receiveValue: { snapshot in
if let row = snapshot.itemIdentifiers[0].immuTableRow as? TopTotalsPeriodStatsRow {
subscribersListRow = row
expectation.fulfill()
}
})
.store(in: &cancellables)

let subscribers: [StatsFollower] = [
.init(name: "First Subscriber", subscribedDate: Date(), avatarURL: nil),
.init(name: "Second Subscriber", subscribedDate: Date(), avatarURL: nil),
.init(name: "Third Subscriber", subscribedDate: Date(), avatarURL: nil)
]
store.subscribersList.send(.success(subscribers))

wait(for: [expectation], timeout: 1)
XCTAssertEqual(subscribersListRow?.dataRows.count, 3)
XCTAssertEqual(subscribersListRow?.dataRows[0].name, "First Subscriber")
XCTAssertEqual(subscribersListRow?.dataRows[2].name, "Third Subscriber")
}

func testTableViewSnapshot_emailsSummaryLoaded() throws {
let expectation = expectation(description: "First section should be loading")
let expectation = expectation(description: "First section should be TopTotalsPeriodStatsRow")
var emailsSummaryRow: TopTotalsPeriodStatsRow?
sut.tableViewSnapshot
.sink(receiveValue: { snapshot in
if let row = snapshot.itemIdentifiers.first?.immuTableRow as? TopTotalsPeriodStatsRow {
if let row = snapshot.itemIdentifiers[1].immuTableRow as? TopTotalsPeriodStatsRow {
emailsSummaryRow = row
expectation.fulfill()
}
Expand All @@ -57,9 +82,15 @@ final class StatsSubscribersViewModelTests: XCTestCase {

private class StatsSubscribersStoreMock: StatsSubscribersStoreProtocol {
var emailsSummary: CurrentValueSubject<StatsSubscribersStore.State<StatsEmailsSummaryData>, Never> = .init(.idle)
var subscribersList: CurrentValueSubject<StatsSubscribersStore.State<[StatsFollower]>, Never> = .init(.idle)
var updateEmailsSummaryCalled = false
var updateSubscribersListCalled = false

func updateEmailsSummary(quantity: Int, sortField: StatsEmailsSummaryData.SortField) {
updateEmailsSummaryCalled = true
}

func updateSubscribersList(quantity: Int) {
updateSubscribersListCalled = true
}
}