Skip to content

Commit 35ff495

Browse files
authored
CMM-1142 stats create todays card rust (#22502)
* Adding card and basic info * Add horuly granularity * Some styling * Following iOS distribution * Adding yesterday's data * Gradiente and style * Line fixes * detekt * Adding tests * trailing line * Extractign strings * Adding pull to refresh * Using rust library * Fixing date period * Updating version * Calling daily aggregates * Adding tests * detekt * Rename and tests * Using Long instead of Int for stats * Other PR suggestions * Using RS trunk version
1 parent 9040f15 commit 35ff495

File tree

7 files changed

+955
-209
lines changed

7 files changed

+955
-209
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package org.wordpress.android.ui.newstats.todaysstat
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.withContext
5+
import org.wordpress.android.fluxc.utils.AppLogWrapper
6+
import org.wordpress.android.modules.IO_THREAD
7+
import org.wordpress.android.networking.restapi.WpComApiClientProvider
8+
import org.wordpress.android.util.AppLog
9+
import rs.wordpress.api.kotlin.WpComApiClient
10+
import rs.wordpress.api.kotlin.WpRequestResult
11+
import uniffi.wp_api.StatsVisitsDataValue
12+
import uniffi.wp_api.StatsVisitsParams
13+
import uniffi.wp_api.StatsVisitsUnit
14+
import java.text.SimpleDateFormat
15+
import java.util.Calendar
16+
import java.util.Locale
17+
import javax.inject.Inject
18+
import javax.inject.Named
19+
20+
private const val HOURLY_QUANTITY = 24u
21+
private const val DAILY_QUANTITY = 1u
22+
23+
// Daily aggregates response field indexes
24+
// Response fields order: period, views, visitors, likes, reblogs, comments, posts
25+
@Suppress("unused") private const val INDEX_PERIOD = 0
26+
private const val INDEX_VIEWS = 1
27+
private const val INDEX_VISITORS = 2
28+
private const val INDEX_LIKES = 3
29+
@Suppress("unused") private const val INDEX_REBLOGS = 4
30+
private const val INDEX_COMMENTS = 5
31+
@Suppress("unused") private const val INDEX_POSTS = 6
32+
33+
/**
34+
* Repository for fetching stats data using the wordpress-rs API.
35+
* Handles hourly visits/views data for the Today's Stats card chart.
36+
*/
37+
class StatsRepository @Inject constructor(
38+
private val wpComApiClientProvider: WpComApiClientProvider,
39+
private val appLogWrapper: AppLogWrapper,
40+
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
41+
) {
42+
/**
43+
* Access token for API authentication.
44+
* Marked as @Volatile to ensure visibility across threads since this repository is accessed
45+
* from multiple coroutine contexts (main thread initialization, IO dispatcher for API calls).
46+
*/
47+
@Volatile
48+
private var accessToken: String? = null
49+
50+
private val wpComApiClient: WpComApiClient by lazy {
51+
check(accessToken != null) { "Repository not initialized" }
52+
wpComApiClientProvider.getWpComApiClient(accessToken!!)
53+
}
54+
55+
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
56+
57+
fun init(accessToken: String) {
58+
this.accessToken = accessToken
59+
}
60+
61+
/**
62+
* Fetches today's aggregated stats (views, visitors, likes, comments).
63+
*
64+
* @param siteId The WordPress.com site ID
65+
* @return Today's aggregated stats or error
66+
*/
67+
suspend fun fetchTodayAggregates(siteId: Long): TodayAggregatesResult = withContext(ioDispatcher) {
68+
if (accessToken == null) {
69+
appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized")
70+
return@withContext TodayAggregatesResult.Error("Repository not initialized")
71+
}
72+
73+
val calendar = Calendar.getInstance()
74+
val dateString = dateFormat.format(calendar.time)
75+
76+
val params = StatsVisitsParams(
77+
unit = StatsVisitsUnit.DAY,
78+
quantity = DAILY_QUANTITY,
79+
endDate = dateString,
80+
)
81+
82+
val result = wpComApiClient.request { requestBuilder ->
83+
requestBuilder.statsVisits().getStatsVisits(
84+
wpComSiteId = siteId.toULong(),
85+
params = params
86+
)
87+
}
88+
89+
when (result) {
90+
is WpRequestResult.Success -> {
91+
val response = result.response.data
92+
val row = response.data.firstOrNull()
93+
val aggregates = row?.let { parseDailyAggregates(it) }
94+
if (aggregates != null) {
95+
TodayAggregatesResult.Success(aggregates)
96+
} else {
97+
TodayAggregatesResult.Error("No data available")
98+
}
99+
}
100+
101+
is WpRequestResult.WpError -> {
102+
appLogWrapper.e(AppLog.T.STATS, "API Error fetching today aggregates: ${result.errorMessage}")
103+
TodayAggregatesResult.Error(result.errorMessage)
104+
}
105+
106+
else -> {
107+
appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching today aggregates")
108+
TodayAggregatesResult.Error("Unknown error")
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Fetches hourly views data for the specified date.
115+
*
116+
* @param siteId The WordPress.com site ID
117+
* @param offsetDays Number of days to offset from today (0 = today, 1 = yesterday, etc.)
118+
* @return List of hourly views data points, or empty list if fetch fails
119+
*/
120+
suspend fun fetchHourlyViews(
121+
siteId: Long,
122+
offsetDays: Int = 0
123+
): HourlyViewsResult = withContext(ioDispatcher) {
124+
if (accessToken == null) {
125+
appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized")
126+
return@withContext HourlyViewsResult.Error("Repository not initialized")
127+
}
128+
129+
val calendar = Calendar.getInstance()
130+
// The API's endDate is exclusive for hourly queries, so we need to add 1 day to get
131+
// the target day's hours. Formula: 1 (for exclusive end) - offsetDays (0=today, 1=yesterday)
132+
// Examples: offsetDays=0 → tomorrow's date → fetches today's hours
133+
// offsetDays=1 → today's date → fetches yesterday's hours
134+
calendar.add(Calendar.DAY_OF_YEAR, 1 - offsetDays)
135+
val dateString = dateFormat.format(calendar.time)
136+
137+
val params = StatsVisitsParams(
138+
unit = StatsVisitsUnit.HOUR,
139+
quantity = HOURLY_QUANTITY,
140+
endDate = dateString,
141+
)
142+
143+
val result = wpComApiClient.request { requestBuilder ->
144+
requestBuilder.statsVisits().getStatsVisits(
145+
wpComSiteId = siteId.toULong(),
146+
params = params
147+
)
148+
}
149+
150+
when (result) {
151+
is WpRequestResult.Success -> {
152+
val response = result.response.data
153+
val dataPoints = response.data.mapNotNull { row ->
154+
parseHourlyDataRow(row)
155+
}
156+
HourlyViewsResult.Success(dataPoints)
157+
}
158+
159+
is WpRequestResult.WpError -> {
160+
appLogWrapper.e(AppLog.T.STATS, "API Error fetching hourly views: ${result.errorMessage}")
161+
HourlyViewsResult.Error(result.errorMessage)
162+
}
163+
164+
else -> {
165+
appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching hourly views")
166+
HourlyViewsResult.Error("Unknown error")
167+
}
168+
}
169+
}
170+
171+
@Suppress("TooGenericExceptionCaught", "ReturnCount")
172+
private fun parseHourlyDataRow(row: Any?): HourlyViewsDataPoint? {
173+
return try {
174+
val rowList = row as? List<*> ?: return null
175+
val periodValue = rowList.getOrNull(0)
176+
val viewsValue = rowList.getOrNull(1)
177+
178+
// Extract values from wrapper types
179+
val period = when (periodValue) {
180+
is StatsVisitsDataValue.String -> periodValue.v1
181+
else -> return null
182+
}
183+
184+
val views = when (viewsValue) {
185+
is StatsVisitsDataValue.Number -> viewsValue.v1.toLong()
186+
else -> 0L
187+
}
188+
189+
HourlyViewsDataPoint(period = period, views = views)
190+
} catch (e: Exception) {
191+
appLogWrapper.w(AppLog.T.STATS, "Failed to parse stats row: ${e.message}")
192+
null
193+
}
194+
}
195+
196+
@Suppress("TooGenericExceptionCaught")
197+
private fun parseDailyAggregates(row: Any?): TodayAggregates? {
198+
return try {
199+
val rowList = row as? List<*> ?: return null
200+
val viewsValue = rowList.getOrNull(INDEX_VIEWS)
201+
val visitorsValue = rowList.getOrNull(INDEX_VISITORS)
202+
val likesValue = rowList.getOrNull(INDEX_LIKES)
203+
val commentsValue = rowList.getOrNull(INDEX_COMMENTS)
204+
205+
TodayAggregates(
206+
views = extractLongValue(viewsValue),
207+
visitors = extractLongValue(visitorsValue),
208+
likes = extractLongValue(likesValue),
209+
comments = extractLongValue(commentsValue)
210+
)
211+
} catch (e: Exception) {
212+
appLogWrapper.w(AppLog.T.STATS, "Failed to parse daily aggregates: ${e.message}")
213+
null
214+
}
215+
}
216+
217+
private fun extractLongValue(value: Any?): Long {
218+
return when (value) {
219+
is StatsVisitsDataValue.Number -> value.v1.toLong()
220+
else -> 0L
221+
}
222+
}
223+
}
224+
225+
/**
226+
* Result wrapper for hourly views fetch operation.
227+
*/
228+
sealed class HourlyViewsResult {
229+
data class Success(val dataPoints: List<HourlyViewsDataPoint>) : HourlyViewsResult()
230+
data class Error(val message: String) : HourlyViewsResult()
231+
}
232+
233+
/**
234+
* Raw data point from the stats API.
235+
*/
236+
data class HourlyViewsDataPoint(
237+
val period: String,
238+
val views: Long
239+
)
240+
241+
/**
242+
* Result wrapper for today's aggregated stats fetch operation.
243+
*/
244+
sealed class TodayAggregatesResult {
245+
data class Success(val aggregates: TodayAggregates) : TodayAggregatesResult()
246+
data class Error(val message: String) : TodayAggregatesResult()
247+
}
248+
249+
/**
250+
* Today's aggregated stats data.
251+
*/
252+
data class TodayAggregates(
253+
val views: Long,
254+
val visitors: Long,
255+
val likes: Long,
256+
val comments: Long
257+
)

WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,10 +376,10 @@ private fun StatsChart(chartData: ChartData) {
376376

377377
@Composable
378378
private fun MetricsRow(
379-
views: Int,
380-
visitors: Int,
381-
likes: Int,
382-
comments: Int
379+
views: Long,
380+
visitors: Long,
381+
likes: Long,
382+
comments: Long
383383
) {
384384
Row(
385385
modifier = Modifier.fillMaxWidth(),
@@ -457,7 +457,7 @@ private fun SecondaryMetricItem(
457457
}
458458
}
459459

460-
private fun formatStatValue(value: Int): String {
460+
private fun formatStatValue(value: Long): String {
461461
return when {
462462
value >= MILLION -> String.format(Locale.getDefault(), "%.1fM", value / MILLION.toDouble())
463463
value >= THOUSAND -> String.format(Locale.getDefault(), "%.1fK", value / THOUSAND.toDouble())

WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ sealed class TodaysStatsCardUiState {
77
data object Loading : TodaysStatsCardUiState()
88

99
data class Loaded(
10-
val views: Int,
11-
val visitors: Int,
12-
val likes: Int,
13-
val comments: Int,
10+
val views: Long,
11+
val visitors: Long,
12+
val likes: Long,
13+
val comments: Long,
1414
val chartData: ChartData,
1515
val onCardClick: () -> Unit
1616
) : TodaysStatsCardUiState()

0 commit comments

Comments
 (0)