Skip to content

Commit

Permalink
Stats: Subscribers List Card (#23067)
Browse files Browse the repository at this point in the history
  • Loading branch information
staskus authored Apr 25, 2024
2 parents f045b25 + 579498e commit 1377cb4
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 21 deletions.
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: '14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098'
# 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 `14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098`)
- 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: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098
: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: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098
: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: 4ac1d35f8415bdc8d4c8e39d6aa7a9f6dab5933d

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 @@ -457,7 +461,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.subscriberSince", value: "Subscriber since", comment: "Table column title that shows the date since the user became a subscriber.")
static let clicks = NSLocalizedString("Clicks", comment: "Label for number of clicks.")
static let downloads = NSLocalizedString("Downloads", comment: "Label for number of file downloads.")
static let emailsSummaryOpens = NSLocalizedString("stats.subscribers.emailsSummary.column.opens", value: "Opens", comment: "A title for table's column that shows a number of email openings")
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 @@ -87,3 +90,37 @@ 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(),
userIconURL: $0.avatarURL,
statSection: .subscribersList
)
}
}
}
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
}
}

0 comments on commit 1377cb4

Please sign in to comment.