Skip to content

Commit abff1d5

Browse files
nbradburyCopilot
andauthored
Delete subscriber (#21989)
* Created shell for adding subscribers * Fleshed out shell for adding subscribers * Fleshed out shell for adding subscribers (again) * Fixed broken text field * Changed result type of addSubscribers fun * Added progress * Simplified layout * Fixed positioning in layout * Don't use onError when adding subscribers * Separate screens in SubscribersActivity * Moved adding subscribers to its own view model * Update WordPress/src/main/res/values/strings.xml Co-authored-by: Copilot <[email protected]> * Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt Co-authored-by: Copilot <[email protected]> * Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersScreen.kt Co-authored-by: Copilot <[email protected]> * Let view model handle success/failure * First pass at deleting subscriber * Added confirmation dialog for deletion * Refresh list after successful deletion * Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt Co-authored-by: Copilot <[email protected]> * Use rememberSavable for the button state * Update WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt Co-authored-by: Copilot <[email protected]> * Use ioDispatcher for all network requests * First pass at using event observer for deleting subscriber * Clear uiEvent after handling * Added a couple of comments * Use SingleLiveEvent to simplify event observation and clearing * Updated WP-rs version * Reverted SingleLiveEvent due to state loss during configuration change * Clear the event after setting it * Moved clearing the event to its own fun (again) * Use runCatching when deleting * Use runCatching when adding a subscriber * Updated wordpress-rs hash * Added a delay to deletion * Remove local subscriber on successful deletion * Updated wordpress-rs version * Show deletion success dialog --------- Co-authored-by: Copilot <[email protected]>
1 parent 5ab4608 commit abff1d5

File tree

7 files changed

+203
-40
lines changed

7 files changed

+203
-40
lines changed

WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ open class DataViewViewModel @Inject constructor(
218218
appLogWrapper.d(AppLog.T.MAIN, "$logTag updateUiState: $state")
219219
}
220220

221+
/**
222+
* Removes an item from the local list of items
223+
*/
224+
fun removeItem(id: Long) {
225+
_items.value = items.value.filter { it.id != id }
226+
}
227+
221228
/**
222229
* Descendants should override this to perform their specific network request
223230
*/

WordPress/src/main/java/org/wordpress/android/ui/subscribers/AddSubscribersViewModel.kt

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
44
import kotlinx.coroutines.CoroutineDispatcher
55
import kotlinx.coroutines.flow.MutableStateFlow
66
import kotlinx.coroutines.flow.asStateFlow
7-
import kotlinx.coroutines.launch
87
import kotlinx.coroutines.withContext
98
import org.wordpress.android.R
109
import org.wordpress.android.fluxc.store.AccountStore
@@ -62,7 +61,7 @@ class AddSubscribersViewModel @Inject constructor(
6261
) {
6362
launch(bgDispatcher) {
6463
val result = addSubscribers(emails)
65-
launch(mainDispatcher) {
64+
withContext(mainDispatcher) {
6665
if (result.isSuccess) {
6766
toastUtilsWrapper.showToast(R.string.subscribers_add_success)
6867
onSuccess()
@@ -73,41 +72,43 @@ class AddSubscribersViewModel @Inject constructor(
7372
}
7473
}
7574

76-
private suspend fun addSubscribers(emails: List<String>): Result<Boolean> = withContext(ioDispatcher) {
77-
val params = AddSubscribersParams(
78-
emails = emails
79-
)
75+
private suspend fun addSubscribers(emails: List<String>) = runCatching {
76+
withContext(ioDispatcher) {
77+
val params = AddSubscribersParams(
78+
emails = emails
79+
)
8080

81-
_showProgress.value = true
82-
try {
83-
val response = wpComApiClient.request { requestBuilder ->
84-
requestBuilder.subscribers().addSubscribers(
85-
wpComSiteId = siteId().toULong(),
86-
params = params
87-
)
88-
}
81+
_showProgress.value = true
82+
try {
83+
val response = wpComApiClient.request { requestBuilder ->
84+
requestBuilder.subscribers().addSubscribers(
85+
wpComSiteId = siteId().toULong(),
86+
params = params
87+
)
88+
}
8989

90-
when (response) {
91-
is WpRequestResult.Success -> {
92-
// the backend may return HTTP 200 even when no subscribers were added, so verify there's
93-
// a valid uploadId before assuming success
94-
if (response.response.data.uploadId == 0.toULong()) {
95-
appLogWrapper.d(AppLog.T.MAIN, "No subscribers added")
96-
return@withContext Result.failure(Exception("No subscribers added"))
97-
} else {
98-
appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers")
99-
return@withContext Result.success(true)
90+
when (response) {
91+
is WpRequestResult.Success -> {
92+
// the backend may return HTTP 200 even when no subscribers were added, so verify there's
93+
// a valid uploadId before assuming success
94+
if (response.response.data.uploadId == 0.toULong()) {
95+
appLogWrapper.d(AppLog.T.MAIN, "No subscribers added")
96+
Result.failure(Exception("No subscribers added"))
97+
} else {
98+
appLogWrapper.d(AppLog.T.MAIN, "Successfully added ${emails.size} subscribers")
99+
Result.success(true)
100+
}
100101
}
101-
}
102102

103-
else -> {
104-
val error = (response as? WpRequestResult.WpError)?.errorMessage
105-
appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response")
106-
return@withContext Result.failure(Exception(error))
103+
else -> {
104+
val error = (response as? WpRequestResult.WpError)?.errorMessage
105+
appLogWrapper.e(AppLog.T.MAIN, "Failed to add subscriber: $response")
106+
Result.failure(Exception(error))
107+
}
107108
}
109+
} finally {
110+
_showProgress.value = false
108111
}
109-
} finally {
110-
_showProgress.value = false
111112
}
112113
}
113114
}

WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscriberDetailScreen.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ fun SubscriberDetailScreen(
5757
onUrlClick: (String) -> Unit,
5858
onEmailClick: (String) -> Unit,
5959
onPlanClick: (index: Int) -> Unit,
60+
onDeleteClick: (subscriber: Subscriber) -> Unit,
6061
modifier: Modifier = Modifier,
6162
subscriberStats: State<IndividualSubscriberStats?>? = null
6263
) {
@@ -92,7 +93,11 @@ fun SubscriberDetailScreen(
9293

9394
Spacer(modifier = Modifier.height(32.dp))
9495

95-
DeleteSubscriberButton()
96+
DeleteSubscriberButton(
97+
onClick = {
98+
onDeleteClick(subscriber)
99+
}
100+
)
96101
}
97102
}
98103

@@ -346,9 +351,13 @@ private fun DetailRow(
346351
}
347352

348353
@Composable
349-
private fun DeleteSubscriberButton() {
354+
private fun DeleteSubscriberButton(
355+
onClick: () -> Unit,
356+
) {
350357
Button(
351-
onClick = { /* Handle delete action */ },
358+
onClick = {
359+
onClick()
360+
},
352361
modifier = Modifier.fillMaxWidth(),
353362
colors = ButtonDefaults.buttonColors(
354363
containerColor = Color.Transparent,
@@ -399,7 +408,8 @@ fun SubscriberDetailScreenPreview() {
399408
subscriberStats = remember { mutableStateOf(subscriberStats) },
400409
onUrlClick = {},
401410
onEmailClick = {},
402-
onPlanClick = {}
411+
onPlanClick = {},
412+
onDeleteClick = {}
403413
)
404414
}
405415
}

WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersActivity.kt

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.wordpress.android.ui.subscribers
22

33
import android.os.Build
44
import android.os.Bundle
5+
import android.widget.Toast
56
import androidx.activity.viewModels
67
import androidx.compose.foundation.layout.padding
78
import androidx.compose.material.icons.Icons
@@ -17,15 +18,20 @@ import androidx.compose.runtime.Composable
1718
import androidx.compose.runtime.collectAsState
1819
import androidx.compose.runtime.mutableStateOf
1920
import androidx.compose.runtime.remember
21+
import androidx.compose.runtime.saveable.rememberSaveable
2022
import androidx.compose.ui.Modifier
2123
import androidx.compose.ui.platform.ComposeView
2224
import androidx.compose.ui.platform.ViewCompositionStrategy
2325
import androidx.compose.ui.res.stringResource
26+
import androidx.lifecycle.lifecycleScope
2427
import androidx.navigation.NavHostController
2528
import androidx.navigation.compose.NavHost
2629
import androidx.navigation.compose.composable
2730
import androidx.navigation.compose.rememberNavController
31+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2832
import dagger.hilt.android.AndroidEntryPoint
33+
import kotlinx.coroutines.flow.filterNotNull
34+
import kotlinx.coroutines.launch
2935
import org.wordpress.android.R
3036
import org.wordpress.android.ui.ActivityLauncher
3137
import org.wordpress.android.ui.compose.theme.AppThemeM3
@@ -40,6 +46,7 @@ class SubscribersActivity : BaseAppCompatActivity() {
4046
private val addSubscribersViewModel by viewModels<AddSubscribersViewModel>()
4147

4248
private lateinit var composeView: ComposeView
49+
private lateinit var navController: NavHostController
4350

4451
override fun onCreate(savedInstanceState: Bundle?) {
4552
super.onCreate(savedInstanceState)
@@ -55,6 +62,24 @@ class SubscribersActivity : BaseAppCompatActivity() {
5562
}
5663
}
5764
)
65+
66+
lifecycleScope.launch {
67+
viewModel.uiEvent.filterNotNull().collect { event ->
68+
when (event) {
69+
is SubscribersViewModel.UiEvent.ShowDeleteConfirmationDialog -> {
70+
showDeleteConfirmationDialog(event.subscriber, navController)
71+
}
72+
73+
is SubscribersViewModel.UiEvent.ShowDeleteSuccessDialog -> {
74+
showDeleteSuccessDialog()
75+
}
76+
77+
is SubscribersViewModel.UiEvent.ShowToast -> {
78+
Toast.makeText(this@SubscribersActivity, event.messageRes, Toast.LENGTH_SHORT).show()
79+
}
80+
}
81+
}
82+
}
5883
}
5984

6085
private enum class SubscriberScreen {
@@ -67,10 +92,10 @@ class SubscribersActivity : BaseAppCompatActivity() {
6792
@OptIn(ExperimentalMaterial3Api::class)
6893
@Composable
6994
private fun NavigableContent() {
70-
val navController = rememberNavController()
95+
navController = rememberNavController()
7196
val listTitle = stringResource(R.string.subscribers)
7297
val titleState = remember { mutableStateOf(listTitle) }
73-
val showAddSubscribersButtonState = remember { mutableStateOf(true) }
98+
val showAddSubscribersButtonState = rememberSaveable { mutableStateOf(true) }
7499

75100
AppThemeM3 {
76101
Scaffold(
@@ -240,6 +265,11 @@ class SubscribersActivity : BaseAppCompatActivity() {
240265
)
241266
navController.navigate(route = SubscriberScreen.Plan.name)
242267
},
268+
onDeleteClick = { _ ->
269+
viewModel.onDeleteSubscriberClick(
270+
subscriber = subscriber,
271+
)
272+
},
243273
modifier = modifier,
244274
subscriberStats = viewModel.subscriberStats.collectAsState()
245275
)
@@ -265,6 +295,39 @@ class SubscribersActivity : BaseAppCompatActivity() {
265295
)
266296
}
267297

298+
/**
299+
* The user tapped the "Delete subscriber" button, so show a confirmation dialog
300+
* before telling the view model to actually delete the subscriber
301+
*/
302+
private fun showDeleteConfirmationDialog(
303+
subscriber: Subscriber,
304+
navController: NavHostController
305+
) {
306+
MaterialAlertDialogBuilder(this).also { builder ->
307+
builder.setTitle(R.string.subscribers_delete_confirmation_title)
308+
builder.setMessage(R.string.subscribers_delete_confirmation_message)
309+
builder.setPositiveButton(R.string.delete) { _, _ ->
310+
viewModel.deleteSubscriberConfirmed(
311+
subscriber = subscriber,
312+
onSuccess = {
313+
navController.navigateUp()
314+
}
315+
)
316+
}
317+
builder.setNegativeButton(R.string.cancel, null)
318+
builder.show()
319+
}
320+
}
321+
322+
private fun showDeleteSuccessDialog() {
323+
MaterialAlertDialogBuilder(this).also { builder ->
324+
builder.setTitle(R.string.subscribers_delete_success_title)
325+
builder.setMessage(R.string.subscribers_delete_success_message)
326+
builder.setPositiveButton(R.string.ok, null)
327+
builder.show()
328+
}
329+
}
330+
268331
private fun onEmailClick(email: String) {
269332
ActivityLauncher.openUrlExternal(this, "mailto:$email")
270333
}

WordPress/src/main/java/org/wordpress/android/ui/subscribers/SubscribersViewModel.kt

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import javax.inject.Named
3030

3131
@HiltViewModel
3232
class SubscribersViewModel @Inject constructor(
33-
@Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
33+
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
3434
private val appLogWrapper: AppLogWrapper,
3535
) : DataViewViewModel(
3636
mainDispatcher = mainDispatcher,
@@ -44,6 +44,15 @@ class SubscribersViewModel @Inject constructor(
4444
@Inject
4545
lateinit var dateFormatWrapper: SimpleDateFormatWrapper
4646

47+
sealed class UiEvent {
48+
data class ShowDeleteConfirmationDialog(val subscriber: Subscriber) : UiEvent()
49+
data object ShowDeleteSuccessDialog : UiEvent()
50+
data class ShowToast(val messageRes: Int) : UiEvent()
51+
}
52+
53+
private val _uiEvent = MutableStateFlow<UiEvent?>(null)
54+
val uiEvent = _uiEvent
55+
4756
override fun getSupportedFilters(): List<DataViewDropdownItem> {
4857
return listOf(
4958
DataViewDropdownItem(
@@ -175,7 +184,7 @@ class SubscribersViewModel @Inject constructor(
175184
)
176185
}
177186

178-
/*
187+
/**
179188
* Returns the subscriber with the given ID, or null if not found. Note that this does NOT do a network call,
180189
* it simply returns the subscriber from the existing list of items.
181190
*/
@@ -210,6 +219,39 @@ class SubscribersViewModel @Inject constructor(
210219
}
211220
}
212221

222+
private suspend fun deleteSubscriber(subscriber: Subscriber) = runCatching {
223+
withContext(ioDispatcher) {
224+
val response = if (subscriber.isEmailSubscriber) {
225+
wpComApiClient.request { requestBuilder ->
226+
requestBuilder.followers().deleteEmailFollower(
227+
wpComSiteId = siteId().toULong(),
228+
subscriptionId = subscriber.subscriptionId
229+
)
230+
}
231+
} else {
232+
wpComApiClient.request { requestBuilder ->
233+
requestBuilder.followers().deleteFollower(
234+
wpComSiteId = siteId().toULong(),
235+
userId = subscriber.userId
236+
)
237+
}
238+
}
239+
when (response) {
240+
is WpRequestResult.Success -> {
241+
appLogWrapper.d(AppLog.T.MAIN, "Delete subscriber success")
242+
Result.success(true)
243+
}
244+
245+
else -> {
246+
val error = (response as? WpRequestResult.WpError)?.errorMessage
247+
appLogWrapper.e(AppLog.T.MAIN, "Delete subscriber failed: $error")
248+
Result.failure(Exception(error))
249+
}
250+
}
251+
}
252+
}
253+
254+
213255
/**
214256
* Called when an item in the list is clicked. We use this to request stats for the clicked subscriber.
215257
*/
@@ -225,6 +267,41 @@ class SubscribersViewModel @Inject constructor(
225267
}
226268
}
227269

270+
/**
271+
* Trigger the delete confirmation dialog when the user taps the delete button for a subscriber
272+
*/
273+
fun onDeleteSubscriberClick(subscriber: Subscriber) {
274+
appLogWrapper.d(AppLog.T.MAIN, "Clicked on delete subscriber ${subscriber.displayNameOrEmail()}")
275+
_uiEvent.value = UiEvent.ShowDeleteConfirmationDialog(subscriber)
276+
clearUiEvent()
277+
}
278+
279+
/**
280+
* Subscriber deletion has been confirmed by the user so delete the subscriber
281+
*/
282+
fun deleteSubscriberConfirmed(subscriber: Subscriber, onSuccess: () -> Unit) {
283+
launch(ioDispatcher) {
284+
val result = deleteSubscriber(subscriber = subscriber)
285+
286+
withContext(mainDispatcher) {
287+
if (result.isSuccess) {
288+
// note that it may take a few seconds for the subscriber to actually be deleted,
289+
// which is why we only remove it locally instead of fetching the list again
290+
removeItem(subscriber.userId)
291+
_uiEvent.value = UiEvent.ShowDeleteSuccessDialog
292+
onSuccess()
293+
} else {
294+
_uiEvent.value = UiEvent.ShowToast(R.string.subscribers_delete_failed)
295+
}
296+
clearUiEvent()
297+
}
298+
}
299+
}
300+
301+
private fun clearUiEvent() {
302+
_uiEvent.value = null
303+
}
304+
228305
companion object {
229306
private const val ID_FILTER_EMAIL = 1L
230307
private const val ID_FILTER_READER = 2L

0 commit comments

Comments
 (0)