Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 @@ -4,23 +4,30 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
Expand Down Expand Up @@ -48,6 +55,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.res.stringResource
Expand All @@ -61,6 +69,7 @@ import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.newstats.components.AddStatsCardBottomSheet
import org.wordpress.android.ui.newstats.components.CardPosition
import org.wordpress.android.ui.newstats.countries.CountriesCard
import org.wordpress.android.ui.newstats.countries.CountriesDetailActivity
import org.wordpress.android.ui.newstats.countries.CountriesViewModel
Expand Down Expand Up @@ -138,12 +147,23 @@ private fun NewStatsScreen(
},
actions = {
Box {
IconButton(onClick = { showPeriodMenu = true }) {
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(
Expand Down Expand Up @@ -228,6 +248,7 @@ private fun TrafficTabContent(
// Card configuration state
val visibleCards by newStatsViewModel.visibleCards.collectAsState()
val hiddenCards by newStatsViewModel.hiddenCards.collectAsState()
val isNetworkAvailable by newStatsViewModel.isNetworkAvailable.collectAsState()
var showAddCardSheet by remember { mutableStateOf(false) }
val addCardSheetState = rememberModalBottomSheetState()

Expand All @@ -248,11 +269,43 @@ private fun TrafficTabContent(
)
}

// Track whether to show the no-connection screen
// Once user retries or network becomes available, show cards instead
var showNoConnectionScreen by remember { mutableStateOf(!isNetworkAvailable) }

// React to network availability changes
LaunchedEffect(isNetworkAvailable) {
if (isNetworkAvailable && showNoConnectionScreen) {
// Network became available while on no-connection screen - auto-load
showNoConnectionScreen = false
todaysStatsViewModel.loadData()
viewsStatsViewModel.loadData()
mostViewedViewModel.loadData()
countriesViewModel.loadData()
} else if (!isNetworkAvailable && !showNoConnectionScreen) {
// Network became unavailable while viewing cards - show no-connection screen
showNoConnectionScreen = true
}
}

// Show no connection screen only when network is unavailable
if (showNoConnectionScreen) {
NoConnectionContent(
onRetry = {
newStatsViewModel.checkNetworkStatus()
// Only show cards if network is now available
// The LaunchedEffect will handle loading data when isNetworkAvailable becomes true
}
)
return
}

PullToRefreshBox(
modifier = Modifier.fillMaxSize(),
isRefreshing = isRefreshing,
state = pullToRefreshState,
onRefresh = {
newStatsViewModel.checkNetworkStatus()
todaysStatsViewModel.refresh()
viewsStatsViewModel.refresh()
mostViewedViewModel.refresh()
Expand Down Expand Up @@ -287,18 +340,36 @@ private fun TrafficTabContent(
)
}

// Memoize card positions to avoid recalculation on every recomposition
val cardPositions = remember(visibleCards) {
visibleCards.mapIndexed { index, _ ->
CardPosition(index = index, totalCards = visibleCards.size)
}
}

// Dynamic card rendering based on configuration
visibleCards.forEach { cardType ->
visibleCards.forEachIndexed { index, cardType ->
val cardPosition = cardPositions[index]
when (cardType) {
StatsCardType.TODAYS_STATS -> TodaysStatsCard(
uiState = todaysStatsUiState,
onRemoveCard = { newStatsViewModel.removeCard(cardType) }
onRemoveCard = { newStatsViewModel.removeCard(cardType) },
cardPosition = cardPosition,
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
)
StatsCardType.VIEWS_STATS -> ViewsStatsCard(
uiState = viewsStatsUiState,
onChartTypeChanged = viewsStatsViewModel::onChartTypeChanged,
onRetry = viewsStatsViewModel::onRetry,
onRemoveCard = { newStatsViewModel.removeCard(cardType) }
onRemoveCard = { newStatsViewModel.removeCard(cardType) },
cardPosition = cardPosition,
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
)
StatsCardType.MOST_VIEWED_POSTS_AND_PAGES -> MostViewedCard(
uiState = postsUiState,
Expand All @@ -316,7 +387,12 @@ private fun TrafficTabContent(
)
},
onRetry = mostViewedViewModel::onRetryPosts,
onRemoveCard = { newStatsViewModel.removeCard(cardType) }
onRemoveCard = { newStatsViewModel.removeCard(cardType) },
cardPosition = cardPosition,
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
)
StatsCardType.MOST_VIEWED_REFERRERS -> MostViewedCard(
uiState = referrersUiState,
Expand All @@ -334,7 +410,12 @@ private fun TrafficTabContent(
)
},
onRetry = mostViewedViewModel::onRetryReferrers,
onRemoveCard = { newStatsViewModel.removeCard(cardType) }
onRemoveCard = { newStatsViewModel.removeCard(cardType) },
cardPosition = cardPosition,
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
)
StatsCardType.COUNTRIES -> CountriesCard(
uiState = countriesUiState,
Expand All @@ -353,7 +434,12 @@ private fun TrafficTabContent(
)
},
onRetry = countriesViewModel::onRetry,
onRemoveCard = { newStatsViewModel.removeCard(cardType) }
onRemoveCard = { newStatsViewModel.removeCard(cardType) },
cardPosition = cardPosition,
onMoveUp = { newStatsViewModel.moveCardUp(cardType) },
onMoveToTop = { newStatsViewModel.moveCardToTop(cardType) },
onMoveDown = { newStatsViewModel.moveCardDown(cardType) },
onMoveToBottom = { newStatsViewModel.moveCardToBottom(cardType) }
)
}
}
Expand Down Expand Up @@ -386,6 +472,55 @@ private fun AddCardButton(
}
}

@Composable
private fun NoConnectionContent(
onRetry: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(R.drawable.ic_wifi_off_24px),
contentDescription = null,
modifier = Modifier
.size(48.dp)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = CircleShape
)
.padding(12.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.no_connection_error_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.no_connection_error_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRetry) {
Text(stringResource(R.string.retry))
}
}
}
}

@Composable
private fun PlaceholderTabContent(tab: StatsTab) {
Box(
Expand Down Expand Up @@ -435,6 +570,17 @@ private fun StatsPeriodMenu(
}
}

@Composable
private fun StatsPeriod.getDisplayLabel(): String {
return when (this) {
is StatsPeriod.Custom -> {
val formatter = java.time.format.DateTimeFormatter.ofPattern("MMM d")
"${startDate.format(formatter)} - ${endDate.format(formatter)}"
}
else -> stringResource(id = labelResId)
}
}

@Preview
@Composable
fun NewStatsScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.wordpress.android.ui.mysite.SelectedSiteRepository
import org.wordpress.android.ui.newstats.repository.StatsCardsConfigurationRepository
import org.wordpress.android.util.NetworkUtilsWrapper
import javax.inject.Inject

/**
Expand All @@ -18,22 +19,31 @@ import javax.inject.Inject
@HiltViewModel
class NewStatsViewModel @Inject constructor(
private val selectedSiteRepository: SelectedSiteRepository,
private val cardConfigurationRepository: StatsCardsConfigurationRepository
private val cardConfigurationRepository: StatsCardsConfigurationRepository,
private val networkUtilsWrapper: NetworkUtilsWrapper
) : ViewModel() {
private val _visibleCards = MutableStateFlow<List<StatsCardType>>(StatsCardType.defaultCards())
val visibleCards: StateFlow<List<StatsCardType>> = _visibleCards.asStateFlow()

private val _hiddenCards = MutableStateFlow<List<StatsCardType>>(emptyList())
val hiddenCards: StateFlow<List<StatsCardType>> = _hiddenCards.asStateFlow()

private val _isNetworkAvailable = MutableStateFlow(true)
val isNetworkAvailable: StateFlow<Boolean> = _isNetworkAvailable.asStateFlow()

private val siteId: Long
get() = selectedSiteRepository.getSelectedSite()?.siteId ?: 0L

init {
checkNetworkStatus()
loadConfiguration()
observeConfigurationChanges()
}

fun checkNetworkStatus() {
_isNetworkAvailable.value = networkUtilsWrapper.isNetworkAvailable()
}

private fun loadConfiguration() {
val currentSiteId = siteId // Capture siteId to avoid race conditions during site switching
viewModelScope.launch {
Expand Down Expand Up @@ -71,4 +81,32 @@ class NewStatsViewModel @Inject constructor(
cardConfigurationRepository.addCard(currentSiteId, cardType)
}
}

fun moveCardUp(cardType: StatsCardType) {
val currentSiteId = siteId // Capture siteId to avoid race conditions during site switching
viewModelScope.launch {
cardConfigurationRepository.moveCardUp(currentSiteId, cardType)
}
}

fun moveCardToTop(cardType: StatsCardType) {
val currentSiteId = siteId // Capture siteId to avoid race conditions during site switching
viewModelScope.launch {
cardConfigurationRepository.moveCardToTop(currentSiteId, cardType)
}
}

fun moveCardDown(cardType: StatsCardType) {
val currentSiteId = siteId // Capture siteId to avoid race conditions during site switching
viewModelScope.launch {
cardConfigurationRepository.moveCardDown(currentSiteId, cardType)
}
}

fun moveCardToBottom(cardType: StatsCardType) {
val currentSiteId = siteId // Capture siteId to avoid race conditions during site switching
viewModelScope.launch {
cardConfigurationRepository.moveCardToBottom(currentSiteId, cardType)
}
}
}
Loading