From 11804b265fe6a8f9c06935d44625119d5687f931 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 13:50:18 -0400 Subject: [PATCH 01/37] Created shell for adding subscribers --- .../ui/subscribers/SubscribersActivity.kt | 16 ++++++++++++ .../ui/subscribers/SubscribersViewModel.kt | 26 +++++++++++++++++++ WordPress/src/main/res/values/strings.xml | 1 + 3 files changed, 43 insertions(+) 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 61e735fbc777..730cbbe8fdee 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 @@ -6,6 +6,7 @@ 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.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -57,6 +58,7 @@ class SubscribersActivity : BaseAppCompatActivity() { List, Detail, Plan, + AddSubscribers } @OptIn(ExperimentalMaterial3Api::class) @@ -81,6 +83,16 @@ class SubscribersActivity : BaseAppCompatActivity() { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) } }, + actions = { + IconButton(onClick = { + navController.navigate(route = SubscriberScreen.AddSubscribers.name) + }) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.subscribers_add_subscribers) + ) + } + } ) }, ) { contentPadding -> @@ -180,6 +192,10 @@ class SubscribersActivity : BaseAppCompatActivity() { } } } + + composable(route = SubscriberScreen.AddSubscribers.name) { + titleState.value = stringResource(R.string.subscribers_add_subscribers) + } } } } 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 214d29c7f63b..229ab3147bfd 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 @@ -18,6 +18,7 @@ import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.AddSubscribersParams import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams import uniffi.wp_api.ListSubscribersSortField @@ -180,6 +181,31 @@ class SubscribersViewModel @Inject constructor( ) } + suspend fun addSubscriber(email: String, displayName: String): Boolean = withContext(ioDispatcher) { + val params = AddSubscribersParams( + emails = listOf(email), + ) + + val response = wpComApiClient.request { requestBuilder -> + requestBuilder.subscribers().addSubscribers( + wpComSiteId = siteId().toULong(), + params = params + ) + } + + when (response) { + is WpRequestResult.Success -> { + appLogWrapper.d(AppLog.T.MAIN, "Successfully added subscriber: $email") + return@withContext true + } + else -> { + onError((response as? WpRequestResult.WpError)?.errorMessage) + appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") + return@withContext false + } + } + } + /* * Returns the subscriber with the given ID, or null if not found. Note that this does NOT do a network call, * it simply returns the subscriber from the existing list of items. diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index bbc9c695d777..1bbb27c0ed08 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5024,6 +5024,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Renewal information Renewal interval Gift + Add subscribers Application password credentials stored for %1$s From 49a1656eb7d214f83fa83f859c61608ca4b3dd35 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 14:56:20 -0400 Subject: [PATCH 02/37] Fleshed out shell for adding subscribers --- .../android/ui/subscribers/SubscribersActivity.kt | 14 ++++++++++++++ .../android/ui/subscribers/SubscribersViewModel.kt | 6 +++--- WordPress/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) 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 730cbbe8fdee..1378b1cb93ed 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 @@ -21,10 +21,12 @@ 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.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController 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.compose.theme.AppThemeM3 @@ -195,6 +197,18 @@ class SubscribersActivity : BaseAppCompatActivity() { composable(route = SubscriberScreen.AddSubscribers.name) { titleState.value = stringResource(R.string.subscribers_add_subscribers) + AddSubscribersScreen( + onSubmit = { emails -> + lifecycleScope.launch { + val result = viewModel.addSubscribers(emails) + if (result) { + navController.navigateUp() + } + } + }, + onCancel = { navController.navigateUp() }, + modifier = Modifier.padding(contentPadding) + ) } } } 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 229ab3147bfd..654399697667 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 @@ -181,9 +181,9 @@ class SubscribersViewModel @Inject constructor( ) } - suspend fun addSubscriber(email: String, displayName: String): Boolean = withContext(ioDispatcher) { + suspend fun addSubscribers(emails: List): Boolean = withContext(ioDispatcher) { val params = AddSubscribersParams( - emails = listOf(email), + emails = emails ) val response = wpComApiClient.request { requestBuilder -> @@ -195,7 +195,7 @@ class SubscribersViewModel @Inject constructor( when (response) { is WpRequestResult.Success -> { - appLogWrapper.d(AppLog.T.MAIN, "Successfully added subscriber: $email") + appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") return@withContext true } else -> { diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 1bbb27c0ed08..127e384727d9 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5025,6 +5025,10 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Renewal interval Gift Add subscribers + name@example.com + + By clicking Send you represent that you\'ve obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. + Application password credentials stored for %1$s From 91118dc54473f9a1a4ae3005d7f368ff7f4ed525 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 14:56:32 -0400 Subject: [PATCH 03/37] Fleshed out shell for adding subscribers (again) --- .../ui/subscribers/AddSubscribersScreen.kt | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt new file mode 100644 index 000000000000..793936759b63 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -0,0 +1,129 @@ +package org.wordpress.android.ui.subscribers + +import android.content.res.Configuration +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.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppThemeM3 +import org.wordpress.android.util.validateEmail + +@Composable +fun AddSubscribersScreen( + onSubmit: (List) -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + var entry = "" + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + Row( + modifier = modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = "", + onValueChange = { + entry = it + }, + label = { + Text(stringResource(id = R.string.subscribers_add_email_hint)) + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + if (isValidEntry(entry)) { + onSubmit(parseEntry(entry)) + } + } + ), + modifier = modifier.fillMaxWidth() + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.subscribers_add_disclosure), + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedButton( + onClick = onCancel, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(id = android.R.string.cancel)) + } + + Button( + onClick = { onSubmit(parseEntry(entry)) }, + enabled = entry.isNotEmpty(), + modifier = Modifier.weight(1f) + ) { + Text(stringResource(id = R.string.send)) + } + } + } +} + +private fun isValidEntry(entry: String): Boolean { + parseEntry(entry).forEach { + if (!validateEmail(it)) { + return false + } + } + return true +} + +private fun parseEntry(entry: String): List { + return entry.split(",").map { it.trim() } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AddSubscribersScreenPreview() { + AppThemeM3 { + AddSubscribersScreen( + onSubmit = {}, + onCancel = {} + ) + } +} From a7db1f4c7908b2617c735e596027930abb3f78ac Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 15:09:07 -0400 Subject: [PATCH 04/37] Fixed broken text field --- .../android/ui/subscribers/AddSubscribersScreen.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 793936759b63..363fdd403308 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -20,6 +20,10 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -35,7 +39,8 @@ fun AddSubscribersScreen( onCancel: () -> Unit, modifier: Modifier = Modifier, ) { - var entry = "" + var entry by remember { mutableStateOf("") } + Column( modifier = modifier .fillMaxSize() @@ -47,7 +52,7 @@ fun AddSubscribersScreen( modifier = modifier.fillMaxWidth() ) { OutlinedTextField( - value = "", + value = entry, onValueChange = { entry = it }, From b1f58b34e89edec039b3c0b8979d040e1e746be5 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 15:23:26 -0400 Subject: [PATCH 05/37] Changed result type of addSubscribers fun --- .../android/ui/subscribers/AddSubscribersScreen.kt | 8 ++++---- .../android/ui/subscribers/SubscribersActivity.kt | 14 +++++++++++++- .../android/ui/subscribers/SubscribersViewModel.kt | 7 ++++--- WordPress/src/main/res/values/strings.xml | 2 ++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 363fdd403308..5d6da1376a6e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -40,6 +40,7 @@ fun AddSubscribersScreen( modifier: Modifier = Modifier, ) { var entry by remember { mutableStateOf("") } + var isValidEntry by remember { mutableStateOf(false) } Column( modifier = modifier @@ -55,6 +56,7 @@ fun AddSubscribersScreen( value = entry, onValueChange = { entry = it + isValidEntry = isValidEntry(entry) }, label = { Text(stringResource(id = R.string.subscribers_add_email_hint)) @@ -64,9 +66,7 @@ fun AddSubscribersScreen( ), keyboardActions = KeyboardActions( onDone = { - if (isValidEntry(entry)) { - onSubmit(parseEntry(entry)) - } + onSubmit(parseEntry(entry)) } ), modifier = modifier.fillMaxWidth() @@ -99,7 +99,7 @@ fun AddSubscribersScreen( Button( onClick = { onSubmit(parseEntry(entry)) }, - enabled = entry.isNotEmpty(), + enabled = isValidEntry, modifier = Modifier.weight(1f) ) { Text(stringResource(id = R.string.send)) 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 1378b1cb93ed..bb59ace28323 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 @@ -2,6 +2,7 @@ package org.wordpress.android.ui.subscribers import android.os.Build import android.os.Bundle +import android.widget.Toast import androidx.activity.viewModels import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -201,8 +202,19 @@ class SubscribersActivity : BaseAppCompatActivity() { onSubmit = { emails -> lifecycleScope.launch { val result = viewModel.addSubscribers(emails) - if (result) { + if (result.isSuccess) { + Toast.makeText( + this@SubscribersActivity, + getString(R.string.subscribers_add_success), + Toast.LENGTH_SHORT + ).show() navController.navigateUp() + } else { + Toast.makeText( + this@SubscribersActivity, + getString(R.string.subscribers_add_failed), + Toast.LENGTH_LONG + ).show() } } }, 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 654399697667..ca084a69c0f4 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 @@ -181,7 +181,7 @@ class SubscribersViewModel @Inject constructor( ) } - suspend fun addSubscribers(emails: List): Boolean = withContext(ioDispatcher) { + suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { val params = AddSubscribersParams( emails = emails ) @@ -196,12 +196,13 @@ class SubscribersViewModel @Inject constructor( when (response) { is WpRequestResult.Success -> { appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") - return@withContext true + return@withContext Result.success(true) } else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage onError((response as? WpRequestResult.WpError)?.errorMessage) appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") - return@withContext false + return@withContext Result.failure(Exception(error)) } } } diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 127e384727d9..339a494b12f8 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5029,6 +5029,8 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> By clicking Send you represent that you\'ve obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. + Successfully added subscribers + Failed to add subscribers Application password credentials stored for %1$s From 3bff7bcf9d7cb36428ce0f34dbaff5c452411593 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 2 Jul 2025 16:15:16 -0400 Subject: [PATCH 06/37] Added progress --- .../ui/subscribers/AddSubscribersScreen.kt | 48 +++++++++++++------ .../ui/subscribers/SubscribersActivity.kt | 10 +++- .../ui/subscribers/SubscribersViewModel.kt | 41 +++++++++------- WordPress/src/main/res/values/strings.xml | 3 +- 4 files changed, 68 insertions(+), 34 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 5d6da1376a6e..012586db8ac1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.subscribers import android.content.res.Configuration 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 @@ -10,20 +11,24 @@ 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.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -37,6 +42,7 @@ import org.wordpress.android.util.validateEmail fun AddSubscribersScreen( onSubmit: (List) -> Unit, onCancel: () -> Unit, + showProgress: State, modifier: Modifier = Modifier, ) { var entry by remember { mutableStateOf("") } @@ -86,23 +92,34 @@ fun AddSubscribersScreen( Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - OutlinedButton( - onClick = onCancel, - modifier = Modifier.weight(1f) + if (showProgress.value) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center ) { - Text(stringResource(id = android.R.string.cancel)) + CircularProgressIndicator( + modifier = Modifier.size(48.dp) + ) } - - Button( - onClick = { onSubmit(parseEntry(entry)) }, - enabled = isValidEntry, - modifier = Modifier.weight(1f) + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Text(stringResource(id = R.string.send)) + OutlinedButton( + onClick = onCancel, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(id = android.R.string.cancel)) + } + + Button( + onClick = { onSubmit(parseEntry(entry)) }, + enabled = isValidEntry, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(id = R.string.send)) + } } } } @@ -128,7 +145,8 @@ private fun AddSubscribersScreenPreview() { AppThemeM3 { AddSubscribersScreen( onSubmit = {}, - onCancel = {} + onCancel = {}, + showProgress = remember { mutableStateOf(false) } ) } } 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 bb59ace28323..4d32bd16eb87 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 @@ -203,10 +203,15 @@ class SubscribersActivity : BaseAppCompatActivity() { lifecycleScope.launch { val result = viewModel.addSubscribers(emails) if (result.isSuccess) { + val toastMsg = if (emails.size > 1) { + getString(R.string.subscribers_add_success_plural, emails.size) + } else { + getString(R.string.subscribers_add_success_singular) + } Toast.makeText( this@SubscribersActivity, - getString(R.string.subscribers_add_success), - Toast.LENGTH_SHORT + toastMsg, + Toast.LENGTH_LONG ).show() navController.navigateUp() } else { @@ -219,6 +224,7 @@ class SubscribersActivity : BaseAppCompatActivity() { } }, onCancel = { navController.navigateUp() }, + showProgress = viewModel.showAddSubscribersProgress.collectAsState(), modifier = Modifier.padding(contentPadding) ) } 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 ca084a69c0f4..0a08befc26e8 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 @@ -40,6 +40,9 @@ class SubscribersViewModel @Inject constructor( private val _subscriberStats = MutableStateFlow(null) val subscriberStats = _subscriberStats.asStateFlow() + private val _showAddSubscribersProgress = MutableStateFlow(false) + val showAddSubscribersProgress = _showAddSubscribersProgress.asStateFlow() + private var statsJob: Job? = null @Inject @@ -186,24 +189,30 @@ class SubscribersViewModel @Inject constructor( emails = emails ) - val response = wpComApiClient.request { requestBuilder -> - requestBuilder.subscribers().addSubscribers( - wpComSiteId = siteId().toULong(), - params = params - ) - } - - when (response) { - is WpRequestResult.Success -> { - appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") - return@withContext Result.success(true) + _showAddSubscribersProgress.value = true + try { + val response = wpComApiClient.request { requestBuilder -> + requestBuilder.subscribers().addSubscribers( + wpComSiteId = siteId().toULong(), + params = params + ) } - else -> { - val error = (response as? WpRequestResult.WpError)?.errorMessage - onError((response as? WpRequestResult.WpError)?.errorMessage) - appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") - return@withContext Result.failure(Exception(error)) + + when (response) { + is WpRequestResult.Success -> { + appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") + return@withContext Result.success(true) + } + + else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage + onError((response as? WpRequestResult.WpError)?.errorMessage) + appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") + return@withContext Result.failure(Exception(error)) + } } + } finally { + _showAddSubscribersProgress.value = false } } diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 339a494b12f8..be09a5945b56 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5029,7 +5029,8 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> By clicking Send you represent that you\'ve obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. - Successfully added subscribers + Successfully added subscriber + Successfully added %d subscribers Failed to add subscribers From 318f1d9b3dba51d67674c2f51fd283d4edfa92f7 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 06:54:24 -0400 Subject: [PATCH 07/37] Simplified layout --- .../ui/subscribers/AddSubscribersScreen.kt | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 012586db8ac1..96b62957a247 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -55,42 +55,34 @@ fun AddSubscribersScreen( .padding(16.dp) .verticalScroll(rememberScrollState()) ) { - Row( + OutlinedTextField( + value = entry, + onValueChange = { + entry = it + isValidEntry = isValidEntry(entry) + }, + label = { + Text(stringResource(id = R.string.subscribers_add_email_hint)) + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + onSubmit(parseEntry(entry)) + } + ), modifier = modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = entry, - onValueChange = { - entry = it - isValidEntry = isValidEntry(entry) - }, - label = { - Text(stringResource(id = R.string.subscribers_add_email_hint)) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - onSubmit(parseEntry(entry)) - } - ), - modifier = modifier.fillMaxWidth() - ) - } + ) Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier.fillMaxWidth() - ) { - Text( - text = stringResource(id = R.string.subscribers_add_disclosure), - style = MaterialTheme.typography.bodyMedium - ) - } + Text( + text = stringResource(id = R.string.subscribers_add_disclosure), + style = MaterialTheme.typography.bodyMedium + ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) if (showProgress.value) { Box( From c09b9776f6b95653a169698180cfbbabeb205948 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 07:01:17 -0400 Subject: [PATCH 08/37] Fixed positioning in layout --- .../android/ui/subscribers/AddSubscribersScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 96b62957a247..ec307a4fb2ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -7,7 +7,6 @@ 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 @@ -50,7 +49,7 @@ fun AddSubscribersScreen( Column( modifier = modifier - .fillMaxSize() + .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(16.dp) .verticalScroll(rememberScrollState()) @@ -72,7 +71,8 @@ fun AddSubscribersScreen( onSubmit(parseEntry(entry)) } ), - modifier = modifier.fillMaxWidth() + singleLine = false, + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) From d18c101a881fb7740b0e40b2a0b50257c5749127 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 07:16:16 -0400 Subject: [PATCH 09/37] Don't use onError when adding subscribers --- .../android/ui/subscribers/AddSubscribersScreen.kt | 2 +- .../android/ui/subscribers/SubscribersViewModel.kt | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index ec307a4fb2ae..48234d655cb8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -120,7 +120,7 @@ fun AddSubscribersScreen( private fun isValidEntry(entry: String): Boolean { parseEntry(entry).forEach { if (!validateEmail(it)) { - return false + return true } } return true 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 0a08befc26e8..95778a31de8b 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 @@ -200,13 +200,19 @@ class SubscribersViewModel @Inject constructor( when (response) { is WpRequestResult.Success -> { - appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") - return@withContext Result.success(true) + // the backend may return HTTP 200 even when no subscribers were added, so verify there's + // a valid uploadId before assuming success + if (response.response.data.uploadId == 0.toULong()) { + appLogWrapper.d(AppLog.T.MAIN, "No subscribers added") + return@withContext Result.failure(Exception("No subscribers added")) + } else { + appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") + return@withContext Result.success(true) + } } else -> { val error = (response as? WpRequestResult.WpError)?.errorMessage - onError((response as? WpRequestResult.WpError)?.errorMessage) appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") return@withContext Result.failure(Exception(error)) } From 92d5e0b1c9eb6da0451f647e5fc7f8c45afddc7c Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 07:25:33 -0400 Subject: [PATCH 10/37] Separate screens in SubscribersActivity --- .../ui/subscribers/SubscribersActivity.kt | 199 +++++++++++------- 1 file changed, 118 insertions(+), 81 deletions(-) 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 4d32bd16eb87..a7f19f75bafe 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 @@ -23,6 +23,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -70,6 +71,7 @@ class SubscribersActivity : BaseAppCompatActivity() { val navController = rememberNavController() val listTitle = stringResource(R.string.subscribers) val titleState = remember { mutableStateOf(listTitle) } + AppThemeM3 { Scaffold( topBar = { @@ -105,39 +107,8 @@ class SubscribersActivity : BaseAppCompatActivity() { ) { 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, - supportedSorts = viewModel.getSupportedSorts(), - currentSort = viewModel.itemSortBy.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_USER_ID, - value = subscriber.userId - ) - navController.navigate(route = SubscriberScreen.Detail.name) - } - }, - onFilterClick = { filter -> - viewModel.onFilterClick(filter) - }, - onSortClick = { sort -> - viewModel.onSortClick(sort) - }, + ShowListScreen( + navController, modifier = Modifier.padding(contentPadding) ) } @@ -148,28 +119,10 @@ class SubscribersActivity : BaseAppCompatActivity() { if (userId != null) { viewModel.getSubscriber(userId)?.let { subscriber -> titleState.value = subscriber.displayNameOrEmail() - SubscriberDetailScreen( + ShowSubscriberDetailScreen( subscriber = subscriber, - onEmailClick = { email -> - onEmailClick(email) - }, - onUrlClick = { url -> - onUrlClick(url) - }, - onPlanClick = { planIndex -> - navController.currentBackStackEntry?.savedStateHandle?.set( - key = KEY_USER_ID, - value = userId - ) - // plans don't have a unique id, so we use the index to identify them - navController.currentBackStackEntry?.savedStateHandle?.set( - key = KEY_PLAN_INDEX, - value = planIndex - ) - navController.navigate(route = SubscriberScreen.Plan.name) - }, - modifier = Modifier.padding(contentPadding), - subscriberStats = viewModel.subscriberStats.collectAsState() + navController = navController, + modifier = Modifier.padding(contentPadding) ) } } @@ -198,33 +151,8 @@ class SubscribersActivity : BaseAppCompatActivity() { composable(route = SubscriberScreen.AddSubscribers.name) { titleState.value = stringResource(R.string.subscribers_add_subscribers) - AddSubscribersScreen( - onSubmit = { emails -> - lifecycleScope.launch { - val result = viewModel.addSubscribers(emails) - if (result.isSuccess) { - val toastMsg = if (emails.size > 1) { - getString(R.string.subscribers_add_success_plural, emails.size) - } else { - getString(R.string.subscribers_add_success_singular) - } - Toast.makeText( - this@SubscribersActivity, - toastMsg, - Toast.LENGTH_LONG - ).show() - navController.navigateUp() - } else { - Toast.makeText( - this@SubscribersActivity, - getString(R.string.subscribers_add_failed), - Toast.LENGTH_LONG - ).show() - } - } - }, - onCancel = { navController.navigateUp() }, - showProgress = viewModel.showAddSubscribersProgress.collectAsState(), + ShowAddSubscribersScreen( + navController = navController, modifier = Modifier.padding(contentPadding) ) } @@ -233,6 +161,115 @@ class SubscribersActivity : BaseAppCompatActivity() { } } + @Composable + private fun ShowListScreen( + navController: NavHostController, + modifier: Modifier + ) { + DataViewScreen( + uiState = viewModel.uiState.collectAsState(), + items = viewModel.items.collectAsState(), + supportedFilters = viewModel.getSupportedFilters(), + currentFilter = viewModel.itemFilter.collectAsState().value, + supportedSorts = viewModel.getSupportedSorts(), + currentSort = viewModel.itemSortBy.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_USER_ID, + value = subscriber.userId + ) + navController.navigate(route = SubscriberScreen.Detail.name) + } + }, + onFilterClick = { filter -> + viewModel.onFilterClick(filter) + }, + onSortClick = { sort -> + viewModel.onSortClick(sort) + }, + modifier = modifier + ) + } + + @Composable + private fun ShowSubscriberDetailScreen( + subscriber: Subscriber, + navController: NavHostController, + modifier: Modifier + ) { + SubscriberDetailScreen( + subscriber = subscriber, + onEmailClick = { email -> + onEmailClick(email) + }, + onUrlClick = { url -> + onUrlClick(url) + }, + onPlanClick = { planIndex -> + navController.currentBackStackEntry?.savedStateHandle?.set( + key = KEY_USER_ID, + value = subscriber.userId + ) + // plans don't have a unique id, so we use the index to identify them + navController.currentBackStackEntry?.savedStateHandle?.set( + key = KEY_PLAN_INDEX, + value = planIndex + ) + navController.navigate(route = SubscriberScreen.Plan.name) + }, + modifier = modifier, + subscriberStats = viewModel.subscriberStats.collectAsState() + ) + } + + @Composable + private fun ShowAddSubscribersScreen( + navController: NavHostController, + modifier: Modifier + ) { + AddSubscribersScreen( + onSubmit = { emails -> + lifecycleScope.launch { + val result = viewModel.addSubscribers(emails) + if (result.isSuccess) { + val toastMsg = if (emails.size > 1) { + getString(R.string.subscribers_add_success_plural, emails.size) + } else { + getString(R.string.subscribers_add_success_singular) + } + Toast.makeText( + this@SubscribersActivity, + toastMsg, + Toast.LENGTH_LONG + ).show() + navController.navigateUp() + } else { + Toast.makeText( + this@SubscribersActivity, + getString(R.string.subscribers_add_failed), + Toast.LENGTH_LONG + ).show() + } + } + }, + onCancel = { navController.navigateUp() }, + showProgress = viewModel.showAddSubscribersProgress.collectAsState(), + modifier = modifier + ) + } + private fun onEmailClick(email: String) { ActivityLauncher.openUrlExternal(this, "mailto:$email") } From e4b00686876123a14c09c6f20af47aa49a210023 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 08:00:24 -0400 Subject: [PATCH 11/37] Moved adding subscribers to its own view model --- .../ui/subscribers/AddSubscribersScreen.kt | 2 +- .../ui/subscribers/AddSubscribersViewModel.kt | 90 +++++++++++++++++++ .../ui/subscribers/SubscribersActivity.kt | 31 ++++--- .../ui/subscribers/SubscribersViewModel.kt | 42 --------- 4 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index 48234d655cb8..ec307a4fb2ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -120,7 +120,7 @@ fun AddSubscribersScreen( private fun isValidEntry(entry: String): Boolean { parseEntry(entry).forEach { if (!validateEmail(it)) { - return true + return false } } return true diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt new file mode 100644 index 000000000000..c450f312e3bf --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt @@ -0,0 +1,90 @@ +package org.wordpress.android.ui.subscribers + +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.IO_THREAD +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.AppLog +import org.wordpress.android.viewmodel.ScopedViewModel +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.AddSubscribersParams +import uniffi.wp_api.WpAuthentication +import uniffi.wp_api.WpAuthenticationProvider +import javax.inject.Inject +import javax.inject.Named + +@HiltViewModel +class AddSubscribersViewModel @Inject constructor( + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + private val appLogWrapper: AppLogWrapper, +) : ScopedViewModel(bgDispatcher) { + @Inject + @Named(IO_THREAD) + lateinit var ioDispatcher: CoroutineDispatcher + + @Inject + lateinit var selectedSiteRepository: SelectedSiteRepository + + @Inject + lateinit var accountStore: AccountStore + + private val _showProgress = MutableStateFlow(false) + val showProgress = _showProgress.asStateFlow() + + private val wpComApiClient: WpComApiClient by lazy { + WpComApiClient( + WpAuthenticationProvider.staticWithAuth( + WpAuthentication.Bearer(token = accountStore.accessToken!!) + ) + ) + } + + private fun siteId(): Long { + return selectedSiteRepository.getSelectedSite()?.siteId ?: 0L + } + + suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { + val params = AddSubscribersParams( + emails = emails + ) + + _showProgress.value = true + try { + val response = wpComApiClient.request { requestBuilder -> + requestBuilder.subscribers().addSubscribers( + wpComSiteId = siteId().toULong(), + params = params + ) + } + + when (response) { + is WpRequestResult.Success -> { + // the backend may return HTTP 200 even when no subscribers were added, so verify there's + // a valid uploadId before assuming success + if (response.response.data.uploadId == 0.toULong()) { + appLogWrapper.d(AppLog.T.MAIN, "No subscribers added") + return@withContext Result.failure(Exception("No subscribers added")) + } else { + appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") + return@withContext Result.success(true) + } + } + + else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage + appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") + return@withContext Result.failure(Exception(error)) + } + } + } finally { + _showProgress.value = false + } + } +} 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 a7f19f75bafe..30170d98752c 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 @@ -40,6 +40,8 @@ import uniffi.wp_api.Subscriber @AndroidEntryPoint class SubscribersActivity : BaseAppCompatActivity() { private val viewModel by viewModels() + private val addSubscribersViewModel by viewModels() + private lateinit var composeView: ComposeView override fun onCreate(savedInstanceState: Bundle?) { @@ -71,6 +73,7 @@ class SubscribersActivity : BaseAppCompatActivity() { val navController = rememberNavController() val listTitle = stringResource(R.string.subscribers) val titleState = remember { mutableStateOf(listTitle) } + val showAddSubscribersButtonState = remember { mutableStateOf(true) } AppThemeM3 { Scaffold( @@ -89,13 +92,15 @@ class SubscribersActivity : BaseAppCompatActivity() { } }, actions = { - IconButton(onClick = { - navController.navigate(route = SubscriberScreen.AddSubscribers.name) - }) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(R.string.subscribers_add_subscribers) - ) + if (showAddSubscribersButtonState.value) { + IconButton(onClick = { + navController.navigate(route = SubscriberScreen.AddSubscribers.name) + }) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.subscribers_add_subscribers) + ) + } } } ) @@ -107,6 +112,7 @@ class SubscribersActivity : BaseAppCompatActivity() { ) { composable(route = SubscriberScreen.List.name) { titleState.value = listTitle + showAddSubscribersButtonState.value = true ShowListScreen( navController, modifier = Modifier.padding(contentPadding) @@ -119,7 +125,8 @@ class SubscribersActivity : BaseAppCompatActivity() { if (userId != null) { viewModel.getSubscriber(userId)?.let { subscriber -> titleState.value = subscriber.displayNameOrEmail() - ShowSubscriberDetailScreen( + showAddSubscribersButtonState.value = false + ShowSubscriberDetailScreen( subscriber = subscriber, navController = navController, modifier = Modifier.padding(contentPadding) @@ -138,6 +145,7 @@ class SubscribersActivity : BaseAppCompatActivity() { subscriber.plans?.let { plans -> if (planIndex in plans.indices) { titleState.value = plans[planIndex].title + showAddSubscribersButtonState.value = false SubscriberPlanScreen( plan = plans[planIndex], modifier = Modifier.padding(contentPadding) @@ -151,7 +159,8 @@ class SubscribersActivity : BaseAppCompatActivity() { composable(route = SubscriberScreen.AddSubscribers.name) { titleState.value = stringResource(R.string.subscribers_add_subscribers) - ShowAddSubscribersScreen( + showAddSubscribersButtonState.value = false + ShowAddSubscribersScreen( navController = navController, modifier = Modifier.padding(contentPadding) ) @@ -242,7 +251,7 @@ class SubscribersActivity : BaseAppCompatActivity() { AddSubscribersScreen( onSubmit = { emails -> lifecycleScope.launch { - val result = viewModel.addSubscribers(emails) + val result = addSubscribersViewModel.addSubscribers(emails) if (result.isSuccess) { val toastMsg = if (emails.size > 1) { getString(R.string.subscribers_add_success_plural, emails.size) @@ -265,7 +274,7 @@ class SubscribersActivity : BaseAppCompatActivity() { } }, onCancel = { navController.navigateUp() }, - showProgress = viewModel.showAddSubscribersProgress.collectAsState(), + showProgress = addSubscribersViewModel.showProgress.collectAsState(), modifier = modifier ) } 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 95778a31de8b..214d29c7f63b 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 @@ -18,7 +18,6 @@ import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog import rs.wordpress.api.kotlin.WpRequestResult -import uniffi.wp_api.AddSubscribersParams import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams import uniffi.wp_api.ListSubscribersSortField @@ -40,9 +39,6 @@ class SubscribersViewModel @Inject constructor( private val _subscriberStats = MutableStateFlow(null) val subscriberStats = _subscriberStats.asStateFlow() - private val _showAddSubscribersProgress = MutableStateFlow(false) - val showAddSubscribersProgress = _showAddSubscribersProgress.asStateFlow() - private var statsJob: Job? = null @Inject @@ -184,44 +180,6 @@ class SubscribersViewModel @Inject constructor( ) } - suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { - val params = AddSubscribersParams( - emails = emails - ) - - _showAddSubscribersProgress.value = true - try { - val response = wpComApiClient.request { requestBuilder -> - requestBuilder.subscribers().addSubscribers( - wpComSiteId = siteId().toULong(), - params = params - ) - } - - when (response) { - is WpRequestResult.Success -> { - // the backend may return HTTP 200 even when no subscribers were added, so verify there's - // a valid uploadId before assuming success - if (response.response.data.uploadId == 0.toULong()) { - appLogWrapper.d(AppLog.T.MAIN, "No subscribers added") - return@withContext Result.failure(Exception("No subscribers added")) - } else { - appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") - return@withContext Result.success(true) - } - } - - else -> { - val error = (response as? WpRequestResult.WpError)?.errorMessage - appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") - return@withContext Result.failure(Exception(error)) - } - } - } finally { - _showAddSubscribersProgress.value = false - } - } - /* * Returns the subscriber with the given ID, or null if not found. Note that this does NOT do a network call, * it simply returns the subscriber from the existing list of items. From 6abbd6650c5f18aa0327a1620908186e3af2a3b0 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 08:58:40 -0400 Subject: [PATCH 12/37] Update WordPress/src/main/res/values/strings.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- WordPress/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index be09a5945b56..ce015bdb17da 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5027,7 +5027,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Add subscribers name@example.com - By clicking Send you represent that you\'ve obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. + By clicking Send you represent that you've obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. Successfully added subscriber Successfully added %d subscribers From 50511d51d4d40be59bf8afbba79de21903ef9d2a Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 08:58:52 -0400 Subject: [PATCH 13/37] Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../wordpress/android/ui/subscribers/AddSubscribersScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index ec307a4fb2ae..ff815daf08d0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -68,7 +68,9 @@ fun AddSubscribersScreen( ), keyboardActions = KeyboardActions( onDone = { - onSubmit(parseEntry(entry)) + if (isValidEntry) { + onSubmit(parseEntry(entry)) + } } ), singleLine = false, From 56da02e710956f0e861f8672e3ef69457ce61556 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 08:59:06 -0400 Subject: [PATCH 14/37] Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../wordpress/android/ui/subscribers/AddSubscribersScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt index ff815daf08d0..27fc6c8b1dbb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt @@ -129,7 +129,7 @@ private fun isValidEntry(entry: String): Boolean { } private fun parseEntry(entry: String): List { - return entry.split(",").map { it.trim() } + return entry.split(",").map { it.trim() }.filter { it.isNotEmpty() } } @Preview From d8fb9b4f38a5b4b9d5707581ddd409bb3ad0d665 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 09:31:06 -0400 Subject: [PATCH 15/37] Let view model handle success/failure --- .../ui/subscribers/AddSubscribersViewModel.kt | 25 ++++++++++++++- .../ui/subscribers/SubscribersActivity.kt | 31 ++++--------------- WordPress/src/main/res/values/strings.xml | 5 ++- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt index c450f312e3bf..5377b14794f7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt @@ -4,13 +4,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.wordpress.android.R import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.IO_THREAD +import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.util.AppLog +import org.wordpress.android.util.ToastUtilsWrapper import org.wordpress.android.viewmodel.ScopedViewModel import rs.wordpress.api.kotlin.WpComApiClient import rs.wordpress.api.kotlin.WpRequestResult @@ -22,8 +26,10 @@ import javax.inject.Named @HiltViewModel class AddSubscribersViewModel @Inject constructor( + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val appLogWrapper: AppLogWrapper, + private val toastUtilsWrapper: ToastUtilsWrapper, ) : ScopedViewModel(bgDispatcher) { @Inject @Named(IO_THREAD) @@ -50,7 +56,24 @@ class AddSubscribersViewModel @Inject constructor( return selectedSiteRepository.getSelectedSite()?.siteId ?: 0L } - suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { + fun onSubmitClick( + emails: List, + onSuccess: () -> Unit + ) { + launch(bgDispatcher) { + val result = addSubscribers(emails) + launch(mainDispatcher) { + if (result.isSuccess) { + toastUtilsWrapper.showToast(R.string.subscribers_add_success) + onSuccess() + } else { + toastUtilsWrapper.showToast(R.string.subscribers_add_failed) + } + } + } + } + + private suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { val params = AddSubscribersParams( emails = emails ) 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 30170d98752c..15736e84bd82 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 @@ -2,7 +2,6 @@ package org.wordpress.android.ui.subscribers import android.os.Build import android.os.Bundle -import android.widget.Toast import androidx.activity.viewModels import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -22,13 +21,11 @@ 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.lifecycle.lifecycleScope import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController 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.compose.theme.AppThemeM3 @@ -126,7 +123,7 @@ class SubscribersActivity : BaseAppCompatActivity() { viewModel.getSubscriber(userId)?.let { subscriber -> titleState.value = subscriber.displayNameOrEmail() showAddSubscribersButtonState.value = false - ShowSubscriberDetailScreen( + ShowSubscriberDetailScreen( subscriber = subscriber, navController = navController, modifier = Modifier.padding(contentPadding) @@ -160,7 +157,7 @@ class SubscribersActivity : BaseAppCompatActivity() { composable(route = SubscriberScreen.AddSubscribers.name) { titleState.value = stringResource(R.string.subscribers_add_subscribers) showAddSubscribersButtonState.value = false - ShowAddSubscribersScreen( + ShowAddSubscribersScreen( navController = navController, modifier = Modifier.padding(contentPadding) ) @@ -250,28 +247,12 @@ class SubscribersActivity : BaseAppCompatActivity() { ) { AddSubscribersScreen( onSubmit = { emails -> - lifecycleScope.launch { - val result = addSubscribersViewModel.addSubscribers(emails) - if (result.isSuccess) { - val toastMsg = if (emails.size > 1) { - getString(R.string.subscribers_add_success_plural, emails.size) - } else { - getString(R.string.subscribers_add_success_singular) - } - Toast.makeText( - this@SubscribersActivity, - toastMsg, - Toast.LENGTH_LONG - ).show() + addSubscribersViewModel.onSubmitClick( + emails = emails, + onSuccess = { navController.navigateUp() - } else { - Toast.makeText( - this@SubscribersActivity, - getString(R.string.subscribers_add_failed), - Toast.LENGTH_LONG - ).show() } - } + ) }, onCancel = { navController.navigateUp() }, showProgress = addSubscribersViewModel.showProgress.collectAsState(), diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index ce015bdb17da..339a494b12f8 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5027,10 +5027,9 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Add subscribers name@example.com - By clicking Send you represent that you've obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. + By clicking Send you represent that you\'ve obtained the appropriate consent to email each person. Spam complaints or high bounce rate from your subscribers may lead to action against your account. - Successfully added subscriber - Successfully added %d subscribers + Successfully added subscribers Failed to add subscribers From b9f58f777e1989ec823236c35d2d6b7aec41aec0 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 10:48:46 -0400 Subject: [PATCH 16/37] First pass at deleting subscriber --- .../ui/subscribers/SubscriberDetailScreen.kt | 18 +++-- .../ui/subscribers/SubscribersActivity.kt | 3 + .../ui/subscribers/SubscribersViewModel.kt | 67 ++++++++++++++++++- WordPress/src/main/res/values/strings.xml | 4 ++ gradle/libs.versions.toml | 2 +- 5 files changed, 88 insertions(+), 6 deletions(-) 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 index 42a0cd6f8f44..efc7e44e4ffb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt @@ -57,6 +57,7 @@ fun SubscriberDetailScreen( onUrlClick: (String) -> Unit, onEmailClick: (String) -> Unit, onPlanClick: (index: Int) -> Unit, + onDeleteClick: (subscriber: Subscriber) -> Unit, modifier: Modifier = Modifier, subscriberStats: State? = null ) { @@ -92,7 +93,11 @@ fun SubscriberDetailScreen( Spacer(modifier = Modifier.height(32.dp)) - DeleteSubscriberButton() + DeleteSubscriberButton( + onClick = { + onDeleteClick(subscriber) + } + ) } } @@ -346,9 +351,13 @@ private fun DetailRow( } @Composable -private fun DeleteSubscriberButton() { +private fun DeleteSubscriberButton( + onClick: () -> Unit, +) { Button( - onClick = { /* Handle delete action */ }, + onClick = { + onClick() + }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, @@ -399,7 +408,8 @@ fun SubscriberDetailScreenPreview() { subscriberStats = remember { mutableStateOf(subscriberStats) }, onUrlClick = {}, onEmailClick = {}, - onPlanClick = {} + onPlanClick = {}, + onDeleteClick = {} ) } } 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 15736e84bd82..7195497cd215 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 @@ -235,6 +235,9 @@ class SubscribersActivity : BaseAppCompatActivity() { ) navController.navigate(route = SubscriberScreen.Plan.name) }, + onDeleteClick = { subscriber -> + viewModel.onDeleteSubscriberClick(this@SubscribersActivity, subscriber) + }, modifier = modifier, subscriberStats = viewModel.subscriberStats.collectAsState() ) 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 214d29c7f63b..acf8c439bdbc 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 @@ -1,5 +1,7 @@ package org.wordpress.android.ui.subscribers +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job @@ -9,6 +11,7 @@ import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.models.wrappers.SimpleDateFormatWrapper +import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.dataview.DataViewDropdownItem import org.wordpress.android.ui.dataview.DataViewFieldType @@ -17,6 +20,7 @@ import org.wordpress.android.ui.dataview.DataViewItemField import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog +import org.wordpress.android.util.ToastUtilsWrapper import rs.wordpress.api.kotlin.WpRequestResult import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams @@ -30,8 +34,10 @@ import javax.inject.Named @HiltViewModel class SubscribersViewModel @Inject constructor( - @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val appLogWrapper: AppLogWrapper, + private val toastUtilsWrapper: ToastUtilsWrapper, ) : DataViewViewModel( mainDispatcher = mainDispatcher, appLogWrapper = appLogWrapper @@ -215,6 +221,37 @@ class SubscribersViewModel @Inject constructor( } } + private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(ioDispatcher) { + val response = if (subscriber.isEmailSubscriber) { + wpComApiClient.request { requestBuilder -> + requestBuilder.followers().deleteEmailFollower( + wpComSiteId = siteId().toULong(), + subscriptionId = subscriber.subscriptionId + ) + } + } else { + wpComApiClient.request { requestBuilder -> + requestBuilder.followers().deleteFollower( + wpComSiteId = siteId().toULong(), + userId = subscriber.userId + ) + } + } + when (response) { + is WpRequestResult.Success -> { + appLogWrapper.d(AppLog.T.MAIN, "Delete subscriber success") + return@withContext Result.success(true) + } + + else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage + appLogWrapper.e(AppLog.T.MAIN, "Delete subscriber failed: $error") + return@withContext Result.failure(Exception(error)) + } + } + } + + /** * Called when an item in the list is clicked. We use this to request stats for the clicked subscriber. */ @@ -230,6 +267,34 @@ class SubscribersViewModel @Inject constructor( } } + fun onDeleteSubscriberClick( + context: Context, + subscriber: Subscriber, + onSuccess: () -> Unit + ) { + appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}") + MaterialAlertDialogBuilder(context).also { builder -> + builder.setTitle(R.string.subscribers_delete_confirmation_title) + builder.setMessage(R.string.subscribers_delete_confirmation_message) + builder.setPositiveButton(R.string.delete) { _, _ -> + launch(bgDispatcher) { + val result = deleteSubscriber(subscriber = subscriber) + withContext(mainDispatcher) { + if (result.isSuccess) { + toastUtilsWrapper.showToast(R.string.subscribers_delete_success) + onSuccess() + } else { + toastUtilsWrapper.showToast(R.string.subscribers_delete_failed) + } + } + } + } + builder.setNegativeButton(R.string.cancel) { _, _ -> + } + builder.show() + } + } + companion object { private const val ID_FILTER_EMAIL = 1L private const val ID_FILTER_READER = 2L diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 339a494b12f8..0bf212ce20bd 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5031,6 +5031,10 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Successfully added subscribers Failed to add subscribers + Delete subscriber + Are you sure you want to delete this subscriber? + Subscriber deleted + Failed to delete subscriber Application password credentials stored for %1$s diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4c8ddb660f4..a5bd6cf82847 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ wiremock = '2.26.3' wordpress-aztec = 'v2.1.4' wordpress-lint = '2.1.0' wordpress-persistent-edittext = '1.0.2' -wordpress-rs = 'trunk-14fe1179d108dc91066776e28ee9690864cab1f1' +wordpress-rs = 'trunk-342fd04e1f398fdf9167ae9f8a7618da10f10688' wordpress-utils = '3.14.0' automattic-ucrop = '2.2.11' zendesk = '5.5.0' From 84a42789cd5be0aa2c3e11901ca03ffed0bf2a55 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 11:41:03 -0400 Subject: [PATCH 17/37] Added confirmation dialog for deletion --- .../android/ui/subscribers/SubscribersActivity.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 7195497cd215..f391329f8a05 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 @@ -235,8 +235,15 @@ class SubscribersActivity : BaseAppCompatActivity() { ) navController.navigate(route = SubscriberScreen.Plan.name) }, - onDeleteClick = { subscriber -> - viewModel.onDeleteSubscriberClick(this@SubscribersActivity, subscriber) + onDeleteClick = { _ -> + viewModel.onDeleteSubscriberClick( + context = this@SubscribersActivity, + subscriber = subscriber, + onSuccess = { + // TODO refresh list + navController.navigateUp() + } + ) }, modifier = modifier, subscriberStats = viewModel.subscriberStats.collectAsState() From 8be7a50c213c56834d4c7626cae85914653a600b Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 12:08:45 -0400 Subject: [PATCH 18/37] Refresh list after successful deletion --- .../org/wordpress/android/ui/subscribers/SubscribersActivity.kt | 1 - .../org/wordpress/android/ui/subscribers/SubscribersViewModel.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) 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 f391329f8a05..e7d4db9aca33 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 @@ -240,7 +240,6 @@ class SubscribersActivity : BaseAppCompatActivity() { context = this@SubscribersActivity, subscriber = subscriber, onSuccess = { - // TODO refresh list navController.navigateUp() } ) 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 acf8c439bdbc..762c8925f032 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 @@ -283,6 +283,7 @@ class SubscribersViewModel @Inject constructor( if (result.isSuccess) { toastUtilsWrapper.showToast(R.string.subscribers_delete_success) onSuccess() + onRefreshData() } else { toastUtilsWrapper.showToast(R.string.subscribers_delete_failed) } From 02ac3aca09734c199b912d8ad03e1d33ae80c630 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:06:50 -0400 Subject: [PATCH 19/37] Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../wordpress/android/ui/subscribers/AddSubscribersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt index 5377b14794f7..07d1e3ccf312 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt @@ -62,7 +62,7 @@ class AddSubscribersViewModel @Inject constructor( ) { launch(bgDispatcher) { val result = addSubscribers(emails) - launch(mainDispatcher) { + withContext(mainDispatcher) { if (result.isSuccess) { toastUtilsWrapper.showToast(R.string.subscribers_add_success) onSuccess() From 9e3d04e81142c305482c56c52578c2385046e362 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:14:34 -0400 Subject: [PATCH 20/37] Use rememberSavable for the button state --- .../wordpress/android/ui/subscribers/SubscribersActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 e7d4db9aca33..83d9873ff6e4 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -70,7 +71,7 @@ class SubscribersActivity : BaseAppCompatActivity() { val navController = rememberNavController() val listTitle = stringResource(R.string.subscribers) val titleState = remember { mutableStateOf(listTitle) } - val showAddSubscribersButtonState = remember { mutableStateOf(true) } + val showAddSubscribersButtonState = rememberSaveable { mutableStateOf(true) } AppThemeM3 { Scaffold( From 10c5b75497fc2b3c39d5a085212ac29a065a08b5 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:16:54 -0400 Subject: [PATCH 21/37] Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../wordpress/android/ui/subscribers/SubscribersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 762c8925f032..01c3cdc75574 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 @@ -221,7 +221,7 @@ class SubscribersViewModel @Inject constructor( } } - private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(ioDispatcher) { + private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(bgDispatcher) { val response = if (subscriber.isEmailSubscriber) { wpComApiClient.request { requestBuilder -> requestBuilder.followers().deleteEmailFollower( From fbd729203a924bdc7f134a74d15f68676386b778 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:28:54 -0400 Subject: [PATCH 22/37] Use ioDispatcher for all network requests --- .../android/ui/subscribers/SubscribersViewModel.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 01c3cdc75574..9a5089cafa08 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 @@ -11,7 +11,6 @@ import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.models.wrappers.SimpleDateFormatWrapper -import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.dataview.DataViewDropdownItem import org.wordpress.android.ui.dataview.DataViewFieldType @@ -35,7 +34,6 @@ import javax.inject.Named @HiltViewModel class SubscribersViewModel @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, - @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val appLogWrapper: AppLogWrapper, private val toastUtilsWrapper: ToastUtilsWrapper, ) : DataViewViewModel( @@ -221,7 +219,7 @@ class SubscribersViewModel @Inject constructor( } } - private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(bgDispatcher) { + private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(ioDispatcher) { val response = if (subscriber.isEmailSubscriber) { wpComApiClient.request { requestBuilder -> requestBuilder.followers().deleteEmailFollower( @@ -277,7 +275,7 @@ class SubscribersViewModel @Inject constructor( builder.setTitle(R.string.subscribers_delete_confirmation_title) builder.setMessage(R.string.subscribers_delete_confirmation_message) builder.setPositiveButton(R.string.delete) { _, _ -> - launch(bgDispatcher) { + launch(ioDispatcher) { val result = deleteSubscriber(subscriber = subscriber) withContext(mainDispatcher) { if (result.isSuccess) { From 3bac5d974cb095608179180666d0f55693cacecc Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:54:08 -0400 Subject: [PATCH 23/37] First pass at using event observer for deleting subscriber --- .../ui/subscribers/SubscribersActivity.kt | 48 +++++++++++++++++-- .../ui/subscribers/SubscribersViewModel.kt | 48 ++++++++----------- 2 files changed, 64 insertions(+), 32 deletions(-) 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 83d9873ff6e4..ccd89ed29c83 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 @@ -2,7 +2,12 @@ package org.wordpress.android.ui.subscribers import android.os.Build import android.os.Bundle +import android.widget.Toast import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -26,6 +31,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.ui.ActivityLauncher @@ -41,6 +47,7 @@ class SubscribersActivity : BaseAppCompatActivity() { private val addSubscribersViewModel by viewModels() private lateinit var composeView: ComposeView + private lateinit var navController: NavHostController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,6 +63,20 @@ class SubscribersActivity : BaseAppCompatActivity() { } } ) + + viewModel.uiEvent + .filterNotNull() + .onEach { event -> + when (event) { + is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { + showDeleteConfirmationDialog(event.subscriber, navController) + } + is SubscribersViewModel.UiEvent.ShowToast -> { + Toast.makeText(this, event.messageRes, Toast.LENGTH_SHORT).show() + } + } + } + .launchIn(lifecycleScope) } private enum class SubscriberScreen { @@ -68,7 +89,7 @@ class SubscribersActivity : BaseAppCompatActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun NavigableContent() { - val navController = rememberNavController() + navController = rememberNavController() val listTitle = stringResource(R.string.subscribers) val titleState = remember { mutableStateOf(listTitle) } val showAddSubscribersButtonState = rememberSaveable { mutableStateOf(true) } @@ -238,11 +259,7 @@ class SubscribersActivity : BaseAppCompatActivity() { }, onDeleteClick = { _ -> viewModel.onDeleteSubscriberClick( - context = this@SubscribersActivity, subscriber = subscriber, - onSuccess = { - navController.navigateUp() - } ) }, modifier = modifier, @@ -270,6 +287,27 @@ class SubscribersActivity : BaseAppCompatActivity() { ) } + private fun showDeleteConfirmationDialog( + subscriber: Subscriber, + navController: NavHostController + ) { + MaterialAlertDialogBuilder(this).also { builder -> + builder.setTitle(R.string.subscribers_delete_confirmation_title) + builder.setMessage(R.string.subscribers_delete_confirmation_message) + builder.setPositiveButton(R.string.delete) { _, _ -> + viewModel.deleteSubscriberConfirmed( + subscriber = subscriber, + onSuccess = { + navController.navigateUp() + } + ) + } + builder.setNegativeButton(R.string.cancel) { _, _ -> + } + builder.show() + } + } + private fun onEmailClick(email: String) { ActivityLauncher.openUrlExternal(this, "mailto:$email") } 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 9a5089cafa08..9a38e5c9651d 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 @@ -1,7 +1,5 @@ package org.wordpress.android.ui.subscribers -import android.content.Context -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job @@ -19,7 +17,6 @@ import org.wordpress.android.ui.dataview.DataViewItemField import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog -import org.wordpress.android.util.ToastUtilsWrapper import rs.wordpress.api.kotlin.WpRequestResult import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams @@ -35,7 +32,6 @@ import javax.inject.Named class SubscribersViewModel @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, private val appLogWrapper: AppLogWrapper, - private val toastUtilsWrapper: ToastUtilsWrapper, ) : DataViewViewModel( mainDispatcher = mainDispatcher, appLogWrapper = appLogWrapper @@ -48,6 +44,13 @@ class SubscribersViewModel @Inject constructor( @Inject lateinit var dateFormatWrapper: SimpleDateFormatWrapper + sealed class UiEvent { + data class ShowDeleteConfirmationDialog(val subscriber: Subscriber) : UiEvent() + data class ShowToast(val messageRes: Int) : UiEvent() + } + private val _uiEvent = MutableStateFlow(null) + val uiEvent = _uiEvent.asStateFlow() + override fun getSupportedFilters(): List { return listOf( DataViewDropdownItem( @@ -265,32 +268,23 @@ class SubscribersViewModel @Inject constructor( } } - fun onDeleteSubscriberClick( - context: Context, - subscriber: Subscriber, - onSuccess: () -> Unit - ) { + fun onDeleteSubscriberClick(subscriber: Subscriber) { appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}") - MaterialAlertDialogBuilder(context).also { builder -> - builder.setTitle(R.string.subscribers_delete_confirmation_title) - builder.setMessage(R.string.subscribers_delete_confirmation_message) - builder.setPositiveButton(R.string.delete) { _, _ -> - launch(ioDispatcher) { - val result = deleteSubscriber(subscriber = subscriber) - withContext(mainDispatcher) { - if (result.isSuccess) { - toastUtilsWrapper.showToast(R.string.subscribers_delete_success) - onSuccess() - onRefreshData() - } else { - toastUtilsWrapper.showToast(R.string.subscribers_delete_failed) - } - } + _uiEvent.value = UiEvent.ShowDeleteConfirmationDialog(subscriber) + } + + fun deleteSubscriberConfirmed(subscriber: Subscriber, onSuccess: () -> Unit) { + launch(ioDispatcher) { + val result = deleteSubscriber(subscriber = subscriber) + withContext(mainDispatcher) { + if (result.isSuccess) { + _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_success) + onSuccess() + onRefreshData() + } else { + _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed) } } - builder.setNegativeButton(R.string.cancel) { _, _ -> - } - builder.show() } } From 6af224aecaba34b101ede4722bb34efb86a396d9 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 3 Jul 2025 15:58:35 -0400 Subject: [PATCH 24/37] Clear uiEvent after handling --- .../wordpress/android/ui/subscribers/SubscribersActivity.kt | 5 +++-- .../wordpress/android/ui/subscribers/SubscribersViewModel.kt | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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 ccd89ed29c83..c27a7b2ba8f9 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 @@ -70,9 +70,11 @@ class SubscribersActivity : BaseAppCompatActivity() { when (event) { is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { showDeleteConfirmationDialog(event.subscriber, navController) + viewModel.clearUiEvent() } is SubscribersViewModel.UiEvent.ShowToast -> { Toast.makeText(this, event.messageRes, Toast.LENGTH_SHORT).show() + viewModel.clearUiEvent() } } } @@ -302,8 +304,7 @@ class SubscribersActivity : BaseAppCompatActivity() { } ) } - builder.setNegativeButton(R.string.cancel) { _, _ -> - } + builder.setNegativeButton(R.string.cancel, null) builder.show() } } 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 9a38e5c9651d..963f6b96ff9c 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 @@ -288,6 +288,10 @@ class SubscribersViewModel @Inject constructor( } } + fun clearUiEvent() { + _uiEvent.value = null + } + companion object { private const val ID_FILTER_EMAIL = 1L private const val ID_FILTER_READER = 2L From dd455ab7ad8fc3e9c2c5e90b83b6e7efcb2c944c Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 09:04:53 -0400 Subject: [PATCH 25/37] Added a couple of comments --- .../android/ui/subscribers/SubscribersViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 963f6b96ff9c..dea3c0da0c19 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 @@ -268,11 +268,17 @@ class SubscribersViewModel @Inject constructor( } } + /** + * Trigger the delete confirmation dialog when the user taps the delete button for a subscriber + */ fun onDeleteSubscriberClick(subscriber: Subscriber) { appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}") _uiEvent.value = UiEvent.ShowDeleteConfirmationDialog(subscriber) } + /** + * Subscriber deletion has been confirmed by the user. Delete the subscriber and refresh the list. + */ fun deleteSubscriberConfirmed(subscriber: Subscriber, onSuccess: () -> Unit) { launch(ioDispatcher) { val result = deleteSubscriber(subscriber = subscriber) From 257d866d02e2ecacf036e2e63ae0c69a16cf43bc Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 09:25:51 -0400 Subject: [PATCH 26/37] Use SingleLiveEvent to simplify event observation and clearing --- .../ui/subscribers/SubscribersActivity.kt | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) 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 c27a7b2ba8f9..849b3381ae2c 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 @@ -4,10 +4,6 @@ import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.activity.viewModels -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -64,21 +60,17 @@ class SubscribersActivity : BaseAppCompatActivity() { } ) - viewModel.uiEvent - .filterNotNull() - .onEach { event -> - when (event) { - is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { - showDeleteConfirmationDialog(event.subscriber, navController) - viewModel.clearUiEvent() - } - is SubscribersViewModel.UiEvent.ShowToast -> { - Toast.makeText(this, event.messageRes, Toast.LENGTH_SHORT).show() - viewModel.clearUiEvent() - } + viewModel.uiEvent.observe(this) { event -> + when (event) { + is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { + showDeleteConfirmationDialog(event.subscriber, navController) + } + + is SubscribersViewModel.UiEvent.ShowToast -> { + Toast.makeText(this, event.messageRes, Toast.LENGTH_SHORT).show() } } - .launchIn(lifecycleScope) + } } private enum class SubscriberScreen { From a9c7f4e36ec59e43c48ac5a39c8099c37825904d Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 09:38:51 -0400 Subject: [PATCH 27/37] Updated WP-rs version --- .../android/ui/subscribers/SubscribersViewModel.kt | 9 +++------ gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) 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 dea3c0da0c19..4b4ab1032416 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 @@ -17,6 +17,7 @@ import org.wordpress.android.ui.dataview.DataViewItemField import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog +import org.wordpress.android.viewmodel.SingleLiveEvent import rs.wordpress.api.kotlin.WpRequestResult import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams @@ -48,8 +49,8 @@ class SubscribersViewModel @Inject constructor( data class ShowDeleteConfirmationDialog(val subscriber: Subscriber) : UiEvent() data class ShowToast(val messageRes: Int) : UiEvent() } - private val _uiEvent = MutableStateFlow(null) - val uiEvent = _uiEvent.asStateFlow() + private val _uiEvent = SingleLiveEvent() + val uiEvent = _uiEvent override fun getSupportedFilters(): List { return listOf( @@ -294,10 +295,6 @@ class SubscribersViewModel @Inject constructor( } } - fun clearUiEvent() { - _uiEvent.value = null - } - companion object { private const val ID_FILTER_EMAIL = 1L private const val ID_FILTER_READER = 2L diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5bd6cf82847..309e1b943205 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ wiremock = '2.26.3' wordpress-aztec = 'v2.1.4' wordpress-lint = '2.1.0' wordpress-persistent-edittext = '1.0.2' -wordpress-rs = 'trunk-342fd04e1f398fdf9167ae9f8a7618da10f10688' +wordpress-rs = 'trunk-0c52d3f421b29e4981e3134f2fb3eb85d1392410' wordpress-utils = '3.14.0' automattic-ucrop = '2.2.11' zendesk = '5.5.0' From cb92499377821b8ee38d60e7c34ef7c17aa057b1 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 10:29:00 -0400 Subject: [PATCH 28/37] Reverted SingleLiveEvent due to state loss during configuration change --- .../ui/subscribers/SubscribersActivity.kt | 19 ++++++++++++------- .../ui/subscribers/SubscribersViewModel.kt | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) 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 849b3381ae2c..6d1684ae34b9 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 @@ -19,6 +19,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -60,14 +63,16 @@ class SubscribersActivity : BaseAppCompatActivity() { } ) - viewModel.uiEvent.observe(this) { event -> - when (event) { - is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { - showDeleteConfirmationDialog(event.subscriber, navController) - } + lifecycleScope.launch { + viewModel.uiEvent.filterNotNull().collect { event -> + when (event) { + is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> { + showDeleteConfirmationDialog(event.subscriber, navController) + } - is SubscribersViewModel.UiEvent.ShowToast -> { - Toast.makeText(this, event.messageRes, Toast.LENGTH_SHORT).show() + is SubscribersViewModel.UiEvent.ShowToast -> { + Toast.makeText(this@SubscribersActivity, event.messageRes, Toast.LENGTH_SHORT).show() + } } } } 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 4b4ab1032416..16bcd9b0c330 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 @@ -17,7 +17,6 @@ import org.wordpress.android.ui.dataview.DataViewItemField import org.wordpress.android.ui.dataview.DataViewItemImage import org.wordpress.android.ui.dataview.DataViewViewModel import org.wordpress.android.util.AppLog -import org.wordpress.android.viewmodel.SingleLiveEvent import rs.wordpress.api.kotlin.WpRequestResult import uniffi.wp_api.IndividualSubscriberStats import uniffi.wp_api.IndividualSubscriberStatsParams @@ -49,7 +48,8 @@ class SubscribersViewModel @Inject constructor( data class ShowDeleteConfirmationDialog(val subscriber: Subscriber) : UiEvent() data class ShowToast(val messageRes: Int) : UiEvent() } - private val _uiEvent = SingleLiveEvent() + + private val _uiEvent = MutableStateFlow(null) val uiEvent = _uiEvent override fun getSupportedFilters(): List { From b0241d4c1ddedfbeeb130fc81ce7e9ecec0ee700 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 10:36:43 -0400 Subject: [PATCH 29/37] Clear the event after setting it --- .../wordpress/android/ui/subscribers/SubscribersViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 16bcd9b0c330..f8489bc6b863 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 @@ -275,6 +275,7 @@ class SubscribersViewModel @Inject constructor( fun onDeleteSubscriberClick(subscriber: Subscriber) { appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}") _uiEvent.value = UiEvent.ShowDeleteConfirmationDialog(subscriber) + _uiEvent.value = null } /** @@ -291,6 +292,7 @@ class SubscribersViewModel @Inject constructor( } else { _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed) } + _uiEvent.value = null } } } From 5edf421922b6f1042818b2e6cd42d732d07fbe1b Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 10:38:07 -0400 Subject: [PATCH 30/37] Moved clearing the event to its own fun (again) --- .../android/ui/subscribers/SubscribersViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 f8489bc6b863..78ad9ea5bf0e 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 @@ -275,7 +275,7 @@ class SubscribersViewModel @Inject constructor( fun onDeleteSubscriberClick(subscriber: Subscriber) { appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}") _uiEvent.value = UiEvent.ShowDeleteConfirmationDialog(subscriber) - _uiEvent.value = null + clearUiEvent() } /** @@ -292,11 +292,15 @@ class SubscribersViewModel @Inject constructor( } else { _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed) } - _uiEvent.value = null + clearUiEvent() } } } + private fun clearUiEvent() { + _uiEvent.value = null + } + companion object { private const val ID_FILTER_EMAIL = 1L private const val ID_FILTER_READER = 2L From c79619b6f7b15b387394aed2e04fbb62a31424a5 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 10:45:38 -0400 Subject: [PATCH 31/37] Use runCatching when deleting --- .../android/ui/subscribers/SubscribersViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 78ad9ea5bf0e..4ab43fc26a0e 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 @@ -223,7 +223,7 @@ class SubscribersViewModel @Inject constructor( } } - private suspend fun deleteSubscriber(subscriber: Subscriber): Result = withContext(ioDispatcher) { + private suspend fun deleteSubscriber(subscriber: Subscriber) = runCatching { val response = if (subscriber.isEmailSubscriber) { wpComApiClient.request { requestBuilder -> requestBuilder.followers().deleteEmailFollower( @@ -242,13 +242,13 @@ class SubscribersViewModel @Inject constructor( when (response) { is WpRequestResult.Success -> { appLogWrapper.d(AppLog.T.MAIN, "Delete subscriber success") - return@withContext Result.success(true) + Result.success(true) } else -> { val error = (response as? WpRequestResult.WpError)?.errorMessage appLogWrapper.e(AppLog.T.MAIN, "Delete subscriber failed: $error") - return@withContext Result.failure(Exception(error)) + Result.failure(Exception(error)) } } } From ddb585385ff49b0786fd69551d473498aae4adba Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 10:50:23 -0400 Subject: [PATCH 32/37] Use runCatching when adding a subscriber --- .../ui/subscribers/AddSubscribersViewModel.kt | 61 ++++++++++--------- .../ui/subscribers/SubscribersViewModel.kt | 48 ++++++++------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt index 07d1e3ccf312..918a8ec69e33 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt @@ -4,7 +4,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.fluxc.store.AccountStore @@ -73,41 +72,43 @@ class AddSubscribersViewModel @Inject constructor( } } - private suspend fun addSubscribers(emails: List): Result = withContext(ioDispatcher) { - val params = AddSubscribersParams( - emails = emails - ) + private suspend fun addSubscribers(emails: List) = runCatching { + withContext(ioDispatcher) { + val params = AddSubscribersParams( + emails = emails + ) - _showProgress.value = true - try { - val response = wpComApiClient.request { requestBuilder -> - requestBuilder.subscribers().addSubscribers( - wpComSiteId = siteId().toULong(), - params = params - ) - } + _showProgress.value = true + try { + val response = wpComApiClient.request { requestBuilder -> + requestBuilder.subscribers().addSubscribers( + wpComSiteId = siteId().toULong(), + params = params + ) + } - when (response) { - is WpRequestResult.Success -> { - // the backend may return HTTP 200 even when no subscribers were added, so verify there's - // a valid uploadId before assuming success - if (response.response.data.uploadId == 0.toULong()) { - appLogWrapper.d(AppLog.T.MAIN, "No subscribers added") - return@withContext Result.failure(Exception("No subscribers added")) - } else { - appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") - return@withContext Result.success(true) + when (response) { + is WpRequestResult.Success -> { + // the backend may return HTTP 200 even when no subscribers were added, so verify there's + // a valid uploadId before assuming success + if (response.response.data.uploadId == 0.toULong()) { + appLogWrapper.d(AppLog.T.MAIN, "No subscribers added") + Result.failure(Exception("No subscribers added")) + } else { + appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers") + Result.success(true) + } } - } - else -> { - val error = (response as? WpRequestResult.WpError)?.errorMessage - appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") - return@withContext Result.failure(Exception(error)) + else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage + appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response") + Result.failure(Exception(error)) + } } + } finally { + _showProgress.value = false } - } finally { - _showProgress.value = false } } } 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 4ab43fc26a0e..5211cd6694f1 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 @@ -224,31 +224,33 @@ class SubscribersViewModel @Inject constructor( } private suspend fun deleteSubscriber(subscriber: Subscriber) = runCatching { - val response = if (subscriber.isEmailSubscriber) { - wpComApiClient.request { requestBuilder -> - requestBuilder.followers().deleteEmailFollower( - wpComSiteId = siteId().toULong(), - subscriptionId = subscriber.subscriptionId - ) - } - } else { - wpComApiClient.request { requestBuilder -> - requestBuilder.followers().deleteFollower( - wpComSiteId = siteId().toULong(), - userId = subscriber.userId - ) - } - } - when (response) { - is WpRequestResult.Success -> { - appLogWrapper.d(AppLog.T.MAIN, "Delete subscriber success") - Result.success(true) + withContext(ioDispatcher) { + val response = if (subscriber.isEmailSubscriber) { + wpComApiClient.request { requestBuilder -> + requestBuilder.followers().deleteEmailFollower( + wpComSiteId = siteId().toULong(), + subscriptionId = subscriber.subscriptionId + ) + } + } else { + wpComApiClient.request { requestBuilder -> + requestBuilder.followers().deleteFollower( + wpComSiteId = siteId().toULong(), + userId = subscriber.userId + ) + } } + when (response) { + is WpRequestResult.Success -> { + appLogWrapper.d(AppLog.T.MAIN, "Delete subscriber success") + Result.success(true) + } - else -> { - val error = (response as? WpRequestResult.WpError)?.errorMessage - appLogWrapper.e(AppLog.T.MAIN, "Delete subscriber failed: $error") - Result.failure(Exception(error)) + else -> { + val error = (response as? WpRequestResult.WpError)?.errorMessage + appLogWrapper.e(AppLog.T.MAIN, "Delete subscriber failed: $error") + Result.failure(Exception(error)) + } } } } From 26282b16b4a27624b72c8facd323b39b99ba22d1 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 15:40:34 -0400 Subject: [PATCH 33/37] Updated wordpress-rs hash --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 279ea9b3ee0a..cb0bfafebd41 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ wiremock = '2.26.3' wordpress-aztec = 'v2.1.4' wordpress-lint = '2.1.0' wordpress-persistent-edittext = '1.0.2' -wordpress-rs = 'trunk-0c52d3f421b29e4981e3134f2fb3eb85d1392410' +wordpress-rs = '796-fba85cb161fb58a84a898ee9aa1ce62c726b1b66' wordpress-utils = '3.14.0' automattic-ucrop = '2.2.11' zendesk = '5.5.0' From f2330aa24bf6f9a61d072ed9f1d48e700538242f Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Mon, 7 Jul 2025 16:04:39 -0400 Subject: [PATCH 34/37] Added a delay to deletion --- .../wordpress/android/ui/subscribers/SubscribersViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 5211cd6694f1..dd1c6b1b4b52 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 @@ -3,6 +3,7 @@ package org.wordpress.android.ui.subscribers import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext @@ -286,6 +287,8 @@ class SubscribersViewModel @Inject constructor( fun deleteSubscriberConfirmed(subscriber: Subscriber, onSuccess: () -> Unit) { launch(ioDispatcher) { val result = deleteSubscriber(subscriber = subscriber) + // add a short delay or else refreshing the list may still show the deleted subscriber + delay(DELETE_DELAY) withContext(mainDispatcher) { if (result.isSuccess) { _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_success) @@ -312,6 +315,8 @@ class SubscribersViewModel @Inject constructor( private const val ID_SORT_EMAIL = 3L private const val ID_SORT_PLAN = 4L + private const val DELETE_DELAY = 500L + fun Subscriber.displayNameOrEmail() = displayName.ifEmpty { emailAddress } } } From 3013bf1707c0f79862d3aa37e353d3ec06d13483 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 8 Jul 2025 07:50:14 -0400 Subject: [PATCH 35/37] Remove local subscriber on successful deletion --- .../android/ui/dataview/DataViewViewModel.kt | 7 +++++++ .../android/ui/subscribers/SubscribersActivity.kt | 6 +++--- .../android/ui/subscribers/SubscribersViewModel.kt | 14 ++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewViewModel.kt index 265476d53237..19f32f49a4a1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewViewModel.kt @@ -197,6 +197,13 @@ open class DataViewViewModel @Inject constructor( appLogWrapper.d(AppLog.T.MAIN, "$logTag updateUiState: $state") } + /** + * Removes an item from the local list of items + */ + fun removeItem(id: Long) { + _items.value = items.value.filter { it.id != id } + } + /** * Descendants should override this to perform their specific network request */ 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 6d1684ae34b9..4a8938d8adc0 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 @@ -19,19 +19,19 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.launch 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.lifecycle.lifecycleScope import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch import org.wordpress.android.R import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.compose.theme.AppThemeM3 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 dd1c6b1b4b52..806a07101598 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 @@ -3,7 +3,6 @@ package org.wordpress.android.ui.subscribers import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext @@ -189,7 +188,7 @@ class SubscribersViewModel @Inject constructor( ) } - /* + /** * Returns the subscriber with the given ID, or null if not found. Note that this does NOT do a network call, * it simply returns the subscriber from the existing list of items. */ @@ -282,18 +281,19 @@ class SubscribersViewModel @Inject constructor( } /** - * Subscriber deletion has been confirmed by the user. Delete the subscriber and refresh the list. + * Subscriber deletion has been confirmed by the user so delete the subscriber */ fun deleteSubscriberConfirmed(subscriber: Subscriber, onSuccess: () -> Unit) { launch(ioDispatcher) { val result = deleteSubscriber(subscriber = subscriber) - // add a short delay or else refreshing the list may still show the deleted subscriber - delay(DELETE_DELAY) + withContext(mainDispatcher) { if (result.isSuccess) { + // note that it may take a few seconds for the subscriber to actually be deleted, + // which is why we only remove it locally instead of fetching the list again + removeItem(subscriber.userId) _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_success) onSuccess() - onRefreshData() } else { _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed) } @@ -315,8 +315,6 @@ class SubscribersViewModel @Inject constructor( private const val ID_SORT_EMAIL = 3L private const val ID_SORT_PLAN = 4L - private const val DELETE_DELAY = 500L - fun Subscriber.displayNameOrEmail() = displayName.ifEmpty { emailAddress } } } From ad9a52f41cceb0927f10bc5f4e56899e84297e92 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 8 Jul 2025 07:52:07 -0400 Subject: [PATCH 36/37] Updated wordpress-rs version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb0bfafebd41..5310ec7c6467 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,7 +103,7 @@ wiremock = '2.26.3' wordpress-aztec = 'v2.1.4' wordpress-lint = '2.1.0' wordpress-persistent-edittext = '1.0.2' -wordpress-rs = '796-fba85cb161fb58a84a898ee9aa1ce62c726b1b66' +wordpress-rs = 'trunk-d6ea38c25176ffa2fa3ce8539ae6b078d58cfdad' wordpress-utils = '3.14.0' automattic-ucrop = '2.2.11' zendesk = '5.5.0' From e99baa092f4ce3df8b4604c08c8214d0ab39029b Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 8 Jul 2025 15:04:16 -0400 Subject: [PATCH 37/37] Show deletion success dialog --- .../ui/subscribers/SubscribersActivity.kt | 17 +++++++++++++++++ .../ui/subscribers/SubscribersViewModel.kt | 3 ++- WordPress/src/main/res/values/strings.xml | 3 ++- 3 files changed, 21 insertions(+), 2 deletions(-) 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 5caaf39b78c2..2ac92f520978 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 @@ -70,6 +70,10 @@ class SubscribersActivity : BaseAppCompatActivity() { showDeleteConfirmationDialog(event.subscriber, navController) } + is SubscribersViewModel.UiEvent.ShowDeleteSuccessDialog -> { + showDeleteSuccessDialog() + } + is SubscribersViewModel.UiEvent.ShowToast -> { Toast.makeText(this@SubscribersActivity, event.messageRes, Toast.LENGTH_SHORT).show() } @@ -290,6 +294,10 @@ class SubscribersActivity : BaseAppCompatActivity() { ) } + /** + * The user tapped the "Delete subscriber" button, so show a confirmation dialog + * before telling the view model to actually delete the subscriber + */ private fun showDeleteConfirmationDialog( subscriber: Subscriber, navController: NavHostController @@ -310,6 +318,15 @@ class SubscribersActivity : BaseAppCompatActivity() { } } + private fun showDeleteSuccessDialog() { + MaterialAlertDialogBuilder(this).also { builder -> + builder.setTitle(R.string.subscribers_delete_success_title) + builder.setMessage(R.string.subscribers_delete_success_message) + builder.setPositiveButton(R.string.ok, null) + builder.show() + } + } + private fun onEmailClick(email: String) { ActivityLauncher.openUrlExternal(this, "mailto:$email") } 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 450a31059e8d..23b8ba1c822e 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 @@ -46,6 +46,7 @@ class SubscribersViewModel @Inject constructor( sealed class UiEvent { data class ShowDeleteConfirmationDialog(val subscriber: Subscriber) : UiEvent() + data object ShowDeleteSuccessDialog : UiEvent() data class ShowToast(val messageRes: Int) : UiEvent() } @@ -287,7 +288,7 @@ class SubscribersViewModel @Inject constructor( // note that it may take a few seconds for the subscriber to actually be deleted, // which is why we only remove it locally instead of fetching the list again removeItem(subscriber.userId) - _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_success) + _uiEvent.value = UiEvent.ShowDeleteSuccessDialog onSuccess() } else { _uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 6fd92b7bd864..6e21268c4fc3 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -5034,7 +5034,8 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Failed to add subscribers Delete subscriber Are you sure you want to delete this subscriber? - Subscriber deleted + Subscriber deleted + The subscriber has been deleted, but it may take a minute for the deletion to take effect Failed to delete subscriber