Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
🤖 [Dynamic Cards] Dismissing card from a dropdown menu
After tapping on the ••• menu, a dropdown menu should appear with a "hiding the card" option. After tapping on the card it has to be removed from the screen permanently.

![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/16563318/260eec1f-03fd-491d-a39c-8a7349527f0e)




Discussion hints:
Hi,
I want to work on this issue.
> Hi,
> I want to work on this issue.

Thank you for offering to help on this @malirauf 🙇 
Since this issue is part of a feature that is still work in progress it might be hard to coordinate on the implementation. Would you like to have a look at [an issue from this list ](https://github.com/wordpress-mobile/WordPress-Android/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22)?


Pull request: [Dynamic Cards] Hide dynamic card through a card menu
Fixes #19776

This PR includes a dynamic card's dropdown menu UI and local hiding dynamic cards logic.

![ezgif-2-d23e3d0a64](https://github.com/wordpress-mobile/WordPress-Android/assets/16563318/cc6ac271-4a52-4562-8737-ae547f1375ae)

-----

## To Test:

- Apply this patch: [cardmockspatch.patch](https://github.com/wordpress-mobile/WordPress-Android/files/13690106/cardmockspatch.patch) and run the app.
- Make sure you can see two dynamic cards on the top and bottom of the dashboard.
- Tap on a card's menu and select "Hide it" option.
- Make sure the card disappears permanently - it's not shown after restarting the app.

-----

## Regression Notes

1. Potential unintended areas of impact

    - My Site Dashboard screen and a dynamic dashboard card

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing

3. What automated tests I added (or what prevented me from doing so)

    - N/A

-----

## PR Submission Checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.mysite.cards.dynamiccard.DynamicCardsViewModelSliceTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index 68af2a267ddf..1d6db4d0c792 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -250,6 +250,7 @@ class MySiteViewModel @Inject constructor(
             activityLogCardViewModelSlice.refresh,
             bloganuaryNudgeCardViewModelSlice.refresh,
             sotw2023NudgeCardViewModelSlice.refresh,
+            dynamicCardsViewModelSlice.refresh,
         )
 
     private var shouldMarkUpdateSiteTitleTaskComplete = false
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
index ab9556785b06..3ed80d4e2c1b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
@@ -26,7 +26,7 @@ fun DynamicDashboardCard(
             },
         content = {
             Column(
-                Modifier.padding(top = 8.dp, bottom = 8.dp)
+                Modifier.padding(bottom = 8.dp)
             ) {
                 val isCtaInvisible = card.action !is ActionSource.Button
                 DynamicCardHeader(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
index f6dcbb711854..be2838e43c7d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
@@ -1,23 +1,16 @@
 package org.wordpress.android.ui.mysite.cards.dynamiccard
 
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.MoreVert
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import org.wordpress.android.R
 import org.wordpress.android.ui.compose.styles.DashboardCardTypography
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem
 
 @Composable
 fun DynamicCardHeader(
@@ -25,12 +18,16 @@ fun DynamicCardHeader(
     onHideMenuClicked: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    Row(
-        modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
-        verticalAlignment = Alignment.CenterVertically
+    MySiteCardToolbar(
+        modifier = modifier.padding(bottom = 8.dp),
+        contextMenuItems = listOf(
+            MySiteCardToolbarContextMenuItem.Option(
+                text = stringResource(R.string.my_site_dashboard_card_more_menu_hide_card),
+                onClick = onHideMenuClicked,
+            )
+        ),
     ) {
         Title(title = title, modifier = Modifier.weight(1f))
-        Menu(onHideMenuClicked)
     }
 }
 
@@ -45,17 +42,3 @@ private fun Title(title: String?, modifier: Modifier = Modifier) {
         }
     )
 }
-
-@Composable
-private fun Menu(onHideMenuClicked: () -> Unit) {
-    IconButton(
-        modifier = Modifier.size(32.dp),
-        onClick = onHideMenuClicked
-    ) {
-        Icon(
-            imageVector = Icons.Rounded.MoreVert,
-            contentDescription = stringResource(id = R.string.more),
-            tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
-        )
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
index 8def20febd51..e1bff394e03a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
@@ -5,11 +5,14 @@ import androidx.lifecycle.MutableLiveData
 import org.wordpress.android.fluxc.model.dashboard.CardModel
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams
 import org.wordpress.android.ui.mysite.SiteNavigationAction
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.util.AppLog
 import org.wordpress.android.viewmodel.Event
 import javax.inject.Inject
 
-class DynamicCardsViewModelSlice @Inject constructor() {
+class DynamicCardsViewModelSlice @Inject constructor(
+    private val appPrefsWrapper: AppPrefsWrapper
+) {
     private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
     val onNavigation = _onNavigation as LiveData<Event<SiteNavigationAction>>
 
@@ -17,7 +20,7 @@ class DynamicCardsViewModelSlice @Inject constructor() {
     val refresh = _refresh as LiveData<Event<Boolean>>
     fun getBuilderParams(dynamicCards: CardModel.DynamicCardsModel?): DynamicCardsBuilderParams {
         return DynamicCardsBuilderParams(
-            dynamicCards = dynamicCards,
+            dynamicCards = dynamicCards?.filterVisible(),
             onActionClick = this::onActionClick,
             onMoreMenuClick = this::onMoreMenuClick,
             onHideMenuItemClick = this::onHideMenuItemClick
@@ -35,7 +38,10 @@ class DynamicCardsViewModelSlice @Inject constructor() {
     }
 
     private fun onHideMenuItemClick(cardId: String) {
-        AppLog.d(AppLog.T.MY_SITE_DASHBOARD, "Dynamic dashboard card action hide menu click: $cardId")
-        // TODO
+        appPrefsWrapper.setShouldHideDynamicCard(cardId, true)
+        _refresh.value = Event(true)
     }
+
+    private fun CardModel.DynamicCardsModel.filterVisible(): CardModel.DynamicCardsModel =
+        copy(dynamicCards = dynamicCards.filterNot { appPrefsWrapper.getShouldHideDynamicCard(it.id) })
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
index f66e0418093e..a9d199817ab2 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -210,6 +210,7 @@ public enum DeletablePrefKey implements PrefKey {
         SHOULD_SHOW_DEFAULT_QUICK_LINK_IN_DASHBOARD,
         SHOULD_HIDE_BLOGANUARY_NUDGE_CARD,
         SHOULD_HIDE_SOTW2023_NUDGE_CARD,
+        SHOULD_HIDE_DYNAMIC_CARD,
     }
 
     /**
@@ -1836,4 +1837,12 @@ public static void setShouldHideSotw2023NudgeCard(boolean isHidden) {
     public static boolean getShouldHideSotw2023NudgeCard() {
         return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), false);
     }
+
+    public static void setShouldHideDynamicCard(@NonNull final String id, final boolean isHidden) {
+        prefs().edit().putBoolean(DeletablePrefKey.SHOULD_HIDE_DYNAMIC_CARD.name() + id, isHidden).apply();
+    }
+
+    public static boolean getShouldHideDynamicCard(@NonNull final String id) {
+        return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_DYNAMIC_CARD.name() + id, false);
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
index 307e6772e1ff..10b8ef20cd04 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
@@ -434,6 +434,12 @@ class AppPrefsWrapper @Inject constructor() {
     fun getShouldHideSotw2023NudgeCard(): Boolean =
         AppPrefs.getShouldHideSotw2023NudgeCard()
 
+    fun setShouldHideDynamicCard(id: String, isHidden: Boolean): Unit =
+        AppPrefs.setShouldHideDynamicCard(id, isHidden)
+
+    fun getShouldHideDynamicCard(id: String, ): Boolean =
+        AppPrefs.getShouldHideDynamicCard(id)
+
     fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()
 
     fun setString(prefKey: PrefKey, value: String) {


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
index 2a0628a5429b..2528fad23c30 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
@@ -4,13 +4,19 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.fluxc.model.dashboard.CardModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardRowModel
 import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardModel
+import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardRowModel
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
 
 /* DYNAMIC CARDS */
-private const val DYNAMIC_CARD_ID = "year_in_review_2023"
+private const val DYNAMIC_CARD_ID_FIRST = "year_in_review_2023"
+private const val DYNAMIC_CARD_ID_SECOND = "domain_management"
 private const val DYNAMIC_CARD_TITLE = "News"
 private const val DYNAMIC_CARD_REMOTE_FEATURE_FLAG = "dynamic_dashboard_cards"
 private const val DYNAMIC_CARD_FEATURED_IMAGE = "https://path/to/image"
@@ -21,39 +27,72 @@ private const val DYNAMIC_CARD_ROW_ICON = "https://path/to/image"
 private const val DYNAMIC_CARD_ROW_TITLE = "Row title"
 private const val DYNAMIC_CARD_ROW_DESCRIPTION = "Row description"
 
-private val DYNAMIC_CARD_ROW_MODEL = DynamicCardRowModel(
+private val dynamicCardRowModel = DynamicCardRowModel(
     icon = DYNAMIC_CARD_ROW_ICON,
     title = DYNAMIC_CARD_ROW_TITLE,
-    description = DYNAMIC_CARD_ROW_DESCRIPTION
+    description = DYNAMIC_CARD_ROW_DESCRIPTION,
 )
 
-private val DYNAMIC_CARD_MODEL = DynamicCardModel(
-    id = DYNAMIC_CARD_ID,
+private val firstDynamicCardModel = DynamicCardModel(
+    id = DYNAMIC_CARD_ID_FIRST,
     title = DYNAMIC_CARD_TITLE,
     remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = DYNAMIC_CARD_URL,
     action = DYNAMIC_CARD_ACTION,
     order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
-    rows = listOf(DYNAMIC_CARD_ROW_MODEL)
+    rows = listOf(dynamicCardRowModel),
+)
+
+private val secondDynamicCardModel = DynamicCardModel(
+    id = DYNAMIC_CARD_ID_SECOND,
+    title = null,
+    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    featuredImage = null,
+    url = null,
+    action = null,
+    order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
+    rows = emptyList(),
+)
+
+private val dynamicCardsModel = CardModel.DynamicCardsModel(
+    dynamicCards = listOf(firstDynamicCardModel, secondDynamicCardModel),
 )
 
-private val DYNAMIC_CARDS_MODEL = CardModel.DynamicCardsModel(
-    dynamicCards = listOf(DYNAMIC_CARD_MODEL)
+private val filteredDynamicCardsModel = CardModel.DynamicCardsModel(
+    dynamicCards = listOf(firstDynamicCardModel),
 )
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class DynamicCardsViewModelSliceTest : BaseUnitTest() {
-    lateinit var dynamicCardsViewModelSlice: DynamicCardsViewModelSlice
+    @Mock
+    private lateinit var appPrefsWrapper: AppPrefsWrapper
+    private lateinit var viewModelSlice: DynamicCardsViewModelSlice
 
     @Before
     fun setUp() {
-        dynamicCardsViewModelSlice = DynamicCardsViewModelSlice()
+        MockitoAnnotations.openMocks(this)
+        viewModelSlice = DynamicCardsViewModelSlice(appPrefsWrapper)
     }
 
     @Test
-    fun testGetBuilderParams() {
-        val builderParams = dynamicCardsViewModelSlice.getBuilderParams(DYNAMIC_CARDS_MODEL)
-        assertThat(builderParams.dynamicCards).isEqualTo(DYNAMIC_CARDS_MODEL)
+    fun `WHEN get builder params THEN return only not hidden cards only`() {
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST)).thenReturn(false)
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_SECOND)).thenReturn(true)
+
+        val builderParams = viewModelSlice.getBuilderParams(dynamicCardsModel)
+        assertThat(builderParams.dynamicCards).isEqualTo(filteredDynamicCardsModel)
+    }
+
+    @Test
+    fun `WHEN card hide menu item clicked THEN hide card locally and refresh`() {
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST)).thenReturn(false)
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_SECOND)).thenReturn(true)
+
+        val builderParams = viewModelSlice.getBuilderParams(dynamicCardsModel)
+        builderParams.onHideMenuItemClick.invoke(DYNAMIC_CARD_ID_FIRST)
+
+        verify(appPrefsWrapper).setShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST, true)
+        assertThat(viewModelSlice.refresh.value?.peekContent()).isTrue
     }
 }