Skip to content
5 changes: 5 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@
android:theme="@style/WordPress.NoActionBar"
android:exported="false" />

<activity
android:name=".ui.newstats.poststats.PostStatsDetailActivity"
android:theme="@style/WordPress.NoActionBar"
android:exported="false" />

<activity
android:name=".ui.newstats.subscribers.subscriberslist.SubscribersListDetailActivity"
android:theme="@style/WordPress.NoActionBar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,8 @@ import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomain
import org.wordpress.android.ui.main.WPMainActivity
import org.wordpress.android.ui.media.MediaBrowserActivity
import org.wordpress.android.ui.media.MediaBrowserType
import org.wordpress.android.ui.mysite.SelectedSiteRepository
import org.wordpress.android.ui.mysite.menu.MenuActivity
import org.wordpress.android.ui.mysite.personalization.PersonalizationActivity
import org.wordpress.android.ui.stats.StatsConstants
import org.wordpress.android.ui.stats.refresh.lists.detail.StatsDetailActivity
import org.wordpress.android.ui.sitemonitor.SiteMonitorParentActivity
import org.wordpress.android.ui.sitemonitor.SiteMonitorType
import org.wordpress.android.util.AppLog
Expand All @@ -44,9 +41,7 @@ import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ActivityNavigator @Inject constructor(
private val selectedSiteRepository: SelectedSiteRepository
) {
class ActivityNavigator @Inject constructor() {
fun navigateToCampaignListingPage(context: Context, campaignListingPageSource: CampaignListingPageSource) {
context.startActivity(
Intent(context, BlazeCampaignParentActivity::class.java).apply {
Expand Down Expand Up @@ -277,26 +272,6 @@ class ActivityNavigator @Inject constructor(
.build()
}

/**
* Opens the per-post stats detail screen for a Posts & Pages item tapped in the new stats.
* Shows an error toast when no site is selected.
*/
fun openPostDetailStats(context: Context, postId: Long, postType: String?, postTitle: String, postUrl: String?) {
val site = selectedSiteRepository.getSelectedSite() ?: run {
ToastUtils.showToast(context, R.string.blog_not_found, ToastUtils.Duration.SHORT)
return
}
AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_POSTS_AND_PAGES_ITEM_TAPPED)
StatsDetailActivity.start(
context = context,
site = site,
postId = postId,
postType = statsDetailItemType(postType),
postTitle = postTitle,
postUrl = postUrl
)
}

fun navigateToApplicationPasswordReauthentication(activity: Activity, authenticationUrl: String) {
val intent = Intent(activity, ApplicationPasswordReauthenticateDialogActivity::class.java)
intent.putExtra(ApplicationPasswordDialogActivity.EXTRA_SITE_URL, authenticationUrl)
Expand All @@ -309,12 +284,3 @@ class ActivityNavigator @Inject constructor(
)
}
}

/**
* Maps the API post type of a Posts & Pages item to the item type expected by the post detail
* stats screen, mirroring the old stats mapping (pages and the homepage share one detail layout).
*/
internal fun statsDetailItemType(postType: String?): String = when (postType) {
StatsConstants.ITEM_TYPE_POST, StatsConstants.ITEM_TYPE_ATTACHMENT -> postType
else -> StatsConstants.ITEM_TYPE_HOME_PAGE
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ enum class InsightsCardType(
ALL_TIME_STATS(
R.string.stats_insights_all_time_stats_title
),
LATEST_POST(
R.string.stats_insights_latest_post_summary
),
MOST_POPULAR_DAY(
R.string.stats_insights_most_popular_day
),
Expand All @@ -27,6 +30,7 @@ enum class InsightsCardType(
listOf(
YEAR_IN_REVIEW,
ALL_TIME_STATS,
LATEST_POST,
MOST_POPULAR_DAY,
MOST_POPULAR_TIME,
TAGS_AND_CATEGORIES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,11 @@ class InsightsViewModel @Inject constructor(
}

companion object {
// TAGS_AND_CATEGORIES is intentionally absent
// from both checks: it has its own dedicated
// fetch path via StatsTagsUseCase in
// TagsAndCategoriesViewModel.
// TAGS_AND_CATEGORIES and LATEST_POST are
// intentionally absent from both checks: each
// fetches on its own, from
// TagsAndCategoriesViewModel and
// LatestPostViewModel respectively.
private fun List<InsightsCardType>.needsSummary():
Boolean = any {
it == InsightsCardType.ALL_TIME_STATS ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.PagePostCreationSourcesDetail
import org.wordpress.android.ui.ActivityNavigator
import org.wordpress.android.ui.compose.components.FeedbackDialog
import org.wordpress.android.ui.compose.theme.AppThemeM3
Expand Down Expand Up @@ -108,8 +109,11 @@ import org.wordpress.android.ui.newstats.subscribers.SubscribersTabContent
import android.widget.Toast
import org.wordpress.android.ui.newstats.alltimestats.AllTimeStatsCard
import org.wordpress.android.ui.newstats.alltimestats.AllTimeStatsViewModel
import org.wordpress.android.ui.newstats.latestpost.LatestPostCard
import org.wordpress.android.ui.newstats.latestpost.LatestPostViewModel
import org.wordpress.android.ui.newstats.mostpopularday.MostPopularDayCard
import org.wordpress.android.ui.newstats.mostpopularday.MostPopularDayViewModel
import org.wordpress.android.ui.newstats.poststats.PostStatsDetailActivity
import org.wordpress.android.ui.newstats.mostpopulartime.MostPopularTimeCard
import org.wordpress.android.ui.newstats.mostpopulartime.MostPopularTimeViewModel
import org.wordpress.android.ui.newstats.yearinreview.YearInReviewCard
Expand Down Expand Up @@ -189,7 +193,8 @@ class NewStatsActivity : BaseAppCompatActivity() {
onStatsUrlClick = { url ->
activityNavigator.openInCustomTab(this, url)
},
onPostItemClick = ::openPostDetailStats
onPostItemClick = ::openPostDetailStats,
onCreatePostClick = ::createNewPost
)
}
}
Expand All @@ -205,7 +210,21 @@ class NewStatsActivity : BaseAppCompatActivity() {
}

private fun openPostDetailStats(item: MostViewedItem) {
activityNavigator.openPostDetailStats(this, item.id, item.postType, item.title, item.url)
analyticsTracker.track(Stat.STATS_POSTS_AND_PAGES_ITEM_TAPPED)
PostStatsDetailActivity.start(this, item.id, item.title)
}

private fun createNewPost() {
selectedSiteRepository.getSelectedSite()?.let { site ->
ActivityLauncher.addNewPostForResult(
this,
site,
false,
PagePostCreationSourcesDetail.POST_FROM_STATS,
-1,
null
)
}
}

/**
Expand Down Expand Up @@ -306,7 +325,8 @@ private fun NewStatsScreen(
showIntroBottomSheet: Boolean = false,
onIntroDismissed: () -> Unit = {},
onStatsUrlClick: (String) -> Unit = {},
onPostItemClick: (MostViewedItem) -> Unit = {}
onPostItemClick: (MostViewedItem) -> Unit = {},
onCreatePostClick: () -> Unit = {}
) {
val viewsStatsViewModel: ViewsStatsViewModel = viewModel()
val selectedPeriod by viewsStatsViewModel.selectedPeriod.collectAsState()
Expand Down Expand Up @@ -448,7 +468,8 @@ private fun NewStatsScreen(
tab = tabs[page],
viewsStatsViewModel = viewsStatsViewModel,
onStatsUrlClick = onStatsUrlClick,
onPostItemClick = onPostItemClick
onPostItemClick = onPostItemClick,
onCreatePostClick = onCreatePostClick
)
}
}
Expand All @@ -460,7 +481,8 @@ private fun StatsTabContent(
tab: StatsTab,
viewsStatsViewModel: ViewsStatsViewModel,
onStatsUrlClick: (String) -> Unit = {},
onPostItemClick: (MostViewedItem) -> Unit = {}
onPostItemClick: (MostViewedItem) -> Unit = {},
onCreatePostClick: () -> Unit = {}
) {
when (tab) {
StatsTab.TRAFFIC -> TrafficTabContent(
Expand All @@ -469,7 +491,8 @@ private fun StatsTabContent(
onPostItemClick = onPostItemClick
)
StatsTab.INSIGHTS -> InsightsTabContent(
onStatsUrlClick = onStatsUrlClick
onStatsUrlClick = onStatsUrlClick,
onCreatePostClick = onCreatePostClick
)
StatsTab.SUBSCRIBERS -> SubscribersTabContent()
}
Expand Down Expand Up @@ -1121,15 +1144,18 @@ private fun InsightsTabContent(
mostPopularDayViewModel: MostPopularDayViewModel = viewModel(),
mostPopularTimeViewModel: MostPopularTimeViewModel = viewModel(),
tagsAndCategoriesViewModel: TagsAndCategoriesViewModel = viewModel(),
latestPostViewModel: LatestPostViewModel = viewModel(),
insightsViewModel: InsightsViewModel = viewModel(),
onStatsUrlClick: (String) -> Unit = {}
onStatsUrlClick: (String) -> Unit = {},
onCreatePostClick: () -> Unit = {}
) {
val context = LocalContext.current
val yearInReviewUiState by yearInReviewViewModel.uiState.collectAsState()
val allTimeStatsUiState by allTimeStatsViewModel.uiState.collectAsState()
val mostPopularDayUiState by mostPopularDayViewModel.uiState.collectAsState()
val mostPopularTimeUiState by mostPopularTimeViewModel.uiState.collectAsState()
val tagsAndCategoriesUiState by tagsAndCategoriesViewModel.uiState.collectAsState()
val latestPostUiState by latestPostViewModel.uiState.collectAsState()
val isRefreshing by insightsViewModel.isDataRefreshing.collectAsState()
val pullToRefreshState = rememberPullToRefreshState()

Expand All @@ -1145,6 +1171,9 @@ private fun InsightsTabContent(
if (InsightsCardType.TAGS_AND_CATEGORIES in cardsToLoad) {
tagsAndCategoriesViewModel.loadData()
}
if (InsightsCardType.LATEST_POST in cardsToLoad) {
latestPostViewModel.loadData()
}
}

val onRetryData = remember { { insightsViewModel.fetchData() } }
Expand Down Expand Up @@ -1218,6 +1247,11 @@ private fun InsightsTabContent(
) {
tagsAndCategoriesViewModel.refresh()
}
if (InsightsCardType.LATEST_POST
in visibleCards
) {
latestPostViewModel.refresh()
}
},
indicator = {
PullToRefreshDefaults.Indicator(
Expand Down Expand Up @@ -1269,6 +1303,24 @@ private fun InsightsTabContent(
onMoveDown = { insightsViewModel.moveCardDown(cardType) },
onMoveToBottom = { insightsViewModel.moveCardToBottom(cardType) }
)
InsightsCardType.LATEST_POST -> LatestPostCard(
uiState = latestPostUiState,
onRemoveCard = { insightsViewModel.removeCard(cardType) },
onRetry = { latestPostViewModel.refresh() },
onPostClick = { postId, title ->
PostStatsDetailActivity.start(
context,
postId,
title
)
},
onCreatePostClick = onCreatePostClick,
cardPosition = pos,
onMoveUp = { insightsViewModel.moveCardUp(cardType) },
onMoveToTop = { insightsViewModel.moveCardToTop(cardType) },
onMoveDown = { insightsViewModel.moveCardDown(cardType) },
onMoveToBottom = { insightsViewModel.moveCardToBottom(cardType) }
)
InsightsCardType.MOST_POPULAR_DAY -> MostPopularDayCard(
uiState = mostPopularDayUiState,
onRemoveCard = { insightsViewModel.removeCard(cardType) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ import androidx.compose.ui.graphics.Color
object StatsColors {
val ChangeBadgePositive = Color(0xFF2E7D32)
val ChangeBadgeNegative = Color(0xFFE91E63)

/**
* The selected bar in a day-views chart. Shares the negative badge's hue, but it marks the
* user's selection, not a decline -- keep them separate so either can move independently.
*/
val ChartSelectedBar = Color(0xFFE91E63)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.wordpress.android.ui.newstats.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

// Zero-value entries still get a sliver of a bar so the series reads as continuous.
private const val MIN_BAR_FRACTION = 0.02f

/**
* A compact bar chart of a single series, scaled against its own largest value. Renders nothing
* when every value is zero.
*/
@Composable
fun StatsBarChart(
values: List<Long>,
height: Dp,
barSpacing: Dp,
modifier: Modifier = Modifier
) {
val maxValue = values.maxOrNull() ?: 0L
if (maxValue <= 0L) return

val barColor = MaterialTheme.colorScheme.primary

Row(
modifier = modifier
.fillMaxWidth()
.height(height),
horizontalArrangement = Arrangement.spacedBy(barSpacing),
verticalAlignment = Alignment.Bottom
) {
values.forEach { value ->
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
val fraction = (
value.toFloat() / maxValue.toFloat()
).coerceIn(MIN_BAR_FRACTION, 1f)
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(fraction)
.clip(
RoundedCornerShape(
topStart = 2.dp,
topEnd = 2.dp
)
)
.background(barColor)
)
}
}
}
}
Loading