Skip to content

Commit

Permalink
feat: Course Level Error Handling for Empty States (#393)
Browse files Browse the repository at this point in the history
Co-authored-by: Farhan Arshad <43750646+farhan-arshad-dev@users.noreply.github.com>
  • Loading branch information
dixidroid and farhan-arshad-dev authored Oct 31, 2024
1 parent 2839535 commit a4eac8d
Show file tree
Hide file tree
Showing 27 changed files with 603 additions and 304 deletions.
31 changes: 31 additions & 0 deletions core/src/main/java/org/openedx/core/NoContentScreenType.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
65 changes: 65 additions & 0 deletions core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1400,3 +1454,14 @@ private fun RoundTabsBarPreview() {
)
}
}

@Preview
@Composable
private fun PreviewNoContentScreen() {
OpenEdXTheme(darkTheme = true) {
NoContentScreen(
"No Content available",
rememberVectorPainter(image = Icons.Filled.Info)
)
}
}
12 changes: 1 addition & 11 deletions core/src/main/java/org/openedx/core/ui/WebContentScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ 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
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
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/res/drawable/core_ic_no_announcements.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="86dp"
android:height="86dp"
android:viewportWidth="960"
android:viewportHeight="960">

<path
android:fillColor="#ffffff"
android:pathData="M720,520v-80h160v80L720,520ZM768,800 L640,704 688,640 816,736 768,800ZM688,320 L640,256 768,160 816,224 688,320ZM200,760v-160h-40q-33,0 -56.5,-23.5T80,520v-80q0,-33 23.5,-56.5T160,360h160l200,-120v480L320,600h-40v160h-80ZM440,578v-196l-98,58L160,440v80h182l98,58ZM560,614v-268q27,24 43.5,58.5T620,480q0,41 -16.5,75.5T560,614ZM300,480Z" />

</vector>
11 changes: 11 additions & 0 deletions core/src/main/res/drawable/core_ic_no_content.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="86dp"
android:height="86dp"
android:viewportWidth="20"
android:viewportHeight="20">

<path
android:fillColor="#ffffff"
android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM10,11C9.45,11 9,10.55 9,10V6C9,5.45 9.45,5 10,5C10.55,5 11,5.45 11,6V10C11,10.55 10.55,11 10,11ZM11,15H9V13H11V15Z" />

</vector>
11 changes: 11 additions & 0 deletions core/src/main/res/drawable/core_ic_no_handouts.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="86dp"
android:height="86dp"
android:viewportWidth="18"
android:viewportHeight="18">

<path
android:fillColor="#ffffff"
android:pathData="M16,0H2C0.9,0 0,0.9 0,2V16C0,17.1 0.9,18 2,18H16C17.1,18 18,17.1 18,16V2C18,0.9 17.1,0 16,0ZM10,14H5C4.45,14 4,13.55 4,13C4,12.45 4.45,12 5,12H10C10.55,12 11,12.45 11,13C11,13.55 10.55,14 10,14ZM13,10H5C4.45,10 4,9.55 4,9C4,8.45 4.45,8 5,8H13C13.55,8 14,8.45 14,9C14,9.55 13.55,10 13,10ZM13,6H5C4.45,6 4,5.55 4,5C4,4.45 4.45,4 5,4H13C13.55,4 14,4.45 14,5C14,5.55 13.55,6 13,6Z" />

</vector>
11 changes: 11 additions & 0 deletions core/src/main/res/drawable/core_ic_no_videos.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="86dp"
android:height="58dp"
android:viewportWidth="18"
android:viewportHeight="12">

<path
android:fillColor="#ffffff"
android:pathData="M14,4.5V1C14,0.45 13.55,0 13,0H1C0.45,0 0,0.45 0,1V11C0,11.55 0.45,12 1,12H13C13.55,12 14,11.55 14,11V7.5L16.29,9.79C16.92,10.42 18,9.97 18,9.08V2.91C18,2.02 16.92,1.57 16.29,2.2L14,4.5Z" />

</vector>
6 changes: 6 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@
<string name="core_course_container_nav_discussions">Discussions</string>
<string name="core_course_container_nav_more">More</string>
<string name="core_course_container_nav_dates" tools:ignore="MissingTranslation">Dates</string>
<string name="core_no_course_content">No course content is currently available.</string>
<string name="core_no_videos">There are currently no videos for this course.</string>
<string name="core_no_dates">Course dates are currently not available.</string>
<string name="core_no_discussion">Unable to load discussions.\n Please try again later.</string>
<string name="core_no_handouts">There are currently no handouts for this course.</string>
<string name="core_no_announcements">There are currently no announcements for this course.</string>
<string name="course_confirm_download">Confirm Download</string>
<string name="core_edit">Edit</string>
<string name="core_offline_progress_sync">Offline Progress Sync</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
}
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ sealed interface CourseDatesUIState {
val calendarSyncState: CalendarSyncState,
) : CourseDatesUIState

data object Empty : CourseDatesUIState
data object Error : CourseDatesUIState
data object Loading : CourseDatesUIState
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit a4eac8d

Please sign in to comment.