Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.wordpress.android.R
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.ActivityNavigator
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.newstats.components.AddCardBottomSheet
Expand Down Expand Up @@ -137,6 +138,9 @@ class NewStatsActivity : BaseAppCompatActivity() {
@Inject
lateinit var newStatsFeatureConfig: NewStatsFeatureConfig

@Inject
lateinit var activityNavigator: ActivityNavigator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val shouldShowIntro =
Expand All @@ -153,6 +157,9 @@ class NewStatsActivity : BaseAppCompatActivity() {
onIntroDismissed = {
appPrefsWrapper
.setNewStatsIntroShown(true)
},
onReferrerChildClick = { url ->
activityNavigator.openInCustomTab(this, url)
}
)
}
Expand Down Expand Up @@ -228,7 +235,8 @@ private fun NewStatsScreen(
showSwitchToOldStats: Boolean = false,
onSwitchToOldStats: () -> Unit = {},
showIntroBottomSheet: Boolean = false,
onIntroDismissed: () -> Unit = {}
onIntroDismissed: () -> Unit = {},
onReferrerChildClick: (String) -> Unit = {}
) {
val viewsStatsViewModel: ViewsStatsViewModel = viewModel()
val selectedPeriod by viewsStatsViewModel.selectedPeriod.collectAsState()
Expand Down Expand Up @@ -370,7 +378,8 @@ private fun NewStatsScreen(
) { page ->
StatsTabContent(
tab = tabs[page],
viewsStatsViewModel = viewsStatsViewModel
viewsStatsViewModel = viewsStatsViewModel,
onReferrerChildClick = onReferrerChildClick
)
}
}
Expand All @@ -380,11 +389,13 @@ private fun NewStatsScreen(
@Composable
private fun StatsTabContent(
tab: StatsTab,
viewsStatsViewModel: ViewsStatsViewModel
viewsStatsViewModel: ViewsStatsViewModel,
onReferrerChildClick: (String) -> Unit = {}
) {
when (tab) {
StatsTab.TRAFFIC -> TrafficTabContent(
viewsStatsViewModel = viewsStatsViewModel
viewsStatsViewModel = viewsStatsViewModel,
onReferrerChildClick = onReferrerChildClick
)
StatsTab.INSIGHTS -> InsightsTabContent()
StatsTab.SUBSCRIBERS -> SubscribersTabContent()
Expand All @@ -406,7 +417,8 @@ private fun TrafficTabContent(
fileDownloadsViewModel: FileDownloadsViewModel = viewModel(),
devicesViewModel: DevicesViewModel = viewModel(),
utmViewModel: UtmViewModel = viewModel(),
newStatsViewModel: NewStatsViewModel = viewModel()
newStatsViewModel: NewStatsViewModel = viewModel(),
onReferrerChildClick: (String) -> Unit = {}
) {
val context = LocalContext.current
val todaysStatsUiState by todaysStatsViewModel.uiState.collectAsState()
Expand Down Expand Up @@ -679,15 +691,11 @@ private fun TrafficTabContent(
uiState = referrersUiState,
cardType = cardType,
onShowAllClick = {
val detailData = mostViewedViewModel.getReferrersDetailData()
MostViewedDetailActivity.start(
// The referrers detail screen self-fetches the full (unbounded) list, so
// only the selected period is passed instead of the whole item list.
MostViewedDetailActivity.startReferrers(
context = context,
cardType = detailData.cardType,
items = detailData.items,
totalViews = detailData.totalViews,
totalViewsChange = detailData.totalViewsChange,
totalViewsChangePercent = detailData.totalViewsChangePercent,
dateRange = detailData.dateRange
period = mostViewedViewModel.getCurrentPeriod()
)
},
onRetry = mostViewedViewModel::onRetryReferrers,
Expand All @@ -696,7 +704,8 @@ private fun TrafficTabContent(
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) },
onChildClick = onReferrerChildClick
)
StatsCardType.LOCATIONS -> LocationsCard(
uiState = locationsUiState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,16 @@ sealed class ReferrersDataResult {
*/
data class ReferrerDataItem(
val name: String,
val views: Long,
val children: List<ReferrerChildDataItem> = emptyList()
)

/**
* A child referrer nested under a referrer group (e.g. a specific search engine).
*/
data class ReferrerChildDataItem(
val name: String,
val url: String?,
val views: Long
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import uniffi.wp_api.StatsFileDownloadsParams
import uniffi.wp_api.StatsFileDownloadsPeriod
import uniffi.wp_api.StatsReferrersParams
import uniffi.wp_api.StatsReferrersPeriod
import uniffi.wp_api.StatsReferrersResults
import uniffi.wp_api.StatsRegionViewsParams
import uniffi.wp_api.StatsRegionViewsPeriod
import uniffi.wp_api.StatsDevicesParams
Expand Down Expand Up @@ -204,19 +205,22 @@ class StatsDataSourceImpl @Inject constructor(
dateRange: StatsDateRange,
max: Int
): ReferrersDataResult {
// Referrers-specific max semantics: the server treats max=0 as "all" but an unset max as
// "default 10", so 0 is sent explicitly (unlike other endpoints that map 0 -> null).
val maxParam = max.coerceAtLeast(0).toUInt()
val params = when (dateRange) {
is StatsDateRange.Preset -> StatsReferrersParams(
period = StatsReferrersPeriod.DAY,
date = dateRange.date,
num = dateRange.num.toUInt(),
max = max.coerceAtLeast(1).toUInt(),
max = maxParam,
locale = wpComLanguage
)
is StatsDateRange.Custom -> StatsReferrersParams(
period = StatsReferrersPeriod.DAY,
date = dateRange.date,
startDate = dateRange.startDate,
max = max.coerceAtLeast(1).toUInt(),
max = maxParam,
locale = wpComLanguage
)
}
Expand All @@ -238,7 +242,8 @@ class StatsDataSourceImpl @Inject constructor(
groups.map { group ->
ReferrerDataItem(
name = group.name.orEmpty(),
views = group.total?.toLong() ?: 0L
views = group.total?.toLong() ?: 0L,
children = group.results.toChildren()
)
}
)
Expand All @@ -249,6 +254,18 @@ class StatsDataSourceImpl @Inject constructor(
}
}

private fun StatsReferrersResults?.toChildren(): List<ReferrerChildDataItem> =
when (this) {
is StatsReferrersResults.Referrers -> v1.map { child ->
ReferrerChildDataItem(
name = child.name.orEmpty(),
url = child.url,
views = child.views?.toLong() ?: 0L
)
}
else -> emptyList()
}

private fun buildCountryViewsParams(dateRange: StatsDateRange, max: Int) = when (dateRange) {
is StatsDateRange.Preset -> StatsCountryViewsParams(
period = StatsCountryViewsPeriod.DAY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,25 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
Expand All @@ -38,13 +34,11 @@ import org.wordpress.android.ui.newstats.components.CardPosition
import org.wordpress.android.ui.newstats.components.ShowAllFooter
import org.wordpress.android.ui.newstats.components.StatsCardMenu
import org.wordpress.android.ui.newstats.util.ShimmerBox
import org.wordpress.android.ui.newstats.util.formatStatValue
import java.util.Locale

private val CardCornerRadius = 10.dp
private val CardPadding = 16.dp
private val CardMargin = 16.dp
internal const val HIGHLIGHTED_ITEM_BACKGROUND_ALPHA = 0.08f
private const val LOADING_SHIMMER_ITEM_COUNT = 5

@Composable
Expand All @@ -60,7 +54,8 @@ fun MostViewedCard(
onMoveToTop: (() -> Unit)? = null,
onMoveDown: (() -> Unit)? = null,
onMoveToBottom: (() -> Unit)? = null,
onOpenWpAdmin: (() -> Unit)? = null
onOpenWpAdmin: (() -> Unit)? = null,
onChildClick: (String) -> Unit = {}
) {
val borderColor = MaterialTheme.colorScheme.outlineVariant

Expand All @@ -80,7 +75,8 @@ fun MostViewedCard(
is MostViewedCardUiState.Loading -> LoadingContent()
is MostViewedCardUiState.Loaded -> LoadedContent(
uiState, cardType, onShowAllClick, onRemoveCard,
cardPosition, onMoveUp, onMoveToTop, onMoveDown, onMoveToBottom
cardPosition, onMoveUp, onMoveToTop, onMoveDown, onMoveToBottom,
onChildClick
)
is MostViewedCardUiState.Error -> ErrorContent(
uiState, cardType, onRetry, onRemoveCard,
Expand Down Expand Up @@ -173,7 +169,8 @@ private fun LoadedContent(
onMoveUp: (() -> Unit)?,
onMoveToTop: (() -> Unit)?,
onMoveDown: (() -> Unit)?,
onMoveToBottom: (() -> Unit)?
onMoveToBottom: (() -> Unit)?,
onChildClick: (String) -> Unit
) {
Column(
modifier = Modifier
Expand All @@ -199,7 +196,20 @@ private fun LoadedContent(
val percentage = if (state.maxViewsForBar > 0) {
item.views.toFloat() / state.maxViewsForBar.toFloat()
} else 0f
MostViewedItemRow(item = item, percentage = percentage)
// Key on the index plus the item id: the index guarantees uniqueness (referrer ids
// are derived from name.hashCode() and can collide on empty/duplicate names), while
// the id resets the row's saved expanded state when a different referrer lands at
// this position after a period reload. rememberSaveable keeps it across rotation.
key(index, item.id) {
MostViewedExpandableRow(
title = item.title,
views = item.views,
change = item.change,
children = item.children,
percentage = percentage,
onChildClick = onChildClick
)
}
if (index < state.items.lastIndex) {
Spacer(modifier = Modifier.height(4.dp))
}
Expand Down Expand Up @@ -282,67 +292,6 @@ private fun ColumnHeadersRow(cardType: StatsCardType) {
}
}

@Composable
private fun MostViewedItemRow(item: MostViewedItem, percentage: Float) {
val barColor = MaterialTheme.colorScheme.primary.copy(alpha = HIGHLIGHTED_ITEM_BACKGROUND_ALPHA)

Box(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clip(RoundedCornerShape(8.dp))
) {
// Background bar representing the percentage
Box(
modifier = Modifier
.fillMaxWidth(fraction = percentage)
.fillMaxHeight()
.background(barColor)
)

// Content
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)

Spacer(modifier = Modifier.width(16.dp))

Row(verticalAlignment = Alignment.CenterVertically) {
Column(horizontalAlignment = Alignment.End) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = formatStatValue(item.views),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
ChangeIndicator(change = item.change)
}
}
}
}
}

@Composable
internal fun ChangeIndicator(change: MostViewedChange) {
val (text, color) = when (change) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,24 @@ data class MostViewedItem(
val id: Long,
val title: String,
val views: Long,
val change: MostViewedChange
val change: MostViewedChange,
val children: List<MostViewedChildItem> = emptyList()
)

/**
* A child item nested under a Most Viewed item (e.g. a referrer under a referrer group).
*
* @param name The name of the child item
* @param url The URL opened when the child row is tapped (null if not linkable)
* @param views The number of views
*/
@Parcelize
data class MostViewedChildItem(
val name: String,
val url: String?,
val views: Long
) : Parcelable

/**
* Represents the change in views compared to the previous period.
*/
Expand Down Expand Up @@ -84,5 +99,6 @@ data class MostViewedDetailItem(
val id: Long,
val title: String,
val views: Long,
val change: MostViewedChange
val change: MostViewedChange,
val children: List<MostViewedChildItem> = emptyList()
) : Parcelable
Loading
Loading