Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
281746d
Add All-time Subscribers stats card for new stats Subscribers tab
adalpari Feb 26, 2026
1614285
Add Subscribers Graph placeholder card for stats Subscribers tab
adalpari Feb 26, 2026
663fa31
Add Subscribers List card for stats Subscribers tab
adalpari Feb 26, 2026
8f812c3
Add Emails card and wire Subscribers tab in new stats screen
adalpari Feb 26, 2026
6d2773a
Add unit tests for Subscribers tab cards
adalpari Feb 26, 2026
cc25b2b
Add tests for SubscribersTabViewModel and SubscribersGraphViewModel
adalpari Feb 26, 2026
a6814a3
Merge remote-tracking branch 'origin/trunk' into feat/CMM-1289-new-st…
adalpari Feb 27, 2026
0f522e8
Improve All-time Subscribers card UI and fix API calls
adalpari Feb 27, 2026
084badb
Improve Emails card alignment and formatting
adalpari Feb 27, 2026
63d6a36
Implement Subscribers Graph card with line chart and period tabs
adalpari Feb 27, 2026
362ac33
Add tests for Subscribers Graph and repository graph method
adalpari Feb 27, 2026
ef767a3
Update wordpress-rs dependency and fix subscriber date formatting
adalpari Feb 27, 2026
8189f9a
Address code review issues for Subscribers tab
adalpari Feb 27, 2026
f67d459
Add pagination to Subscribers and Emails detail pages
adalpari Feb 27, 2026
05e0989
Fix bugs, thread safety issues, and performance in Subscribers tab
adalpari Feb 27, 2026
09203d9
Address code review: fix critical bugs, races, and warnings
adalpari Mar 3, 2026
5b4802d
Fix detekt issues: refactor long methods, remove unused imports, add …
adalpari Mar 3, 2026
e75276d
Fix race condition, security alert, and add base ViewModel tests
adalpari Mar 3, 2026
0dfd7b2
Fix critical coroutine safety, unsafe casts, and pagination bugs
adalpari Mar 3, 2026
d11b4d5
Merge branch 'trunk' into feat/CMM-1289-new-stats-subscribers-tab
adalpari Mar 3, 2026
9e19b42
Simplify subscribers tab: extract shared composable, consolidate helpers
adalpari Mar 3, 2026
c39796a
Merge remote-tracking branch 'origin/trunk' into feat/CMM-1289-new-st…
adalpari Mar 5, 2026
2ae18c4
Update wordpress-rs to 3a23a7389b and adapt SortOrder rename
adalpari Mar 5, 2026
09f3b2e
Merge branch 'trunk' into feat/CMM-1289-new-stats-subscribers-tab
adalpari Mar 16, 2026
040885b
Updating worpdress-rs
adalpari Mar 16, 2026
53e6ff0
Merge remote-tracking branch 'origin/trunk' into feat/CMM-1289-new-st…
adalpari Mar 16, 2026
48dab2a
Merge branch 'trunk' into feat/CMM-1289-new-stats-subscribers-tab
adalpari Mar 17, 2026
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
10 changes: 10 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@
android:theme="@style/WordPress.NoActionBar"
android:exported="false" />

<activity
android:name=".ui.newstats.subscribers.subscriberslist.SubscribersListDetailActivity"
android:theme="@style/WordPress.NoActionBar"
android:exported="false" />

<activity
android:name=".ui.newstats.subscribers.emails.EmailsDetailActivity"
android:theme="@style/WordPress.NoActionBar"
android:exported="false" />

<!-- Account activities -->
<activity
android:name=".ui.main.MeActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import org.wordpress.android.ui.newstats.searchterms.SearchTermsViewModel
import org.wordpress.android.ui.newstats.videoplays.VideoPlaysViewModel
import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsCard
import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsViewModel
import org.wordpress.android.ui.newstats.subscribers.SubscribersTabContent
import android.widget.Toast
import org.wordpress.android.util.AppLog

Expand Down Expand Up @@ -164,39 +165,55 @@ private fun NewStatsScreen(
}
},
actions = {
Box {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { showPeriodMenu = true }
.padding(horizontal = 8.dp)
) {
Text(
text = selectedPeriod.getDisplayLabel(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface
)
Icon(
imageVector = Icons.Default.DateRange,
contentDescription = stringResource(
R.string.stats_period_selector_content_description
),
modifier = Modifier.padding(start = 4.dp)
val currentTab = tabs[pagerState.currentPage]
if (currentTab != StatsTab.SUBSCRIBERS) {
Box {
Row(
verticalAlignment =
Alignment.CenterVertically,
modifier = Modifier
.clickable {
showPeriodMenu = true
}
.padding(horizontal = 8.dp)
) {
Text(
text = selectedPeriod
.getDisplayLabel(),
style = MaterialTheme
.typography.labelLarge,
color = MaterialTheme
.colorScheme.onSurface
)
Icon(
imageVector =
Icons.Default.DateRange,
contentDescription =
stringResource(
R.string
.stats_period_selector_content_description
),
modifier = Modifier
.padding(start = 4.dp)
)
}
StatsPeriodMenu(
expanded = showPeriodMenu,
selectedPeriod = selectedPeriod,
onDismiss = {
showPeriodMenu = false
},
onPresetSelected = { period ->
viewsStatsViewModel
.onPeriodChanged(period)
showPeriodMenu = false
},
onCustomSelected = {
showPeriodMenu = false
showDateRangePicker = true
}
)
}
StatsPeriodMenu(
expanded = showPeriodMenu,
selectedPeriod = selectedPeriod,
onDismiss = { showPeriodMenu = false },
onPresetSelected = { period ->
viewsStatsViewModel.onPeriodChanged(period)
showPeriodMenu = false
},
onCustomSelected = {
showPeriodMenu = false
showDateRangePicker = true
}
)
}
}
)
Expand Down Expand Up @@ -245,6 +262,7 @@ private fun NewStatsScreen(
private fun StatsTabContent(tab: StatsTab, viewsStatsViewModel: ViewsStatsViewModel) {
when (tab) {
StatsTab.TRAFFIC -> TrafficTabContent(viewsStatsViewModel = viewsStatsViewModel)
StatsTab.SUBSCRIBERS -> SubscribersTabContent()
else -> PlaceholderTabContent(tab)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,48 @@ interface StatsDataSource {
dateRange: StatsDateRange,
max: Int = 10
): DevicesDataResult

/**
* Fetches subscriber count stats for a specific site.
*
* @param siteId The WordPress.com site ID
* @param quantity Number of data points to return
* @param unit Time unit: "day", "week", "month", "year"
* @param date Optional date in YYYY-MM-DD format
* @return Result containing subscriber count data
*/
suspend fun fetchStatsSubscribers(
siteId: Long,
quantity: Int = 1,
unit: String? = null,
date: String? = null
): StatsSubscribersDataResult

/**
* Fetches a list of subscribers by user type.
*
* @param siteId The WordPress.com site ID
* @param perPage Number of subscribers per page
* @param page Page number (1-based)
* @return Result containing subscriber items or an error
*/
suspend fun fetchSubscribersByUserType(
siteId: Long,
perPage: Int = 10,
page: Int = 1
): SubscribersByUserTypeDataResult

/**
* Fetches email stats summary for a specific site.
*
* @param siteId The WordPress.com site ID
* @param quantity Number of email items to return
* @return Result containing email summary items or an error
*/
suspend fun fetchStatsEmailsSummary(
siteId: Long,
quantity: Int = 10
): StatsEmailsSummaryDataResult
}

/**
Expand Down Expand Up @@ -529,3 +571,71 @@ sealed class DevicesDataResult {
* (percentage for screen size, view count for browser/platform).
*/
data class DevicesData(val items: Map<String, Double>)

/**
* Result wrapper for stats subscribers fetch operation.
*/
sealed class StatsSubscribersDataResult {
data class Success(
val data: StatsSubscribersData
) : StatsSubscribersDataResult()
data class Error(
val errorType: StatsErrorType
) : StatsSubscribersDataResult()
}

/**
* Stats subscribers data from the API.
*/
data class StatsSubscribersData(
val subscribersData: List<SubscribersDataPoint>
)

/**
* A single data point for subscriber count at a date.
*/
data class SubscribersDataPoint(
val date: String,
val count: Long
)

/**
* Result wrapper for subscribers by user type fetch operation.
*/
sealed class SubscribersByUserTypeDataResult {
data class Success(
val items: List<SubscriberItem>
) : SubscribersByUserTypeDataResult()
data class Error(
val errorType: StatsErrorType
) : SubscribersByUserTypeDataResult()
}

/**
* A single subscriber item.
*/
data class SubscriberItem(
val displayName: String,
val subscribedSince: String
)

/**
* Result wrapper for stats emails summary fetch operation.
*/
sealed class StatsEmailsSummaryDataResult {
data class Success(
val items: List<EmailSummaryItem>
) : StatsEmailsSummaryDataResult()
data class Error(
val errorType: StatsErrorType
) : StatsEmailsSummaryDataResult()
}

/**
* A single email summary item.
*/
data class EmailSummaryItem(
val title: String,
val opens: Long,
val clicks: Long
)
Loading