Skip to content

Commit

Permalink
Merge pull request #20094 from wordpress-mobile/issue/20088-site-moni…
Browse files Browse the repository at this point in the history
…tor-add-pull-to-refresh

Implements: Pull to refresh in Site Monitor Tabs
  • Loading branch information
pantstamp committed Feb 1, 2024
2 parents 96b6b96 + 34b6d4a commit 5c886c3
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -294,21 +298,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 {
Expand All @@ -319,6 +332,12 @@ class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.
)
}
}
PullRefreshIndicator(
refreshing = refreshState.value,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
contentColor = MaterialTheme.colors.primaryVariant,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -95,7 +119,7 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
addressToLoad,
username,
"",
accessToken?:""
accessToken ?: ""
)
}

Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 5c886c3

Please sign in to comment.