From d4b7ff562b2e5c23eb6cd799052f92e6f87776f8 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Fri, 12 Jul 2024 20:55:13 +0500 Subject: [PATCH 1/5] feat: Course Level Error Handling for Empty States - Empty state for Home, Videos, Dates, and Discussion tabs. - Empty state for handouts, and Announcements screen. fixes: LEARNER-10039 --- .../java/org/openedx/core/ui/ComposeCommon.kt | 41 ++++ .../main/res/drawable/core_ic_no_content.xml | 11 + .../presentation/dates/CourseDatesScreen.kt | 38 ++-- .../dates/CourseDatesViewModel.kt | 1 + .../presentation/dates/DashboardUIState.kt | 9 +- .../presentation/handouts/HandoutsUIState.kt | 6 + .../handouts/HandoutsViewModel.kt | 29 ++- .../handouts/HandoutsWebViewFragment.kt | 215 ++++++++++++++++-- .../outline/CourseOutlineScreen.kt | 8 + .../course/presentation/ui/CourseVideosUI.kt | 27 +-- .../videos/CourseVideoViewModel.kt | 4 +- .../videos/CourseVideosUIState.kt | 4 +- .../drawable/course_ic_no_announcements.xml | 11 + .../res/drawable/course_ic_no_content.xml | 11 + .../res/drawable/course_ic_no_handouts.xml | 11 + .../main/res/drawable/course_ic_no_videos.xml | 11 + course/src/main/res/values-uk/strings.xml | 1 - course/src/main/res/values/strings.xml | 5 +- .../handouts/HandoutsViewModelTest.kt | 15 +- .../videos/CourseVideoViewModelTest.kt | 1 - .../topics/DiscussionTopicsScreen.kt | 62 +++-- .../topics/DiscussionTopicsUIState.kt | 3 +- .../topics/DiscussionTopicsViewModel.kt | 7 +- discussion/src/main/res/values/strings.xml | 1 + 24 files changed, 424 insertions(+), 108 deletions(-) create mode 100644 core/src/main/res/drawable/core_ic_no_content.xml create mode 100644 course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt create mode 100644 course/src/main/res/drawable/course_ic_no_announcements.xml create mode 100644 course/src/main/res/drawable/course_ic_no_content.xml create mode 100644 course/src/main/res/drawable/course_ic_no_handouts.xml create mode 100644 course/src/main/res/drawable/course_ic_no_videos.xml diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 26806897f..fc8756258 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -31,10 +31,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider @@ -49,6 +51,7 @@ import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable @@ -1179,6 +1182,33 @@ fun ConnectionErrorView( } } +@Composable +fun NoContentScreen(message: String, icon: Painter) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(80.dp), + painter = icon, + contentDescription = null, + tint = MaterialTheme.appColors.progressBarBackgroundColor, + ) + Spacer(Modifier.height(24.dp)) + Text( + modifier = Modifier.fillMaxWidth(0.8f), + text = message, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } +} + @Composable fun AuthButtonsPanel( onRegisterClick: () -> Unit, @@ -1390,3 +1420,14 @@ private fun RoundTabsBarPreview() { ) } } + +@Preview +@Composable +private fun PreviewNoContentScreen() { + OpenEdXTheme(darkTheme = true) { + NoContentScreen( + "No Content available", + rememberVectorPainter(image = Icons.Filled.Info) + ) + } +} diff --git a/core/src/main/res/drawable/core_ic_no_content.xml b/core/src/main/res/drawable/core_ic_no_content.xml new file mode 100644 index 000000000..94a134d7e --- /dev/null +++ b/core/src/main/res/drawable/core_ic_no_content.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index 76197b93c..29b230b39 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -57,7 +57,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -76,6 +75,7 @@ import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.displayCutoutForLandscape @@ -306,18 +306,10 @@ private fun CourseDatesUI( } DatesUIState.Empty -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.course_dates_unavailable_message), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleMedium, - textAlign = TextAlign.Center - ) - } + NoContentScreen( + message = stringResource(id = R.string.course_dates_unavailable_message), + icon = painterResource(id = R.drawable.course_ic_no_content) + ) } DatesUIState.Loading -> {} @@ -643,6 +635,26 @@ private fun CourseDateItem( } } + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EmptyCourseDatesScreenPreview() { + OpenEdXTheme { + CourseDatesUI( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + uiState = DatesUIState.Empty, + uiMessage = null, + isSelfPaced = true, + calendarSyncUIState = mockCalendarSyncUIState, + onItemClick = {}, + onPLSBannerViewed = {}, + onSyncDates = {}, + onCalendarSyncSwitch = {}, + ) + } +} + @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index 4d6236b67..95e9b4b0c 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -117,6 +117,7 @@ class CourseDatesViewModel( } else { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_unknown_error))) } + _uiState.value = DatesUIState.Empty } finally { courseNotifier.send(CourseLoading(false)) } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt index 8ff75239f..7d3dc3cb3 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt @@ -3,10 +3,7 @@ package org.openedx.course.presentation.dates import org.openedx.core.domain.model.CourseDatesResult sealed class DatesUIState { - data class Dates( - val courseDatesResult: CourseDatesResult, - ) : DatesUIState() - - object Empty : DatesUIState() - object Loading : DatesUIState() + data class Dates(val courseDatesResult: CourseDatesResult) : DatesUIState() + data object Empty : DatesUIState() + data object Loading : DatesUIState() } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt new file mode 100644 index 000000000..6c22b7de7 --- /dev/null +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt @@ -0,0 +1,6 @@ +package org.openedx.course.presentation.handouts + +sealed class HandoutsUIState { + data class HTMLContent(val htmlContent: String) : HandoutsUIState() + data object Error : HandoutsUIState() +} diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index 92aaa139d..c80e1049e 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -1,8 +1,9 @@ package org.openedx.course.presentation.handouts -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.config.Config @@ -23,9 +24,9 @@ class HandoutsViewModel( val apiHostUrl get() = config.getApiHostURL() - private val _htmlContent = MutableLiveData() - val htmlContent: LiveData - get() = _htmlContent + private val _uiState = MutableStateFlow(HandoutsUIState.HTMLContent("")) + val uiState: StateFlow + get() = _uiState.asStateFlow() init { getEnrolledCourse() @@ -33,16 +34,30 @@ class HandoutsViewModel( private fun getEnrolledCourse() { viewModelScope.launch { + var emptyState = false try { if (HandoutsType.valueOf(handoutsType) == HandoutsType.Handouts) { val handouts = interactor.getHandouts(courseId) - _htmlContent.value = handoutsToHtml(handouts) + if (handouts.handoutsHtml.isNotBlank()) { + _uiState.value = HandoutsUIState.HTMLContent(handoutsToHtml(handouts)) + } else { + emptyState = true + } } else { val announcements = interactor.getAnnouncements(courseId) - _htmlContent.value = announcementsToHtml(announcements) + if (announcements.isNotEmpty()) { + _uiState.value = + HandoutsUIState.HTMLContent(announcementsToHtml(announcements)) + } else { + emptyState = true + } } } catch (e: Exception) { //ignore e.printStackTrace() + emptyState = true + } + if (emptyState) { + _uiState.value = HandoutsUIState.Error } } } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt index 16cc67b84..df50a078c 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt @@ -4,24 +4,49 @@ import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.openedx.core.ui.NoContentScreen +import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WebContentScreen import org.openedx.core.ui.WindowSize -import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.windowSizeValue import org.openedx.course.R import org.openedx.course.presentation.CourseAnalyticsEvent @@ -51,31 +76,32 @@ class HandoutsWebViewFragment : Fragment() { setContent { OpenEdXTheme { - val windowSize = rememberWindowSize() - - val htmlBody by viewModel.htmlContent.observeAsState("") val colorBackgroundValue = MaterialTheme.appColors.background.value val colorTextValue = MaterialTheme.appColors.textPrimary.value - - WebContentScreen( - windowSize = windowSize, - apiHostUrl = viewModel.apiHostUrl, + val uiState by viewModel.uiState.collectAsState() + HandoutsScreens( + handoutType = HandoutsType.valueOf(viewModel.handoutsType), + uiState = uiState, title = title, - htmlBody = viewModel.injectDarkMode( - htmlBody, - colorBackgroundValue, - colorTextValue - ), + apiHostUrl = viewModel.apiHostUrl, + onInjectDarkMode = { + viewModel.injectDarkMode( + (uiState as HandoutsUIState.HTMLContent).htmlContent, + colorBackgroundValue, + colorTextValue + ) + }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() - }) + } + ) } } } companion object { - private val ARG_TYPE = "argType" - private val ARG_COURSE_ID = "argCourse" + private const val ARG_TYPE = "argType" + private const val ARG_COURSE_ID = "argCourse" fun newInstance( type: String, @@ -91,24 +117,163 @@ class HandoutsWebViewFragment : Fragment() { } } +@Composable +fun HandoutsScreens( + handoutType: HandoutsType, + uiState: HandoutsUIState, + title: String, + apiHostUrl: String, + onInjectDarkMode: () -> String, + onBackClick: () -> Unit +) { + val windowSize = rememberWindowSize() + when (uiState) { + is HandoutsUIState.HTMLContent -> { + WebContentScreen( + windowSize = windowSize, + apiHostUrl = apiHostUrl, + title = title, + htmlBody = onInjectDarkMode(), + onBackClick = onBackClick + ) + } + + HandoutsUIState.Error -> { + HandoutsEmptyScreen( + windowSize = windowSize, + handoutType = handoutType, + title = title, + onBackClick = onBackClick + ) + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun HandoutsEmptyScreen( + windowSize: WindowSize, + handoutType: HandoutsType, + title: String, + onBackClick: () -> Unit +) { + val messageResId = + if (handoutType == HandoutsType.Handouts) R.string.course_no_handouts else R.string.course_no_announcements + val iconRedId = + if (handoutType == HandoutsType.Handouts) R.drawable.course_ic_no_handouts else R.drawable.course_ic_no_announcements + + val scaffoldState = rememberScaffoldState() + Scaffold( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 24.dp) + .semantics { + testTagsAsResourceId = true + }, + scaffoldState = scaffoldState, + backgroundColor = MaterialTheme.appColors.background + ) { + + val screenWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier.fillMaxWidth() + ) + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(it) + .statusBarsInset() + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column(screenWidth) { + Box( + Modifier + .fillMaxWidth() + .zIndex(1f), + contentAlignment = Alignment.CenterStart + ) { + Toolbar( + label = title, + canShowBackBtn = true, + onBackClick = onBackClick + ) + } + Surface( + Modifier.fillMaxSize(), + color = MaterialTheme.appColors.background + ) { + NoContentScreen( + message = stringResource(id = messageResId), + icon = painterResource(id = iconRedId) + ) + } + } + } + } +} + @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun WebContentScreenPreview() { - WebContentScreen( - windowSize = WindowSize(WindowType.Compact, WindowType.Compact), +fun HandoutsScreensPreview() { + HandoutsScreens( + handoutType = HandoutsType.Handouts, + uiState = HandoutsUIState.HTMLContent(htmlContent = ""), + title = "Handouts", apiHostUrl = "http://localhost:8000", - title = "Handouts", onBackClick = { }, htmlBody = "" + onInjectDarkMode = { "" }, + onBackClick = { } ) } @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable -fun WebContentScreenTabletPreview() { - WebContentScreen( - windowSize = WindowSize(WindowType.Medium, WindowType.Medium), +fun HandoutsScreensTabletPreview() { + HandoutsScreens( + handoutType = HandoutsType.Handouts, + uiState = HandoutsUIState.HTMLContent(htmlContent = ""), + title = "Handouts", apiHostUrl = "http://localhost:8000", - title = "Handouts", onBackClick = { }, htmlBody = "" + onInjectDarkMode = { "" }, + onBackClick = { } ) } + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun EmptyHandoutsScreensPreview() { + OpenEdXTheme(darkTheme = true) { + HandoutsScreens( + handoutType = HandoutsType.Handouts, + uiState = HandoutsUIState.Error, + title = "Handouts", + apiHostUrl = "http://localhost:8000", + onInjectDarkMode = { "" }, + onBackClick = { } + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun EmptyAnnouncementsScreensPreview() { + OpenEdXTheme(darkTheme = true) { + HandoutsScreens( + handoutType = HandoutsType.Announcements, + uiState = HandoutsUIState.Error, + title = "Handouts", + apiHostUrl = "http://localhost:8000", + onInjectDarkMode = { "" }, + onBackClick = { } + ) + } +} diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index 1f31b32de..1f0499803 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -56,6 +56,7 @@ import org.openedx.core.domain.model.Progress import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.TextIcon import org.openedx.core.ui.WindowSize @@ -222,6 +223,13 @@ private fun CourseOutlineUI( Box { when (uiState) { is CourseOutlineUIState.CourseData -> { + if (uiState.courseStructure.blockData.isEmpty()) { + NoContentScreen( + stringResource(id = R.string.course_no_course_content), + painterResource(id = R.drawable.course_ic_no_content) + ) + return@Surface + } LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = listBottomPadding diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index 1a406181d..28d78317a 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -18,8 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.AlertDialog import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -47,8 +45,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -69,6 +67,7 @@ import org.openedx.core.module.download.DownloadModelsSize import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.displayCutoutForLandscape @@ -233,20 +232,10 @@ private fun CourseVideosUI( ) { when (uiState) { is CourseVideosUIState.Empty -> { - Box( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(id = R.string.course_does_not_include_videos), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.headlineSmall, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 40.dp) - ) - } + NoContentScreen( + stringResource(id = R.string.course_no_videos), + painterResource(id = R.drawable.course_ic_no_videos) + ) } is CourseVideosUIState.CourseData -> { @@ -646,9 +635,7 @@ private fun CourseVideosScreenEmptyPreview() { CourseVideosUI( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiMessage = null, - uiState = CourseVideosUIState.Empty( - "This course does not include any videos." - ), + uiState = CourseVideosUIState.Empty, courseTitle = "", onExpandClick = { }, onSubSectionClick = { }, diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index a5bf069cd..352fe72a3 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -148,9 +148,7 @@ class CourseVideoViewModel( var courseStructure = interactor.getCourseStructureForVideos(courseId) val blocks = courseStructure.blockData if (blocks.isEmpty()) { - _uiState.value = CourseVideosUIState.Empty( - message = resourceManager.getString(R.string.course_does_not_include_videos) - ) + _uiState.value = CourseVideosUIState.Empty } else { setBlocks(courseStructure.blockData) courseSubSections.clear() diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosUIState.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosUIState.kt index ce05913d6..8d1a4f60d 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosUIState.kt @@ -15,6 +15,6 @@ sealed class CourseVideosUIState { val downloadModelsSize: DownloadModelsSize ) : CourseVideosUIState() - data class Empty(val message: String) : CourseVideosUIState() - object Loading : CourseVideosUIState() + data object Empty : CourseVideosUIState() + data object Loading : CourseVideosUIState() } diff --git a/course/src/main/res/drawable/course_ic_no_announcements.xml b/course/src/main/res/drawable/course_ic_no_announcements.xml new file mode 100644 index 000000000..fc85b3fe1 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_no_announcements.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/course/src/main/res/drawable/course_ic_no_content.xml b/course/src/main/res/drawable/course_ic_no_content.xml new file mode 100644 index 000000000..94a134d7e --- /dev/null +++ b/course/src/main/res/drawable/course_ic_no_content.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/course/src/main/res/drawable/course_ic_no_handouts.xml b/course/src/main/res/drawable/course_ic_no_handouts.xml new file mode 100644 index 000000000..d1f19a3d3 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_no_handouts.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/course/src/main/res/drawable/course_ic_no_videos.xml b/course/src/main/res/drawable/course_ic_no_videos.xml new file mode 100644 index 000000000..f8a55d1b9 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_no_videos.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/course/src/main/res/values-uk/strings.xml b/course/src/main/res/values-uk/strings.xml index 14f3487c4..ecba8ed56 100644 --- a/course/src/main/res/values-uk/strings.xml +++ b/course/src/main/res/values-uk/strings.xml @@ -12,7 +12,6 @@ Далі Наступна одиниця Завершити - Цей курс не містить відео. Остання одиниця: Продовжити Обговорення diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 51ac39e95..9e5473d10 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -12,7 +12,6 @@ Next Next Unit Finish - This course does not include any videos. Last unit: Resume Discussion @@ -37,6 +36,10 @@ Some content in this part of the course is locked for upgraded users only. You cannot change the download video quality when all videos are downloading Dates Shifted + No course content is currently available. + There are currently no videos for this course. + There are currently no handouts for this course. + There are currently no announcements for this course. Course dates are not currently available. diff --git a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt index 6e8d2dab2..7a92f8c8b 100644 --- a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt @@ -5,12 +5,10 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.* import org.junit.After -import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test @@ -20,7 +18,6 @@ import org.openedx.core.domain.model.* import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import java.net.UnknownHostException -import java.util.* @OptIn(ExperimentalCoroutinesApi::class) class HandoutsViewModelTest { @@ -57,7 +54,7 @@ class HandoutsViewModelTest { coEvery { interactor.getHandouts(any()) } throws UnknownHostException() advanceUntilIdle() - assert(viewModel.htmlContent.value == null) + assert(viewModel.uiState.value == null) } @Test @@ -66,7 +63,7 @@ class HandoutsViewModelTest { coEvery { interactor.getHandouts(any()) } throws Exception() advanceUntilIdle() - assert(viewModel.htmlContent.value == null) + assert(viewModel.uiState.value == null) } @Test @@ -79,7 +76,7 @@ class HandoutsViewModelTest { coVerify(exactly = 1) { interactor.getHandouts(any()) } coVerify(exactly = 0) { interactor.getAnnouncements(any()) } - assert(viewModel.htmlContent.value != null) + assert(viewModel.uiState.value != null) } @Test @@ -97,7 +94,7 @@ class HandoutsViewModelTest { coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } - assert(viewModel.htmlContent.value != null) + assert(viewModel.uiState.value != null) } @Test @@ -111,7 +108,7 @@ class HandoutsViewModelTest { ) ) viewModel.injectDarkMode( - viewModel.htmlContent.value.toString(), + viewModel.uiState.value.toString(), ULong.MAX_VALUE, ULong.MAX_VALUE ) @@ -119,6 +116,6 @@ class HandoutsViewModelTest { coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } - assert(viewModel.htmlContent.value != null) + assert(viewModel.uiState.value != null) } } diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index 9bb8d0f5f..f297ef8e9 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -183,7 +183,6 @@ class CourseVideoViewModelTest { @Before fun setUp() { - every { resourceManager.getString(R.string.course_does_not_include_videos) } returns "" every { resourceManager.getString(R.string.course_can_download_only_with_wifi) } returns cantDownload Dispatchers.setMain(dispatcher) every { config.getApiHostURL() } returns "http://localhost:8000" diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt index 62ec564b6..e9788b25e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt @@ -37,10 +37,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager -import org.koin.androidx.compose.koinViewModel import org.openedx.core.FragmentViewType import org.openedx.core.UIMessage import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.StaticSearchBar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -51,10 +51,11 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue +import org.openedx.discussion.R import org.openedx.discussion.domain.model.Topic import org.openedx.discussion.presentation.ui.ThreadItemCategory import org.openedx.discussion.presentation.ui.TopicItem -import org.openedx.discussion.R as discussionR +import org.openedx.core.R as CoreR @Composable fun DiscussionTopicsScreen( @@ -157,15 +158,17 @@ private fun DiscussionTopicsUI( contentAlignment = Alignment.TopCenter ) { Column(screenWidth) { - StaticSearchBar( - modifier = Modifier - .height(48.dp) - .then(searchTabWidth) - .padding(horizontal = contentPaddings) - .fillMaxWidth(), - text = stringResource(id = discussionR.string.discussion_search_all_posts), - onClick = onSearchClick - ) + if ((uiState is DiscussionTopicsUIState.Error).not()) { + StaticSearchBar( + modifier = Modifier + .height(48.dp) + .then(searchTabWidth) + .padding(horizontal = contentPaddings) + .fillMaxWidth(), + text = stringResource(id = R.string.discussion_search_all_posts), + onClick = onSearchClick + ) + } Surface( modifier = Modifier.padding(top = 10.dp), color = MaterialTheme.appColors.background, @@ -188,7 +191,7 @@ private fun DiscussionTopicsUI( item { Text( modifier = Modifier, - text = stringResource(id = discussionR.string.discussion_main_categories), + text = stringResource(id = R.string.discussion_main_categories), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textPrimaryVariant ) @@ -199,8 +202,8 @@ private fun DiscussionTopicsUI( horizontalArrangement = Arrangement.spacedBy(14.dp) ) { ThreadItemCategory( - name = stringResource(id = discussionR.string.discussion_all_posts), - painterResource = painterResource(id = discussionR.drawable.discussion_all_posts), + name = stringResource(id = R.string.discussion_all_posts), + painterResource = painterResource(id = R.drawable.discussion_all_posts), modifier = Modifier .weight(1f) .height(categoriesHeight), @@ -208,12 +211,12 @@ private fun DiscussionTopicsUI( onItemClick( DiscussionTopicsViewModel.ALL_POSTS, "", - context.getString(discussionR.string.discussion_all_posts) + context.getString(R.string.discussion_all_posts) ) }) ThreadItemCategory( - name = stringResource(id = discussionR.string.discussion_posts_following), - painterResource = painterResource(id = discussionR.drawable.discussion_star), + name = stringResource(id = R.string.discussion_posts_following), + painterResource = painterResource(id = R.drawable.discussion_star), modifier = Modifier .weight(1f) .height(categoriesHeight), @@ -221,7 +224,7 @@ private fun DiscussionTopicsUI( onItemClick( DiscussionTopicsViewModel.FOLLOWING_POSTS, "", - context.getString(discussionR.string.discussion_posts_following) + context.getString(R.string.discussion_posts_following) ) }) } @@ -253,6 +256,12 @@ private fun DiscussionTopicsUI( } DiscussionTopicsUIState.Loading -> {} + else -> { + NoContentScreen( + message = stringResource(id = R.string.discussion_unable_to_load), + icon = painterResource(id = CoreR.drawable.core_ic_no_content) + ) + } } } } @@ -279,6 +288,23 @@ private fun DiscussionTopicsScreenPreview() { } } +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "NEXUS_5_Dark", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ErrorDiscussionTopicsScreenPreview() { + OpenEdXTheme { + DiscussionTopicsUI( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + uiState = DiscussionTopicsUIState.Error, + uiMessage = null, + onItemClick = { _, _, _ -> }, + onSearchClick = {} + ) + } +} + @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt index c57f55e9b..90091f395 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt @@ -5,5 +5,6 @@ import org.openedx.discussion.domain.model.Topic sealed class DiscussionTopicsUIState { data class Topics(val data: List) : DiscussionTopicsUIState() - object Loading : DiscussionTopicsUIState() + data object Loading : DiscussionTopicsUIState() + data object Error : DiscussionTopicsUIState() } \ No newline at end of file diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 456eb79c2..833250859 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -47,13 +47,18 @@ class DiscussionTopicsViewModel( viewModelScope.launch { try { val response = interactor.getCourseTopics(courseId) - _uiState.value = DiscussionTopicsUIState.Topics(response) + if (response.isEmpty().not()) { + _uiState.value = DiscussionTopicsUIState.Topics(response) + } else { + _uiState.value = DiscussionTopicsUIState.Error + } } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) } else { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } + _uiState.value = DiscussionTopicsUIState.Error } finally { courseNotifier.send(CourseLoading(false)) } diff --git a/discussion/src/main/res/values/strings.xml b/discussion/src/main/res/values/strings.xml index a9b11d04d..9b4f86c7c 100644 --- a/discussion/src/main/res/values/strings.xml +++ b/discussion/src/main/res/values/strings.xml @@ -36,6 +36,7 @@ No discussions yet Click the button below to create your first discussion. Unnamed subcategory + Unable to load discussions.\n Please try again later. From a9d6bc3cea2db96614f387de27025034651c0b3b Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 15 Jul 2024 15:01:28 +0500 Subject: [PATCH 2/5] fix: unit test cases --- .../presentation/dates/CourseDatesScreen.kt | 4 +- .../dates/CourseDatesViewModel.kt | 4 +- .../presentation/dates/DashboardUIState.kt | 2 +- .../dates/CourseDatesViewModelTest.kt | 6 +- .../handouts/HandoutsViewModelTest.kt | 19 +++-- .../topics/DiscussionTopicsViewModelTest.kt | 73 ++++++++++++++++--- 6 files changed, 84 insertions(+), 24 deletions(-) diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index 29b230b39..9c75034d3 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -305,7 +305,7 @@ private fun CourseDatesUI( } } - DatesUIState.Empty -> { + DatesUIState.Error -> { NoContentScreen( message = stringResource(id = R.string.course_dates_unavailable_message), icon = painterResource(id = R.drawable.course_ic_no_content) @@ -643,7 +643,7 @@ private fun EmptyCourseDatesScreenPreview() { OpenEdXTheme { CourseDatesUI( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - uiState = DatesUIState.Empty, + uiState = DatesUIState.Error, uiMessage = null, isSelfPaced = true, calendarSyncUIState = mockCalendarSyncUIState, diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index 95e9b4b0c..e38fcb693 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -105,7 +105,7 @@ class CourseDatesViewModel( isSelfPaced = courseStructure?.isSelfPaced ?: false val datesResponse = interactor.getCourseDates(courseId = courseId) if (datesResponse.datesSection.isEmpty()) { - _uiState.value = DatesUIState.Empty + _uiState.value = DatesUIState.Error } else { _uiState.value = DatesUIState.Dates(datesResponse) courseBannerType = datesResponse.courseBanner.bannerType @@ -117,7 +117,7 @@ class CourseDatesViewModel( } else { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_unknown_error))) } - _uiState.value = DatesUIState.Empty + _uiState.value = DatesUIState.Error } finally { courseNotifier.send(CourseLoading(false)) } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt index 7d3dc3cb3..49719f3a9 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt @@ -4,6 +4,6 @@ import org.openedx.core.domain.model.CourseDatesResult sealed class DatesUIState { data class Dates(val courseDatesResult: CourseDatesResult) : DatesUIState() - data object Empty : DatesUIState() + data object Error : DatesUIState() data object Loading : DatesUIState() } diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 11ffb4932..8eacb1bc2 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -187,7 +187,7 @@ class CourseDatesViewModelTest { coVerify(exactly = 1) { interactor.getCourseDates(any()) } Assert.assertEquals(noInternet, message.await()?.message) - assert(viewModel.uiState.value is DatesUIState.Loading) + assert(viewModel.uiState.value is DatesUIState.Error) } @Test @@ -216,7 +216,7 @@ class CourseDatesViewModelTest { coVerify(exactly = 1) { interactor.getCourseDates(any()) } Assert.assertEquals(somethingWrong, message.await()?.message) - assert(viewModel.uiState.value is DatesUIState.Loading) + assert(viewModel.uiState.value is DatesUIState.Error) } @Test @@ -277,6 +277,6 @@ class CourseDatesViewModelTest { coVerify(exactly = 1) { interactor.getCourseDates(any()) } assert(message.await()?.message.isNullOrEmpty()) - assert(viewModel.uiState.value is DatesUIState.Empty) + assert(viewModel.uiState.value is DatesUIState.Error) } } diff --git a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt index 7a92f8c8b..41074294a 100644 --- a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt @@ -7,14 +7,19 @@ import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.config.Config -import org.openedx.core.domain.model.* +import org.openedx.core.domain.model.AnnouncementModel +import org.openedx.core.domain.model.HandoutsModel import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import java.net.UnknownHostException @@ -54,7 +59,7 @@ class HandoutsViewModelTest { coEvery { interactor.getHandouts(any()) } throws UnknownHostException() advanceUntilIdle() - assert(viewModel.uiState.value == null) + assert(viewModel.uiState.value == HandoutsUIState.Error) } @Test @@ -63,7 +68,7 @@ class HandoutsViewModelTest { coEvery { interactor.getHandouts(any()) } throws Exception() advanceUntilIdle() - assert(viewModel.uiState.value == null) + assert(viewModel.uiState.value == HandoutsUIState.Error) } @Test @@ -76,7 +81,7 @@ class HandoutsViewModelTest { coVerify(exactly = 1) { interactor.getHandouts(any()) } coVerify(exactly = 0) { interactor.getAnnouncements(any()) } - assert(viewModel.uiState.value != null) + assert(viewModel.uiState.value is HandoutsUIState.HTMLContent) } @Test @@ -94,7 +99,7 @@ class HandoutsViewModelTest { coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } - assert(viewModel.uiState.value != null) + assert(viewModel.uiState.value is HandoutsUIState.HTMLContent) } @Test @@ -116,6 +121,6 @@ class HandoutsViewModelTest { coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } - assert(viewModel.uiState.value != null) + assert(viewModel.uiState.value is HandoutsUIState.HTMLContent) } } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index 9fc56f6af..5cc7c67a7 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -35,6 +35,7 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.discussion.domain.interactor.DiscussionInteractor +import org.openedx.discussion.domain.model.Topic import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter import java.net.UnknownHostException @@ -144,6 +145,13 @@ class DiscussionTopicsViewModelTest { progress = null ) + private val mockTopic = Topic( + id = "", + name = "All Topics", + threadListUrl = "", + children = emptyList() + ) + @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -160,7 +168,15 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -177,7 +193,15 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -194,9 +218,17 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) - coEvery { interactor.getCourseTopics(any()) } returns mockk() + coEvery { interactor.getCourseTopics(any()) } returns listOf(mockTopic, mockTopic) advanceUntilIdle() val message = async { withTimeoutOrNull(5000) { @@ -211,7 +243,15 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -228,7 +268,15 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -245,9 +293,17 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) - coEvery { interactor.getCourseTopics(any()) } returns mockk() + coEvery { interactor.getCourseTopics(any()) } returns listOf(mockTopic, mockTopic) val message = async { withTimeoutOrNull(5000) { viewModel.uiMessage.first() as? UIMessage.SnackBarMessage @@ -260,5 +316,4 @@ class DiscussionTopicsViewModelTest { assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is DiscussionTopicsUIState.Topics) } - } From ae68061f582e98327b6962fb2779b66397a8ffc8 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Wed, 17 Jul 2024 12:34:04 +0500 Subject: [PATCH 3/5] fix: address PR comments by Moin-1 --- .../org/openedx/core/NoContentScreenType.kt | 31 +++++++++++++++++++ .../java/org/openedx/core/ui/ComposeCommon.kt | 26 +++++++++++++++- .../org/openedx/core/ui/WebContentScreen.kt | 12 +------ .../res/drawable/core_ic_no_announcements.xml | 0 .../main/res/drawable/core_ic_no_handouts.xml | 0 .../main/res/drawable/core_ic_no_videos.xml | 0 core/src/main/res/values/strings.xml | 7 +++++ .../presentation/dates/CourseDatesScreen.kt | 11 ++++--- .../dates/CourseDatesViewModel.kt | 4 +-- .../presentation/handouts/HandoutsUIState.kt | 1 + .../handouts/HandoutsViewModel.kt | 6 ++-- .../handouts/HandoutsWebViewFragment.kt | 20 ++++++------ .../outline/CourseOutlineScreen.kt | 11 ++++--- .../course/presentation/ui/CourseVideosUI.kt | 12 +++---- .../res/drawable/course_ic_no_content.xml | 11 ------- course/src/main/res/values/strings.xml | 4 --- .../topics/DiscussionTopicsScreen.kt | 7 ++--- .../topics/DiscussionTopicsViewModel.kt | 4 +-- discussion/src/main/res/values/strings.xml | 2 +- 19 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/NoContentScreenType.kt rename course/src/main/res/drawable/course_ic_no_announcements.xml => core/src/main/res/drawable/core_ic_no_announcements.xml (100%) rename course/src/main/res/drawable/course_ic_no_handouts.xml => core/src/main/res/drawable/core_ic_no_handouts.xml (100%) rename course/src/main/res/drawable/course_ic_no_videos.xml => core/src/main/res/drawable/core_ic_no_videos.xml (100%) delete mode 100644 course/src/main/res/drawable/course_ic_no_content.xml diff --git a/core/src/main/java/org/openedx/core/NoContentScreenType.kt b/core/src/main/java/org/openedx/core/NoContentScreenType.kt new file mode 100644 index 000000000..88e8ad94b --- /dev/null +++ b/core/src/main/java/org/openedx/core/NoContentScreenType.kt @@ -0,0 +1,31 @@ +package org.openedx.core + +enum class NoContentScreenType( + val iconResId: Int, + val messageResId: Int, +) { + COURSE_OUTLINE( + iconResId = R.drawable.core_ic_no_content, + messageResId = R.string.core_no_course_content + ), + COURSE_VIDEOS( + iconResId = R.drawable.core_ic_no_videos, + messageResId = R.string.core_no_videos + ), + COURSE_DATES( + iconResId = R.drawable.core_ic_no_content, + messageResId = R.string.core_no_dates + ), + COURSE_DISCUSSIONS( + iconResId = R.drawable.core_ic_no_content, + messageResId = R.string.core_no_discussion + ), + COURSE_HANDOUTS( + iconResId = R.drawable.core_ic_no_handouts, + messageResId = R.string.core_no_handouts + ), + COURSE_ANNOUNCEMENTS( + iconResId = R.drawable.core_ic_no_announcements, + messageResId = R.string.core_no_announcements + ) +} diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index fc8756258..acaa2de50 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -101,11 +102,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import coil.ImageLoader import coil.compose.AsyncImage import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import kotlinx.coroutines.launch +import org.openedx.core.NoContentScreenType import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.domain.model.RegistrationField @@ -1182,6 +1185,14 @@ fun ConnectionErrorView( } } +@Composable +fun NoContentScreen(noContentScreenType: NoContentScreenType) { + NoContentScreen( + message = stringResource(id = noContentScreenType.messageResId), + icon = painterResource(id = noContentScreenType.iconResId) + ) +} + @Composable fun NoContentScreen(message: String, icon: Painter) { Column( @@ -1296,6 +1307,19 @@ fun RoundTabsBar( } } +@Composable +fun CircularProgress() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.appColors.background) + .zIndex(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } +} + @Composable private fun RoundTab( modifier: Modifier = Modifier, @@ -1430,4 +1454,4 @@ private fun PreviewNoContentScreen() { rememberVectorPainter(image = Icons.Filled.Info) ) } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt index 06aa70ea2..2fe762b26 100644 --- a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt +++ b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt @@ -6,7 +6,6 @@ import android.net.Uri import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface @@ -101,15 +99,7 @@ fun WebContentScreen( color = MaterialTheme.appColors.background ) { if (htmlBody.isNullOrEmpty() && contentUrl.isNullOrEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.appColors.background) - .zIndex(1f), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } + CircularProgress() } else { var webViewAlpha by rememberSaveable { mutableFloatStateOf(0f) } Surface( diff --git a/course/src/main/res/drawable/course_ic_no_announcements.xml b/core/src/main/res/drawable/core_ic_no_announcements.xml similarity index 100% rename from course/src/main/res/drawable/course_ic_no_announcements.xml rename to core/src/main/res/drawable/core_ic_no_announcements.xml diff --git a/course/src/main/res/drawable/course_ic_no_handouts.xml b/core/src/main/res/drawable/core_ic_no_handouts.xml similarity index 100% rename from course/src/main/res/drawable/course_ic_no_handouts.xml rename to core/src/main/res/drawable/core_ic_no_handouts.xml diff --git a/course/src/main/res/drawable/course_ic_no_videos.xml b/core/src/main/res/drawable/core_ic_no_videos.xml similarity index 100% rename from course/src/main/res/drawable/course_ic_no_videos.xml rename to core/src/main/res/drawable/core_ic_no_videos.xml diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 931d2c6da..760075c6e 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -182,4 +182,11 @@ Discussions More Dates + + No course content is currently available. + There are currently no videos for this course. + Course dates are currently not available. + Unable to load discussions.\n Please try again later. + There are currently no handouts for this course. + There are currently no announcements for this course. diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index 9c75034d3..462942fb5 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager +import org.openedx.core.NoContentScreenType import org.openedx.core.UIMessage import org.openedx.core.data.model.DateType import org.openedx.core.domain.model.CourseDateBlock @@ -74,6 +75,7 @@ import org.openedx.core.presentation.CoreAnalyticsScreen import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState +import org.openedx.core.ui.CircularProgress import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.WindowSize @@ -306,13 +308,12 @@ private fun CourseDatesUI( } DatesUIState.Error -> { - NoContentScreen( - message = stringResource(id = R.string.course_dates_unavailable_message), - icon = painterResource(id = R.drawable.course_ic_no_content) - ) + NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_DATES) } - DatesUIState.Loading -> {} + DatesUIState.Loading -> { + CircularProgress() + } } } } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index e38fcb693..3191fa721 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -112,12 +112,10 @@ class CourseDatesViewModel( checkIfCalendarOutOfDate() } } catch (e: Exception) { + _uiState.value = DatesUIState.Error if (e.isInternetError()) { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection))) - } else { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_unknown_error))) } - _uiState.value = DatesUIState.Error } finally { courseNotifier.send(CourseLoading(false)) } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt index 6c22b7de7..860e4261f 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt @@ -1,6 +1,7 @@ package org.openedx.course.presentation.handouts sealed class HandoutsUIState { + data object Loading : HandoutsUIState() data class HTMLContent(val htmlContent: String) : HandoutsUIState() data object Error : HandoutsUIState() } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index c80e1049e..424f71f81 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -24,15 +24,15 @@ class HandoutsViewModel( val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(HandoutsUIState.HTMLContent("")) + private val _uiState = MutableStateFlow(HandoutsUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() init { - getEnrolledCourse() + getCourseHandouts() } - private fun getEnrolledCourse() { + private fun getCourseHandouts() { viewModelScope.launch { var emptyState = false try { diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt index df50a078c..dbcbde30a 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt @@ -24,8 +24,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Devices @@ -37,6 +35,8 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.openedx.core.NoContentScreenType +import org.openedx.core.ui.CircularProgress import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WebContentScreen @@ -128,6 +128,10 @@ fun HandoutsScreens( ) { val windowSize = rememberWindowSize() when (uiState) { + is HandoutsUIState.Loading -> { + CircularProgress() + } + is HandoutsUIState.HTMLContent -> { WebContentScreen( windowSize = windowSize, @@ -157,10 +161,9 @@ fun HandoutsEmptyScreen( title: String, onBackClick: () -> Unit ) { - val messageResId = - if (handoutType == HandoutsType.Handouts) R.string.course_no_handouts else R.string.course_no_announcements - val iconRedId = - if (handoutType == HandoutsType.Handouts) R.drawable.course_ic_no_handouts else R.drawable.course_ic_no_announcements + val handoutScreenType = + if (handoutType == HandoutsType.Handouts) NoContentScreenType.COURSE_HANDOUTS + else NoContentScreenType.COURSE_ANNOUNCEMENTS val scaffoldState = rememberScaffoldState() Scaffold( @@ -208,10 +211,7 @@ fun HandoutsEmptyScreen( Modifier.fillMaxSize(), color = MaterialTheme.appColors.background ) { - NoContentScreen( - message = stringResource(id = messageResId), - icon = painterResource(id = iconRedId) - ) + NoContentScreen(noContentScreenType = handoutScreenType) } } } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index 1f0499803..ac455043f 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import org.openedx.core.BlockType +import org.openedx.core.NoContentScreenType import org.openedx.core.UIMessage import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block @@ -55,6 +56,7 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.Progress import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseViewMode +import org.openedx.core.ui.CircularProgress import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.OpenEdXButton @@ -224,10 +226,7 @@ private fun CourseOutlineUI( when (uiState) { is CourseOutlineUIState.CourseData -> { if (uiState.courseStructure.blockData.isEmpty()) { - NoContentScreen( - stringResource(id = R.string.course_no_course_content), - painterResource(id = R.drawable.course_ic_no_content) - ) + NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_OUTLINE) return@Surface } LazyColumn( @@ -334,7 +333,9 @@ private fun CourseOutlineUI( } } - CourseOutlineUIState.Loading -> {} + CourseOutlineUIState.Loading -> { + CircularProgress() + } } } } diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index 28d78317a..53aa70367 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -45,7 +45,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -54,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import org.openedx.core.AppDataConstants import org.openedx.core.BlockType +import org.openedx.core.NoContentScreenType import org.openedx.core.UIMessage import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block @@ -66,6 +66,7 @@ import org.openedx.core.extension.toFileSize import org.openedx.core.module.download.DownloadModelsSize import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.settings.video.VideoQualityType +import org.openedx.core.ui.CircularProgress import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.NoContentScreen import org.openedx.core.ui.WindowSize @@ -232,10 +233,7 @@ private fun CourseVideosUI( ) { when (uiState) { is CourseVideosUIState.Empty -> { - NoContentScreen( - stringResource(id = R.string.course_no_videos), - painterResource(id = R.drawable.course_ic_no_videos) - ) + NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_VIDEOS) } is CourseVideosUIState.CourseData -> { @@ -289,7 +287,9 @@ private fun CourseVideosUI( } } - CourseVideosUIState.Loading -> {} + CourseVideosUIState.Loading -> { + CircularProgress() + } } } } diff --git a/course/src/main/res/drawable/course_ic_no_content.xml b/course/src/main/res/drawable/course_ic_no_content.xml deleted file mode 100644 index 94a134d7e..000000000 --- a/course/src/main/res/drawable/course_ic_no_content.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 9e5473d10..4848ac402 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -36,10 +36,6 @@ Some content in this part of the course is locked for upgraded users only. You cannot change the download video quality when all videos are downloading Dates Shifted - No course content is currently available. - There are currently no videos for this course. - There are currently no handouts for this course. - There are currently no announcements for this course. Course dates are not currently available. diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt index e9788b25e..990e14260 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import org.openedx.core.FragmentViewType +import org.openedx.core.NoContentScreenType import org.openedx.core.UIMessage import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.NoContentScreen @@ -55,7 +56,6 @@ import org.openedx.discussion.R import org.openedx.discussion.domain.model.Topic import org.openedx.discussion.presentation.ui.ThreadItemCategory import org.openedx.discussion.presentation.ui.TopicItem -import org.openedx.core.R as CoreR @Composable fun DiscussionTopicsScreen( @@ -257,10 +257,7 @@ private fun DiscussionTopicsUI( DiscussionTopicsUIState.Loading -> {} else -> { - NoContentScreen( - message = stringResource(id = R.string.discussion_unable_to_load), - icon = painterResource(id = CoreR.drawable.core_ic_no_content) - ) + NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_DISCUSSIONS) } } } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 833250859..516ee50f8 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -53,12 +53,10 @@ class DiscussionTopicsViewModel( _uiState.value = DiscussionTopicsUIState.Error } } catch (e: Exception) { + _uiState.value = DiscussionTopicsUIState.Error if (e.isInternetError()) { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) - } else { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } - _uiState.value = DiscussionTopicsUIState.Error } finally { courseNotifier.send(CourseLoading(false)) } diff --git a/discussion/src/main/res/values/strings.xml b/discussion/src/main/res/values/strings.xml index 9b4f86c7c..1ed811681 100644 --- a/discussion/src/main/res/values/strings.xml +++ b/discussion/src/main/res/values/strings.xml @@ -36,7 +36,7 @@ No discussions yet Click the button below to create your first discussion. Unnamed subcategory - Unable to load discussions.\n Please try again later. + From ea9baa7092d1b207eda0fb9d6f02ba3a6b64b814 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Wed, 17 Jul 2024 13:15:15 +0500 Subject: [PATCH 4/5] fix: unit test cases --- .../dates/CourseDatesViewModelTest.kt | 2 +- .../topics/DiscussionTopicsViewModelTest.kt | 102 +----------------- 2 files changed, 5 insertions(+), 99 deletions(-) diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 8eacb1bc2..0c26a2936 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -215,7 +215,7 @@ class CourseDatesViewModelTest { coVerify(exactly = 1) { interactor.getCourseDates(any()) } - Assert.assertEquals(somethingWrong, message.await()?.message) + assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is DatesUIState.Error) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index 5cc7c67a7..ba3ab760a 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -23,14 +23,8 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.BlockType import org.openedx.core.R import org.openedx.core.UIMessage -import org.openedx.core.domain.model.AssignmentProgress -import org.openedx.core.domain.model.Block -import org.openedx.core.domain.model.BlockCounts -import org.openedx.core.domain.model.CourseStructure -import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier @@ -39,7 +33,6 @@ import org.openedx.discussion.domain.model.Topic import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter import java.net.UnknownHostException -import java.util.Date @OptIn(ExperimentalCoroutinesApi::class) class DiscussionTopicsViewModelTest { @@ -56,94 +49,6 @@ class DiscussionTopicsViewModelTest { private val courseNotifier = mockk() private val noInternet = "Slow or no internet connection" - private val somethingWrong = "Something went wrong" - - private val assignmentProgress = AssignmentProgress( - assignmentType = "Homework", - numPointsEarned = 1f, - numPointsPossible = 3f - ) - - private val blocks = listOf( - Block( - id = "id", - blockId = "blockId", - lmsWebUrl = "lmsWebUrl", - legacyWebUrl = "legacyWebUrl", - studentViewUrl = "studentViewUrl", - type = BlockType.CHAPTER, - displayName = "Block", - graded = false, - studentViewData = null, - studentViewMultiDevice = false, - blockCounts = BlockCounts(0), - descendants = listOf("1", "id1"), - descendantsType = BlockType.HTML, - completion = 0.0, - assignmentProgress = assignmentProgress, - due = Date() - ), - Block( - id = "id1", - blockId = "blockId", - lmsWebUrl = "lmsWebUrl", - legacyWebUrl = "legacyWebUrl", - studentViewUrl = "studentViewUrl", - type = BlockType.HTML, - displayName = "Block", - graded = false, - studentViewData = null, - studentViewMultiDevice = false, - blockCounts = BlockCounts(0), - descendants = listOf("id2"), - descendantsType = BlockType.HTML, - completion = 0.0, - assignmentProgress = assignmentProgress, - due = Date() - ), - Block( - id = "id2", - blockId = "blockId", - lmsWebUrl = "lmsWebUrl", - legacyWebUrl = "legacyWebUrl", - studentViewUrl = "studentViewUrl", - type = BlockType.HTML, - displayName = "Block", - graded = false, - studentViewData = null, - studentViewMultiDevice = false, - blockCounts = BlockCounts(0), - descendants = emptyList(), - descendantsType = BlockType.HTML, - completion = 0.0, - assignmentProgress = assignmentProgress, - due = Date() - ) - ) - private val courseStructure = CourseStructure( - root = "", - blockData = blocks, - id = "id", - name = "Course name", - number = "", - org = "Org", - start = Date(), - startDisplay = "", - startType = "", - end = Date(), - coursewareAccess = CoursewareAccess( - true, - "", - "", - "", - "", - "" - ), - media = null, - certificate = null, - isSelfPaced = false, - progress = null - ) private val mockTopic = Topic( id = "", @@ -156,7 +61,6 @@ class DiscussionTopicsViewModelTest { fun setUp() { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) coEvery { courseNotifier.send(any()) } returns Unit } @@ -213,7 +117,8 @@ class DiscussionTopicsViewModelTest { coVerify(exactly = 1) { interactor.getCourseTopics(any()) } - assertEquals(somethingWrong, message.await()?.message) + assert(message.await()?.message.isNullOrEmpty()) + assert(viewModel.uiState.value is DiscussionTopicsUIState.Error) } @Test @@ -288,7 +193,8 @@ class DiscussionTopicsViewModelTest { coVerify(exactly = 1) { interactor.getCourseTopics(any()) } - assertEquals(somethingWrong, message.await()?.message) + assert(message.await()?.message.isNullOrEmpty()) + assert(viewModel.uiState.value is DiscussionTopicsUIState.Error) } @Test From d8566c352c891f7fe60aefdbead57d0722bab88d Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Thu, 18 Jul 2024 10:26:41 +0500 Subject: [PATCH 5/5] fix: address PR comments --- core/src/main/java/org/openedx/core/ui/ComposeCommon.kt | 2 +- discussion/src/main/res/values/strings.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index acaa2de50..331faf8be 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1454,4 +1454,4 @@ private fun PreviewNoContentScreen() { rememberVectorPainter(image = Icons.Filled.Info) ) } -} \ No newline at end of file +} diff --git a/discussion/src/main/res/values/strings.xml b/discussion/src/main/res/values/strings.xml index 1ed811681..a9b11d04d 100644 --- a/discussion/src/main/res/values/strings.xml +++ b/discussion/src/main/res/values/strings.xml @@ -38,7 +38,6 @@ Unnamed subcategory - %1$d vote %1$d votes