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 d50b05cbe..3c4578d58 100644
--- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
+++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
@@ -31,12 +31,15 @@ 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.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -49,6 +52,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
@@ -97,11 +101,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
@@ -1185,6 +1191,41 @@ fun FullScreenErrorView(
}
}
+@Composable
+fun NoContentScreen(noContentScreenType: NoContentScreenType) {
+ NoContentScreen(
+ message = stringResource(id = noContentScreenType.messageResId),
+ icon = painterResource(id = noContentScreenType.iconResId)
+ )
+}
+
+@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,
@@ -1280,6 +1321,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,
@@ -1400,3 +1454,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/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/core/src/main/res/drawable/core_ic_no_announcements.xml b/core/src/main/res/drawable/core_ic_no_announcements.xml
new file mode 100644
index 000000000..fc85b3fe1
--- /dev/null
+++ b/core/src/main/res/drawable/core_ic_no_announcements.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
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/core/src/main/res/drawable/core_ic_no_handouts.xml b/core/src/main/res/drawable/core_ic_no_handouts.xml
new file mode 100644
index 000000000..d1f19a3d3
--- /dev/null
+++ b/core/src/main/res/drawable/core_ic_no_handouts.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/core/src/main/res/drawable/core_ic_no_videos.xml b/core/src/main/res/drawable/core_ic_no_videos.xml
new file mode 100644
index 000000000..f8a55d1b9
--- /dev/null
+++ b/core/src/main/res/drawable/core_ic_no_videos.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index b023e8845..c8d529afa 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -169,6 +169,12 @@
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.
Confirm Download
Edit
Offline Progress Sync
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 d76eb5eab..e15d3f7d4 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
@@ -56,13 +56,13 @@ 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
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,7 +74,10 @@ 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.CalendarSyncState
+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
import org.openedx.core.ui.WindowType
import org.openedx.core.ui.displayCutoutForLandscape
@@ -336,22 +339,13 @@ private fun CourseDatesUI(
}
}
- CourseDatesUIState.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
- )
- }
+ CourseDatesUIState.Error -> {
+ NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_DATES)
}
- CourseDatesUIState.Loading -> {}
+ CourseDatesUIState.Loading -> {
+ CircularProgress()
+ }
}
}
}
@@ -676,6 +670,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 = CourseDatesUIState.Error,
+ uiMessage = null,
+ isSelfPaced = true,
+ useRelativeDates = true,
+ onItemClick = {},
+ onPLSBannerViewed = {},
+ onSyncDates = {},
+ onCalendarSyncStateClick = {},
+ )
+ }
+}
+
@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/CourseDatesUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesUIState.kt
index 5623129d0..17f6e3b46 100644
--- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesUIState.kt
+++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesUIState.kt
@@ -9,6 +9,6 @@ sealed interface CourseDatesUIState {
val calendarSyncState: CalendarSyncState,
) : CourseDatesUIState
- data object Empty : CourseDatesUIState
+ data object Error : CourseDatesUIState
data object Loading : CourseDatesUIState
}
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 48fd0a524..54406019d 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
@@ -101,7 +101,7 @@ class CourseDatesViewModel(
isSelfPaced = courseStructure?.isSelfPaced ?: false
val datesResponse = interactor.getCourseDates(courseId = courseId)
if (datesResponse.datesSection.isEmpty()) {
- _uiState.value = CourseDatesUIState.Empty
+ _uiState.value = CourseDatesUIState.Error
} else {
val courseDates = datesResponse.datesSection.values.flatten()
val calendarState = getCalendarState(courseDates)
@@ -110,10 +110,9 @@ class CourseDatesViewModel(
checkIfCalendarOutOfDate()
}
} catch (e: Exception) {
+ _uiState.value = CourseDatesUIState.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)))
}
} 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 18aebac3f..6dbb71fb2 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,12 +3,11 @@ package org.openedx.course.presentation.dates
import org.openedx.core.domain.model.CourseDatesResult
import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState
-sealed interface DatesUIState {
+sealed class DatesUIState {
data class Dates(
val courseDatesResult: CourseDatesResult,
- val calendarSyncState: CalendarSyncState
- ) : DatesUIState
-
- data object Empty : DatesUIState
- data object Loading : DatesUIState
+ val calendarSyncState: CalendarSyncState,
+ ) : DatesUIState()
+ data object Error : 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..860e4261f
--- /dev/null
+++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsUIState.kt
@@ -0,0 +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 92aaa139d..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
@@ -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,26 +24,40 @@ class HandoutsViewModel(
val apiHostUrl get() = config.getApiHostURL()
- private val _htmlContent = MutableLiveData()
- val htmlContent: LiveData
- get() = _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 {
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..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
@@ -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.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.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
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.Loading -> {
+ CircularProgress()
+ }
+
+ 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 handoutScreenType =
+ if (handoutType == HandoutsType.Handouts) NoContentScreenType.COURSE_HANDOUTS
+ else NoContentScreenType.COURSE_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(noContentScreenType = handoutScreenType)
+ }
+ }
+ }
+ }
+}
+
@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 10ad4f932..90d74e7f5 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
@@ -56,7 +57,9 @@ import org.openedx.core.domain.model.OfflineDownload
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
import org.openedx.core.ui.TextIcon
import org.openedx.core.ui.WindowSize
@@ -222,116 +225,130 @@ private fun CourseOutlineUI(
Box {
when (uiState) {
is CourseOutlineUIState.CourseData -> {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = listBottomPadding
- ) {
- if (uiState.datesBannerInfo.isBannerAvailableForDashboard()) {
- item {
- Box(
- modifier = Modifier
- .padding(all = 8.dp)
- ) {
- if (windowSize.isTablet) {
- CourseDatesBannerTablet(
- banner = uiState.datesBannerInfo,
- resetDates = onResetDatesClick,
- )
- } else {
- CourseDatesBanner(
- banner = uiState.datesBannerInfo,
- resetDates = onResetDatesClick,
- )
+ if (uiState.courseStructure.blockData.isEmpty()) {
+ NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_OUTLINE)
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = listBottomPadding
+ ) {
+ if (uiState.datesBannerInfo.isBannerAvailableForDashboard()) {
+ item {
+ Box(
+ modifier = Modifier
+ .padding(all = 8.dp)
+ ) {
+ if (windowSize.isTablet) {
+ CourseDatesBannerTablet(
+ banner = uiState.datesBannerInfo,
+ resetDates = onResetDatesClick,
+ )
+ } else {
+ CourseDatesBanner(
+ banner = uiState.datesBannerInfo,
+ resetDates = onResetDatesClick,
+ )
+ }
}
}
}
- }
- val certificate = uiState.courseStructure.certificate
- if (certificate?.isCertificateEarned() == true) {
- item {
- CourseMessage(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 12.dp)
- .then(listPadding),
- icon = painterResource(R.drawable.ic_course_certificate),
- message = stringResource(
- R.string.course_you_earned_certificate,
- uiState.courseStructure.name
- ),
- action = stringResource(R.string.course_view_certificate),
- onActionClick = {
- onCertificateClick(certificate.certificateURL ?: "")
- }
- )
+ val certificate = uiState.courseStructure.certificate
+ if (certificate?.isCertificateEarned() == true) {
+ item {
+ CourseMessage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ .then(listPadding),
+ icon = painterResource(R.drawable.ic_course_certificate),
+ message = stringResource(
+ R.string.course_you_earned_certificate,
+ uiState.courseStructure.name
+ ),
+ action = stringResource(R.string.course_view_certificate),
+ onActionClick = {
+ onCertificateClick(
+ certificate.certificateURL ?: ""
+ )
+ }
+ )
+ }
}
- }
- val progress = uiState.courseStructure.progress
- if (progress != null && progress.totalAssignmentsCount > 0) {
- item {
- CourseProgress(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp, start = 24.dp, end = 24.dp),
- progress = progress
- )
+ val progress = uiState.courseStructure.progress
+ if (progress != null && progress.totalAssignmentsCount > 0) {
+ item {
+ CourseProgress(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = 16.dp,
+ start = 24.dp,
+ end = 24.dp
+ ),
+ progress = progress
+ )
+ }
}
- }
- if (uiState.resumeComponent != null) {
- item {
- Box(listPadding) {
- if (windowSize.isTablet) {
- ResumeCourseTablet(
- modifier = Modifier.padding(vertical = 16.dp),
- block = uiState.resumeComponent,
- displayName = uiState.resumeUnitTitle,
- onResumeClick = onResumeClick
- )
- } else {
- ResumeCourse(
- modifier = Modifier.padding(vertical = 16.dp),
- block = uiState.resumeComponent,
- displayName = uiState.resumeUnitTitle,
- onResumeClick = onResumeClick
- )
+ if (uiState.resumeComponent != null) {
+ item {
+ Box(listPadding) {
+ if (windowSize.isTablet) {
+ ResumeCourseTablet(
+ modifier = Modifier.padding(vertical = 16.dp),
+ block = uiState.resumeComponent,
+ displayName = uiState.resumeUnitTitle,
+ onResumeClick = onResumeClick
+ )
+ } else {
+ ResumeCourse(
+ modifier = Modifier.padding(vertical = 16.dp),
+ block = uiState.resumeComponent,
+ displayName = uiState.resumeUnitTitle,
+ onResumeClick = onResumeClick
+ )
+ }
}
}
}
- }
-
- item {
- Spacer(modifier = Modifier.height(12.dp))
- }
- uiState.courseStructure.blockData.forEach { section ->
- val courseSubSections =
- uiState.courseSubSections[section.id]
- val courseSectionsState =
- uiState.courseSectionsState[section.id]
item {
- CourseSection(
- modifier = listPadding.padding(vertical = 4.dp),
- block = section,
- onItemClick = onExpandClick,
- useRelativeDates = uiState.useRelativeDates,
- courseSectionsState = courseSectionsState,
- courseSubSections = courseSubSections,
- downloadedStateMap = uiState.downloadedState,
- onSubSectionClick = onSubSectionClick,
- onDownloadClick = onDownloadClick
- )
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+ uiState.courseStructure.blockData.forEach { section ->
+ val courseSubSections =
+ uiState.courseSubSections[section.id]
+ val courseSectionsState =
+ uiState.courseSectionsState[section.id]
+
+ item {
+ CourseSection(
+ modifier = listPadding.padding(vertical = 4.dp),
+ block = section,
+ onItemClick = onExpandClick,
+ useRelativeDates = uiState.useRelativeDates,
+ courseSectionsState = courseSectionsState,
+ courseSubSections = courseSubSections,
+ downloadedStateMap = uiState.downloadedState,
+ onSubSectionClick = onSubSectionClick,
+ onDownloadClick = onDownloadClick
+ )
+ }
}
}
}
}
- CourseOutlineUIState.Error -> {}
+ CourseOutlineUIState.Error -> {
+ NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_OUTLINE)
+ }
- 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 73afb3d0b..5fd4ea981 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
@@ -48,7 +46,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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
@@ -56,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
@@ -68,7 +66,9 @@ 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
import org.openedx.core.ui.WindowType
import org.openedx.core.ui.displayCutoutForLandscape
@@ -241,20 +241,7 @@ 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(noContentScreenType = NoContentScreenType.COURSE_VIDEOS)
}
is CourseVideosUIState.CourseData -> {
@@ -309,7 +296,9 @@ private fun CourseVideosUI(
}
}
- CourseVideosUIState.Loading -> {}
+ CourseVideosUIState.Loading -> {
+ CircularProgress()
+ }
}
}
}
@@ -656,9 +645,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 e5bbffe05..a02eac54c 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
@@ -154,9 +154,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()
@@ -180,9 +178,7 @@ class CourseVideoViewModel(
}
courseNotifier.send(CourseLoading(false))
} catch (e: Exception) {
- _uiState.value = CourseVideosUIState.Empty(
- message = resourceManager.getString(R.string.course_does_not_include_videos)
- )
+ _uiState.value = CourseVideosUIState.Empty
}
}
}
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 44f485c98..245fb2380 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
@@ -16,6 +16,6 @@ sealed class CourseVideosUIState {
val useRelativeDates: Boolean
) : 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/values/strings.xml b/course/src/main/res/values/strings.xml
index 8be55b9d4..c0b03e756 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
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 ed4e28f58..389196f31 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
@@ -180,7 +180,7 @@ class CourseDatesViewModelTest {
coVerify(exactly = 1) { interactor.getCourseDates(any()) }
Assert.assertEquals(noInternet, message.await()?.message)
- assert(viewModel.uiState.value is CourseDatesUIState.Loading)
+ assert(viewModel.uiState.value is CourseDatesUIState.Error)
}
@Test
@@ -209,8 +209,8 @@ class CourseDatesViewModelTest {
coVerify(exactly = 1) { interactor.getCourseDates(any()) }
- Assert.assertEquals(somethingWrong, message.await()?.message)
- assert(viewModel.uiState.value is CourseDatesUIState.Loading)
+ assert(message.await()?.message.isNullOrEmpty())
+ assert(viewModel.uiState.value is CourseDatesUIState.Error)
}
@Test
@@ -273,6 +273,6 @@ class CourseDatesViewModelTest {
coVerify(exactly = 1) { interactor.getCourseDates(any()) }
assert(message.await()?.message.isNullOrEmpty())
- assert(viewModel.uiState.value is CourseDatesUIState.Empty)
+ assert(viewModel.uiState.value is CourseDatesUIState.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 6e8d2dab2..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
@@ -5,22 +5,24 @@ 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 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.Assert.*
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
-import java.util.*
@OptIn(ExperimentalCoroutinesApi::class)
class HandoutsViewModelTest {
@@ -57,7 +59,7 @@ class HandoutsViewModelTest {
coEvery { interactor.getHandouts(any()) } throws UnknownHostException()
advanceUntilIdle()
- assert(viewModel.htmlContent.value == null)
+ assert(viewModel.uiState.value == HandoutsUIState.Error)
}
@Test
@@ -66,7 +68,7 @@ class HandoutsViewModelTest {
coEvery { interactor.getHandouts(any()) } throws Exception()
advanceUntilIdle()
- assert(viewModel.htmlContent.value == null)
+ assert(viewModel.uiState.value == HandoutsUIState.Error)
}
@Test
@@ -79,7 +81,7 @@ class HandoutsViewModelTest {
coVerify(exactly = 1) { interactor.getHandouts(any()) }
coVerify(exactly = 0) { interactor.getAnnouncements(any()) }
- assert(viewModel.htmlContent.value != null)
+ assert(viewModel.uiState.value is HandoutsUIState.HTMLContent)
}
@Test
@@ -97,7 +99,7 @@ class HandoutsViewModelTest {
coVerify(exactly = 0) { interactor.getHandouts(any()) }
coVerify(exactly = 1) { interactor.getAnnouncements(any()) }
- assert(viewModel.htmlContent.value != null)
+ assert(viewModel.uiState.value is HandoutsUIState.HTMLContent)
}
@Test
@@ -111,7 +113,7 @@ class HandoutsViewModelTest {
)
)
viewModel.injectDarkMode(
- viewModel.htmlContent.value.toString(),
+ viewModel.uiState.value.toString(),
ULong.MAX_VALUE,
ULong.MAX_VALUE
)
@@ -119,6 +121,6 @@ class HandoutsViewModelTest {
coVerify(exactly = 0) { interactor.getHandouts(any()) }
coVerify(exactly = 1) { interactor.getAnnouncements(any()) }
- assert(viewModel.htmlContent.value != null)
+ assert(viewModel.uiState.value is HandoutsUIState.HTMLContent)
}
}
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 562bca77b..812962c83 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
@@ -193,7 +193,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..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
@@ -37,10 +37,11 @@ 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.NoContentScreenType
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 +52,10 @@ 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
@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,9 @@ private fun DiscussionTopicsUI(
}
DiscussionTopicsUIState.Loading -> {}
+ else -> {
+ NoContentScreen(noContentScreenType = NoContentScreenType.COURSE_DISCUSSIONS)
+ }
}
}
}
@@ -279,6 +285,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..f1becc420 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()
-}
\ No newline at end of file
+ data object Loading : DiscussionTopicsUIState()
+ data object Error : DiscussionTopicsUIState()
+}
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..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
@@ -47,12 +47,15 @@ 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) {
+ _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)))
}
} finally {
courseNotifier.send(CourseLoading(false))
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 29a38a6a9..96e3c49f4 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,20 +23,16 @@ 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.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
-import java.util.Date
@OptIn(ExperimentalCoroutinesApi::class)
class DiscussionTopicsViewModelTest {
@@ -53,79 +49,18 @@ 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(),
- offlineDownload = null,
- ),
- 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(),
- offlineDownload = null,
- ),
- 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(),
- offlineDownload = null,
- )
+ private val mockTopic = Topic(
+ id = "",
+ name = "All Topics",
+ threadListUrl = "",
+ children = emptyList()
)
@Before
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
}
@@ -166,14 +101,15 @@ 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
fun `getCourseTopics success`() = runTest(UnconfinedTestDispatcher()) {
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) {
@@ -217,14 +153,15 @@ 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
fun `updateCourseTopics success`() = runTest(UnconfinedTestDispatcher()) {
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