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

Issue:
Site Monitor: Add pull-to-refresh
Add Pull-to-refresh to the Webview pages


Discussion hints:
nan

Pull request: Implements: Pull to refresh in Site Monitor Tabs 
## Closes 
#20088 

## Description
This PR implements pull to refresh functionality in Site monitor Screen 

## Screenshots 
[Screen_recording_20240201_124924.webm](https://github.com/wordpress-mobile/WordPress-Android/assets/17463767/a3946842-df17-423e-b585-d932dccfc5ab)

## To Test:
#### Prerequisite
<details><summary>◐ Toggle the <code>site_monitoring</code> flag</summary>

1. Go to `Me` → `App Settings` → `Debug settings` → `Remote Feature Flags`
2. Tap on the item corresponding to the flag - site_monitoring
3. Tap `RESTART THE APP` button

</details>

###  Site monitor pull to refresh works as expected
1. Go to More → Site Monitoring 
2. i✅ Verify that the webview is loaded
3. 🖐🏼  Refresh the webview by swipe to refresh
4. ✅ Verify that the loading state is shown again 
5. ✅ Verify that only the current tab is refreshed(navigate to the other tabs, before refreshing and check if they still show webview)
6. ✅ Verify that the reload happens as expected

## Merge Instructions 
- Make sure that the PR - https://github.com/wordpress-mobile/WordPress-Android/pull/20092 is merged 
- Remove the Do not merge label 
- Confirm that the base branch is shifted to `trunk` and CI is successful
- Merge as normal 

## Regression Notes

1. Potential unintended areas of impact
- Swipe to refresh doesn't work as expected 
- First time load of the 

4. What I did to test those areas of impact (or what existing automated tests I relied on)
- TODO

5. What automated tests I added (or what prevented me from doing so)
- TODO


PR submission checklist:
- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] 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:
- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [x] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorParentViewModelTest" --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorTabViewModelSliceTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
index 3c015c61d3be..97278d29fc62 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
@@ -19,9 +19,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Tab
@@ -292,21 +296,30 @@ class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.
                 LoadingState()
             }
             is SiteMonitorUiState.Loaded -> {
-                SiteMonitorWebView(webView, modifier)
+                SiteMonitorWebView(webView, tabType, modifier)
             }
             else -> {}
         }
     }
 
+    @OptIn(ExperimentalMaterialApi::class)
     @Composable
-    private fun SiteMonitorWebView(tabWebView: WebView, modifier: Modifier = Modifier) {
+    private fun SiteMonitorWebView(tabWebView: WebView, tabType: SiteMonitorType, modifier: Modifier = Modifier) {
         // the webview is retrieved from the activity, so we need to use a mutable variable
         // to assign to android view
-
         var webView = tabWebView
+
+        val refreshState = siteMonitorParentViewModel.getRefreshState(tabType)
+
+        val pullRefreshState = rememberPullRefreshState(
+            refreshing = refreshState.value,
+            onRefresh = { siteMonitorParentViewModel.refreshData(tabType) }
+        )
+
         Box(
             modifier = modifier
                 .fillMaxSize()
+                .pullRefresh(pullRefreshState)
         ) {
             LazyColumn(modifier = Modifier.fillMaxHeight()) {
                 item {
@@ -317,6 +330,12 @@ class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.
                     )
                 }
             }
+            PullRefreshIndicator(
+                refreshing = refreshState.value,
+                state = pullRefreshState,
+                modifier = Modifier.align(Alignment.TopCenter),
+                contentColor = MaterialTheme.colors.primaryVariant,
+            )
         }
     }
 
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
index 2a18f07e1fb2..104800214320 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
@@ -92,4 +92,36 @@ class SiteMonitorParentViewModel @Inject constructor(
         phpLogViewModel.onCleared()
         webServerViewModel.onCleared()
     }
+
+    fun getRefreshState(siteMonitorType: SiteMonitorType): State<Boolean> {
+        return when (siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.isRefreshing
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.isRefreshing
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.isRefreshing
+            }
+        }
+    }
+
+    fun refreshData(siteMonitorType: SiteMonitorType) {
+        when(siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.refreshData()
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.refreshData()
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.refreshData()
+            }
+        }
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
index 45ec9a56d76f..91dea1294b59 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
@@ -3,22 +3,29 @@ package org.wordpress.android.ui.sitemonitor
 import android.text.TextUtils
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.fluxc.store.AccountStore
 import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.modules.BG_THREAD
 import org.wordpress.android.util.NetworkUtilsWrapper
 import javax.inject.Inject
+import javax.inject.Named
+
+const val REFRESH_DELAY = 500L
 
 class SiteMonitorTabViewModelSlice @Inject constructor(
+    @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
     private val networkUtilsWrapper: NetworkUtilsWrapper,
     private val accountStore: AccountStore,
     private val mapper: SiteMonitorMapper,
     private val siteMonitorUtils: SiteMonitorUtils,
     private val siteStore: SiteStore,
-){
+) {
     private lateinit var scope: CoroutineScope
 
     private lateinit var site: SiteModel
@@ -28,6 +35,9 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
     private val _uiState = mutableStateOf<SiteMonitorUiState>(SiteMonitorUiState.Preparing)
     val uiState: State<SiteMonitorUiState> = _uiState
 
+    private val _isRefreshing = mutableStateOf(false)
+    val isRefreshing: State<Boolean> = _isRefreshing
+
     fun initialize(scope: CoroutineScope) {
         this.scope = scope
     }
@@ -50,7 +60,19 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
         assembleAndShowSiteMonitor()
     }
 
-    private fun checkForInternetConnectivityAndPostErrorIfNeeded() : Boolean {
+    fun refreshData() {
+        scope.launch {
+            _isRefreshing.value = true
+            // this delay is to prevent the refresh from being too fast
+            // so that the user can see the refresh animation
+            // also this would fix the unit tests
+            delay(REFRESH_DELAY)
+            loadView()
+            _isRefreshing.value = false
+        }
+    }
+
+    private fun checkForInternetConnectivityAndPostErrorIfNeeded(): Boolean {
         if (networkUtilsWrapper.isNetworkAvailable()) return true
         postUiState(mapper.toNoNetworkError(::loadView))
         return false
@@ -68,8 +90,10 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
         val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url)
         val url = urlTemplate.replace("{blog}", sanitizedUrl)
 
-        val addressToLoad = prepareAddressToLoad(url)
-        postUiState(mapper.toPrepared(url, addressToLoad, siteMonitorType))
+        scope.launch(bgDispatcher) {
+            val addressToLoad = prepareAddressToLoad(url)
+            postUiState(mapper.toPrepared(url, addressToLoad, siteMonitorType))
+        }
     }
 
     private fun prepareAddressToLoad(url: String): String {
@@ -95,7 +119,7 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
             addressToLoad,
             username,
             "",
-            accessToken?:""
+            accessToken ?: ""
         )
     }
 
@@ -106,7 +130,7 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
     }
 
     fun onUrlLoaded() {
-        if (uiState.value is SiteMonitorUiState.Prepared){
+        if (uiState.value is SiteMonitorUiState.Prepared) {
             postUiState(SiteMonitorUiState.Loaded)
         }
     }


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
index 411f72a9a442..98d3ddf0e3a1 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
@@ -167,4 +167,25 @@ class SiteMonitorParentViewModelTest: BaseUnitTest(){
 
         verify(webServerViewModel).onWebViewError()
     }
+
+    @Test
+    fun `given metrics, when refresh is invoked, then metric vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.METRICS)
+
+        verify(metricsViewModel).refreshData()
+    }
+
+    @Test
+    fun `given php logs, when refresh is invoked, then php logs vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.PHP_LOGS)
+
+        verify(phpLogViewModel).refreshData()
+    }
+
+    @Test
+    fun `given webserver logs, when refresh is invoked, then webserver vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.WEB_SERVER_LOGS)
+
+        verify(webServerViewModel).refreshData()
+    }
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
index 3fe113c67fb7..02eb59f3a157 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
@@ -31,9 +31,12 @@ class SiteMonitorTabViewModelSliceTest : BaseUnitTest() {
 
     val site = mock<SiteModel>()
 
+    val refreshStates = mutableListOf<Boolean>()
+
     @Before
     fun setUp() = test {
         viewModel = SiteMonitorTabViewModelSlice(
+            testDispatcher(),
             networkUtilsWrapper,
             accountStore,
             mapper,
@@ -119,6 +122,19 @@ class SiteMonitorTabViewModelSliceTest : BaseUnitTest() {
         assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
     }
 
+    @Test
+    fun `given loaded  state, when refresh is invoked, then uiState loaded is posted`() = test {
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        advanceUntilIdle()
+        viewModel.onUrlLoaded()
+        viewModel.refreshData()
+
+        assertThat(viewModel.isRefreshing.value).isTrue()
+        advanceUntilIdle()
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Prepared::class.java)
+        assertThat(viewModel.isRefreshing.value).isFalse()
+    }
+
     companion object {
         const val USER_NAME = "user_name"
         const val ACCESS_TOKEN = "access_token"