diff --git a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt index 24d9dc141ea0..ba88d3608468 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.dataview -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -20,19 +19,16 @@ 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.platform.LocalContext import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.dataview.DummyDataViewItems.getDummyDataViewItems +import org.wordpress.android.ui.dataview.compose.RemoteImage /** * Provides a card for displaying a single [DataViewItem] which contains a primary image, @@ -61,6 +57,11 @@ fun DataViewItemCard( RemoteImage( imageUrl = image.imageUrl, fallbackImageRes = image.fallbackImageRes, + modifier = Modifier + .padding(end = 16.dp) + .size(dimensionResource(R.dimen.jp_migration_user_avatar_size)) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface) ) Spacer(modifier = Modifier.width(8.dp)) } @@ -135,34 +136,6 @@ private fun maxLinesFor(type: DataViewFieldType) = when (type) { DataViewFieldType.EMAIL -> 1 } -@Composable -private fun RemoteImage( - imageUrl: String?, - fallbackImageRes: Int, -) { - val modifier = Modifier - .padding(end = 16.dp) - .size(dimensionResource(R.dimen.jp_migration_user_avatar_size)) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surface) - if (imageUrl.isNullOrBlank()) { - Image( - painter = painterResource(id = fallbackImageRes), - contentDescription = null, - modifier = modifier - ) - } else { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .error(fallbackImageRes) - .crossfade(true) - .build(), - contentDescription = null, - modifier = modifier - ) - } -} @Preview(showBackground = true) @Composable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewScreen.kt index c60408e72aa0..dcaf42b14951 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewScreen.kt @@ -8,13 +8,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search @@ -26,9 +24,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -47,16 +43,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R import org.wordpress.android.ui.compose.components.EmptyContentM3 -import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.dataview.DummyDataViewItems.getDummyDataViewItems /** * Provides a basic screen for displaying a list of [DataViewItem]s * which includes search and filter functionality. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DataViewScreen( - title: String, uiState: State, items: State>, supportedFilters: List, @@ -64,47 +59,57 @@ fun DataViewScreen( onSearchQueryChange: (String) -> Unit, onItemClick: (DataViewItem) -> Unit, onFilterClick: (DataViewItemFilter) -> Unit, - onBackClick: () -> Unit, onRefresh: () -> Unit, onFetchMore: () -> Unit, modifier: Modifier = Modifier, errorMessage: String? = null, ) { - Screen( - title = title, - onBackClick = onBackClick, + val refreshState = remember { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + + PullToRefreshBox( + modifier = modifier + .fillMaxSize(), + isRefreshing = refreshState.value, + state = pullToRefreshState, onRefresh = onRefresh, - content = { - Column( - modifier = modifier - .fillMaxWidth() - .padding(16.dp) - ) { - SearchAndFilterBar( - onSearchQueryChange = onSearchQueryChange, - onFilterClick = onFilterClick, - supportedFilters = supportedFilters, - currentFilter = currentFilter - ) + indicator = { + PullToRefreshDefaults.Indicator( + state = pullToRefreshState, + isRefreshing = refreshState.value, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + SearchAndFilterBar( + onSearchQueryChange = onSearchQueryChange, + onFilterClick = onFilterClick, + supportedFilters = supportedFilters, + currentFilter = currentFilter + ) - when (uiState.value) { - DataViewUiState.LOADING -> LoadingDataView() - DataViewUiState.EMPTY -> EmptyDataView() - DataViewUiState.EMPTY_SEARCH -> EmptySearchDataView() - DataViewUiState.ERROR -> ErrorDataView(errorMessage) - DataViewUiState.OFFLINE -> OfflineDataView() - DataViewUiState.LOADING_MORE, - DataViewUiState.LOADED -> LoadedDataView( - items = items, - onItemClick = onItemClick, - onFetchMore = onFetchMore, - showProgress = uiState.value == DataViewUiState.LOADING_MORE - ) - } + when (uiState.value) { + DataViewUiState.LOADING -> LoadingDataView() + DataViewUiState.EMPTY -> EmptyDataView() + DataViewUiState.EMPTY_SEARCH -> EmptySearchDataView() + DataViewUiState.ERROR -> ErrorDataView(errorMessage) + DataViewUiState.OFFLINE -> OfflineDataView() + DataViewUiState.LOADING_MORE, + DataViewUiState.LOADED -> LoadedDataView( + items = items, + onItemClick = onItemClick, + onFetchMore = onFetchMore, + showProgress = uiState.value == DataViewUiState.LOADING_MORE + ) } } - - ) + } } @Composable @@ -230,7 +235,8 @@ private fun LoadedDataView( } if (showProgress) { CircularProgressIndicator( - modifier = Modifier.size(48.dp) + modifier = Modifier + .size(48.dp) .align(Alignment.Center) ) } @@ -312,63 +318,11 @@ private fun OfflineDataView() { } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun Screen( - title: String, - content: @Composable () -> Unit, - onRefresh: () -> Unit, - onBackClick: () -> Unit -) { - AppThemeM3 { - Scaffold( - topBar = { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) - } - }, - ) - }, - ) { contentPadding -> - val refreshState = remember { mutableStateOf(false) } - val pullToRefreshState = rememberPullToRefreshState() - - PullToRefreshBox( - modifier = Modifier - .fillMaxSize(), - isRefreshing = refreshState.value, - state = pullToRefreshState, - onRefresh = onRefresh, - indicator = { - PullToRefreshDefaults.Indicator( - state = pullToRefreshState, - isRefreshing = refreshState.value, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.align(Alignment.TopCenter), - ) - } - ) { - Column( - modifier = Modifier - .imePadding() - .padding(contentPadding) - ) { - content() - } - } - } - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun LoadedPreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.LOADED) }, items = remember { mutableStateOf(getDummyDataViewItems()) }, supportedFilters = emptyList(), @@ -378,7 +332,6 @@ private fun LoadedPreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } @@ -387,7 +340,6 @@ private fun LoadedPreview() { @Composable private fun LoadingPreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.LOADING) }, items = remember { mutableStateOf(emptyList()) }, supportedFilters = emptyList(), @@ -397,7 +349,6 @@ private fun LoadingPreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } @@ -406,7 +357,6 @@ private fun LoadingPreview() { @Composable private fun EmptyPreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.EMPTY) }, items = remember { mutableStateOf(emptyList()) }, supportedFilters = emptyList(), @@ -416,7 +366,6 @@ private fun EmptyPreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } @@ -425,7 +374,6 @@ private fun EmptyPreview() { @Composable private fun EmptySearchPreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.EMPTY_SEARCH) }, items = remember { mutableStateOf(emptyList()) }, supportedFilters = emptyList(), @@ -435,7 +383,6 @@ private fun EmptySearchPreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } @@ -444,7 +391,6 @@ private fun EmptySearchPreview() { @Composable private fun OfflinePreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.OFFLINE) }, items = remember { mutableStateOf(emptyList()) }, supportedFilters = emptyList(), @@ -454,7 +400,6 @@ private fun OfflinePreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } @@ -463,7 +408,6 @@ private fun OfflinePreview() { @Composable private fun ErrorPreview() { DataViewScreen( - title = "Title", uiState = remember { mutableStateOf(DataViewUiState.ERROR) }, items = remember { mutableStateOf(emptyList()) }, supportedFilters = emptyList(), @@ -473,6 +417,5 @@ private fun ErrorPreview() { onSearchQueryChange = { }, onItemClick = {}, onFilterClick = { }, - onBackClick = { }, ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/dataview/compose/RemoteImage.kt b/WordPress/src/main/java/org/wordpress/android/ui/dataview/compose/RemoteImage.kt new file mode 100644 index 000000000000..02b16d6a6063 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/dataview/compose/RemoteImage.kt @@ -0,0 +1,34 @@ +package org.wordpress.android.ui.dataview.compose + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.AsyncImage +import coil.request.ImageRequest + +@Composable +fun RemoteImage( + imageUrl: String?, + fallbackImageRes: Int, + modifier: Modifier = Modifier +) { + if (imageUrl.isNullOrBlank()) { + Image( + painter = painterResource(id = fallbackImageRes), + contentDescription = null, + modifier = modifier + ) + } else { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .error(fallbackImageRes) + .crossfade(true) + .build(), + contentDescription = null, + modifier = modifier + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt new file mode 100644 index 000000000000..afbb68e30fb2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt @@ -0,0 +1,336 @@ +package org.wordpress.android.ui.subscribers + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.MailOutline +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppThemeM3 +import org.wordpress.android.ui.dataview.compose.RemoteImage +import org.wordpress.android.ui.subscribers.SubscribersViewModel.Companion.displayNameOrEmail +import uniffi.wp_api.Subscriber +import java.util.Date + +@Composable +fun SubscriberDetailScreen( + subscriber: Subscriber, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + ProfileHeader(subscriber) + + Spacer(modifier = Modifier.height(24.dp)) + + EmailStatsCard() + + Spacer(modifier = Modifier.height(16.dp)) + + NewsletterSubscriptionCard(subscriber) + + Spacer(modifier = Modifier.height(16.dp)) + + SubscriberDetailsCard(subscriber) + + Spacer(modifier = Modifier.height(32.dp)) + + DeleteSubscriberButton() + } +} + +@Composable +fun ProfileHeader( + subscriber: Subscriber +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row { + RemoteImage( + imageUrl = subscriber.avatar, + fallbackImageRes = R.drawable.ic_user_placeholder_primary_24, + modifier = Modifier + .size(96.dp) + .clip(CircleShape) + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Row { + Text( + text = subscriber.displayNameOrEmail(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + } + Row { + Text( + text = subscriber.emailAddress, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + // TODO remove this once we have actual data + Spacer(modifier = Modifier.height(12.dp)) + Row { + Text( + text = "Note: Displaying dummy data", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +fun EmailStatsCard() { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + StatItem( + icon = Icons.Default.Email, + label = stringResource(R.string.subscribers_emails_sent_label), + value = "100" + ) + StatItem( + icon = Icons.Default.MailOutline, + label = stringResource(R.string.subscribers_opened_label), + value = "10" + ) + StatItem( + icon = Icons.Default.Check, + label = stringResource(R.string.subscribers_clicked_label), + value = "10%" + ) + } + } +} + +@Composable +fun StatItem( + icon: ImageVector, + label: String, + value: String +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = value, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +fun NewsletterSubscriptionCard(subscriber: Subscriber) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text( + text = stringResource(R.string.subscribers_newsletter_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + letterSpacing = 1.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DetailRow( + label = stringResource(R.string.subscribers_date_label), + value = subscriber.dateSubscribed.toString() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + DetailRow( + label = stringResource(R.string.subscribers_plan_label), + value = "???", + valueColor = MaterialTheme.colorScheme.primary + ) + } + } +} + +@Composable +fun SubscriberDetailsCard(subscriber: Subscriber) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text( + text = stringResource(R.string.subscribers_detail_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + letterSpacing = 1.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DetailRow( + label = stringResource(R.string.subscribers_email_label), + value = subscriber.emailAddress, + valueColor = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(12.dp)) + + DetailRow( + label = stringResource(R.string.subscribers_country_label), + value = "???" + ) + + Spacer(modifier = Modifier.height(12.dp)) + + DetailRow( + label = stringResource(R.string.subscribers_site_label), + value = "???", + valueColor = MaterialTheme.colorScheme.primary + ) + } + } +} + +@Composable +fun DetailRow( + label: String, + value: String, + valueColor: Color = MaterialTheme.colorScheme.onSurface +) { + Column { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = valueColor + ) + } +} + +@Composable +fun DeleteSubscriberButton() { + Button( + onClick = { /* Handle delete action */ }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.error + ), + elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.subscribers_delete_button), + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.subscribers_delete_button), + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SubscriberDetailScreenPreview() { + val subscriber = Subscriber( + userId = 0L, + displayName = "User Name", + emailAddress = "email@example.com", + emailSubscriptionId = 0u, + dateSubscribed = Date(), + subscriptionStatus = "Subscribed", + avatar = "", + ) + + AppThemeM3 { + SubscriberDetailScreen(subscriber) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersActivity.kt index bff3e37f2277..b16361705df4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersActivity.kt @@ -3,56 +3,140 @@ package org.wordpress.android.ui.subscribers import android.os.Build import android.os.Bundle import androidx.activity.viewModels +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.dataview.DataViewScreen import org.wordpress.android.ui.main.BaseAppCompatActivity +import org.wordpress.android.ui.subscribers.SubscribersViewModel.Companion.displayNameOrEmail +import uniffi.wp_api.Subscriber @AndroidEntryPoint class SubscribersActivity : BaseAppCompatActivity() { private val viewModel by viewModels() + private lateinit var composeView: ComposeView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + composeView = ComposeView(this) setContentView( - ComposeView(this).apply { + composeView.apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.isForceDarkAllowed = false } setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - DataViewScreen( - title = getString(R.string.subscribers), - uiState = viewModel.uiState.collectAsState(), - items = viewModel.items.collectAsState(), - supportedFilters = viewModel.getSupportedFilters(), - currentFilter = viewModel.itemFilter.collectAsState().value, - errorMessage = viewModel.errorMessage.collectAsState().value, - onRefresh = { - viewModel.onRefreshData() - }, - onFetchMore = { - viewModel.onFetchMoreData() - }, - onSearchQueryChange = { query -> - viewModel.onSearchQueryChange(query) - }, - onItemClick = { item -> - viewModel.onItemClick(item) - }, - onFilterClick = { filter -> - viewModel.onFilterClick(filter) + NavigableContent() + } + } + ) + } + + private enum class SubscriberScreen { + List, + Detail + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun NavigableContent() { + val navController = rememberNavController() + val listTitle = stringResource(R.string.subscribers) + val titleState = remember { mutableStateOf(listTitle) } + AppThemeM3 { + Scaffold( + topBar = { + TopAppBar( + title = { Text(titleState.value) }, + navigationIcon = { + IconButton(onClick = { + if (navController.previousBackStackEntry != null) { + navController.navigateUp() + } else { + finish() + } + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) + } }, - onBackClick = { - finish() - } ) + }, + ) { contentPadding -> + NavHost( + navController = navController, + startDestination = SubscriberScreen.List.name + ) { + composable(route = SubscriberScreen.List.name) { + titleState.value = listTitle + DataViewScreen( + uiState = viewModel.uiState.collectAsState(), + items = viewModel.items.collectAsState(), + supportedFilters = viewModel.getSupportedFilters(), + currentFilter = viewModel.itemFilter.collectAsState().value, + errorMessage = viewModel.errorMessage.collectAsState().value, + onRefresh = { + viewModel.onRefreshData() + }, + onFetchMore = { + viewModel.onFetchMoreData() + }, + onSearchQueryChange = { query -> + viewModel.onSearchQueryChange(query) + }, + onItemClick = { item -> + viewModel.onItemClick(item) + (item.data as? Subscriber)?.let { subscriber -> + navController.currentBackStackEntry?.savedStateHandle?.set( + key = KEY_ID, + value = subscriber.userId + ) + navController.navigate(route = SubscriberScreen.Detail.name) + } + }, + onFilterClick = { filter -> + viewModel.onFilterClick(filter) + }, + modifier = Modifier.padding(contentPadding) + ) + } + + composable(route = SubscriberScreen.Detail.name) { + (navController.previousBackStackEntry?.savedStateHandle?.get(KEY_ID))?.let { userId -> + viewModel.getSubscriber(userId)?.let { subscriber -> + titleState.value = subscriber.displayNameOrEmail() + SubscriberDetailScreen( + subscriber = subscriber, + modifier = Modifier.padding(contentPadding) + ) + } + } + } } } - ) + } + } + + companion object { + private const val KEY_ID = "id" } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt index 8658ab0da65e..4033f3ea772e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt @@ -112,7 +112,7 @@ class SubscribersViewModel @Inject constructor( imageUrl = subscriber.avatar, fallbackImageRes = R.drawable.ic_user_placeholder_primary_24, ), - title = subscriber.displayName.ifEmpty { subscriber.emailAddress }, + title = subscriber.displayNameOrEmail(), fields = listOf( DataViewItemField( value = subscriber.subscriptionStatus, @@ -129,15 +129,21 @@ class SubscribersViewModel @Inject constructor( ) } + fun getSubscriber(userId: Long): Subscriber? { + val item = items.value.firstOrNull { it.id == userId } + return item?.data as? Subscriber + } + override fun onItemClick(item: DataViewItem) { - (item.data as? Subscriber)?.let{ subscriber -> - val name = subscriber.displayName.ifEmpty { subscriber.emailAddress } - appLogWrapper.d(AppLog.T.MAIN, "Clicked on subscriber $name") + (item.data as? Subscriber)?.let { subscriber -> + appLogWrapper.d(AppLog.T.MAIN, "Clicked on subscriber ${subscriber.displayNameOrEmail()}") } } companion object { private const val ID_FILTER_EMAIL = 1L private const val ID_FILTER_READER = 2L + + fun Subscriber.displayNameOrEmail() = displayName.ifEmpty { emailAddress } } } diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index f371665dc5c1..045fd62f02d9 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -4995,6 +4995,18 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> There are no subscribers No results found Something went wrong + Subscription status + Emails + Opened + Clicked + Newsletter subscription + Subscriber details + Subscription date + Plan + Email + Country + Site + Delete subscriber Application password credentials stored for %1$s