From 4c1a90930a7275aa458874bbeab9ce55f327dcd2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk <141041606+PavloNetrebchuk@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:09:22 +0300 Subject: [PATCH] feat: [FC-0047] Relative Dates (#367) * feat: relative dates * fix: Fixes according to designer feedback --- .../app/data/storage/PreferencesManager.kt | 7 + .../java/org/openedx/app/di/ScreenModule.kt | 6 +- .../core/data/storage/CorePreferences.kt | 1 + .../core/domain/model/CourseDateBlock.kt | 6 - .../org/openedx/core/extension/StringExt.kt | 2 +- .../java/org/openedx/core/utils/TimeUtils.kt | 190 ++++++------------ core/src/main/res/values/strings.xml | 17 +- .../presentation/dates/CourseDatesScreen.kt | 34 ++-- .../dates/CourseDatesViewModel.kt | 3 + .../outline/CourseOutlineScreen.kt | 7 +- .../outline/CourseOutlineUIState.kt | 1 + .../outline/CourseOutlineViewModel.kt | 3 + .../course/presentation/ui/CourseUI.kt | 9 +- .../course/presentation/ui/CourseVideosUI.kt | 6 +- .../videos/CourseVideoViewModel.kt | 9 +- .../videos/CourseVideosUIState.kt | 3 +- .../dates/CourseDatesViewModelTest.kt | 7 + .../outline/CourseOutlineViewModelTest.kt | 1 + .../videos/CourseVideoViewModelTest.kt | 1 + .../presentation/AllEnrolledCoursesView.kt | 2 +- .../presentation/DashboardGalleryUIState.kt | 2 +- .../presentation/DashboardGalleryView.kt | 15 +- .../presentation/DashboardGalleryViewModel.kt | 14 +- .../presentation/DashboardListFragment.kt | 2 +- .../discovery/presentation/ui/DiscoveryUI.kt | 7 +- .../presentation/calendar/CalendarFragment.kt | 7 + .../calendar/CalendarSetUpView.kt | 9 + .../calendar/CalendarSettingsView.kt | 8 + .../presentation/calendar/CalendarUIState.kt | 3 +- .../presentation/calendar/CalendarView.kt | 73 +++++++ .../calendar/CalendarViewModel.kt | 10 +- profile/src/main/res/values/strings.xml | 1 + 32 files changed, 265 insertions(+), 201 deletions(-) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt index efd2d16b2..1a4974a19 100644 --- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt +++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt @@ -195,6 +195,12 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getString(CALENDAR_USER) + override var isRelativeDatesEnabled: Boolean + set(value) { + saveBoolean(IS_RELATIVE_DATES_ENABLED, value) + } + get() = getBoolean(IS_RELATIVE_DATES_ENABLED, true) + override var isHideInactiveCourses: Boolean set(value) { saveBoolean(HIDE_INACTIVE_COURSES, value) @@ -225,6 +231,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val CALENDAR_ID = "CALENDAR_ID" private const val RESET_APP_DIRECTORY = "reset_app_directory" private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" + private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED" private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES" private const val CALENDAR_USER = "CALENDAR_USER" } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 541782caf..15ef16498 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -152,6 +152,7 @@ val screenModule = module { get(), get(), get(), + get(), windowSize ) } @@ -204,7 +205,7 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { CoursesToSyncViewModel(get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get(), get(), get(), get()) } viewModel { DisableCalendarSyncDialogViewModel(get(), get(), get(), get()) } @@ -276,7 +277,7 @@ val screenModule = module { get(), get(), get(), - get() + get(), ) } viewModel { (courseId: String) -> @@ -358,6 +359,7 @@ val screenModule = module { get(), get(), get(), + get(), ) } viewModel { (courseId: String, handoutsType: String) -> diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index 7792fb4a4..5435494ba 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -13,6 +13,7 @@ interface CorePreferences { var videoSettings: VideoSettings var appConfig: AppConfig var canResetAppDirectory: Boolean + var isRelativeDatesEnabled: Boolean fun clearCorePreferences() } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt index 97f8612bf..9249d6a23 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt @@ -3,8 +3,6 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize import org.openedx.core.data.model.DateType -import org.openedx.core.utils.isTimeLessThan24Hours -import org.openedx.core.utils.isToday import java.util.Date @Parcelize @@ -29,10 +27,6 @@ data class CourseDateBlock( ) && date.before(Date())) } - fun isTimeDifferenceLessThan24Hours(): Boolean { - return (date.isToday() && date.before(Date())) || date.isTimeLessThan24Hours() - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/core/src/main/java/org/openedx/core/extension/StringExt.kt b/core/src/main/java/org/openedx/core/extension/StringExt.kt index 6d8457fed..d383cf57f 100644 --- a/core/src/main/java/org/openedx/core/extension/StringExt.kt +++ b/core/src/main/java/org/openedx/core/extension/StringExt.kt @@ -42,5 +42,5 @@ fun String.toImageLink(apiHostURL: String): String = if (this.isLinkValid()) { this } else { - apiHostURL + this.removePrefix("/") + (apiHostURL + this).replace(Regex("(? DateUtils.formatDateTime( + context, + date.time, + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() + + daysDiff == -6 -> context.getString(R.string.core_next) + " " + DateUtils.formatDateTime( + context, + date.time, + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() + + daysDiff in -1..1 -> DateUtils.getRelativeTimeSpanString( + date.time, + now.timeInMillis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_TIME + ).toString() + + daysDiff in 2..6 -> DateUtils.getRelativeTimeSpanString( + date.time, + now.timeInMillis, + DateUtils.DAY_IN_MILLIS + ).toString() + + inputDate.get(Calendar.YEAR) == now.get(Calendar.YEAR) -> { + DateUtils.getRelativeTimeSpanString( + date.time, + now.timeInMillis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_DATE + ).toString() + } + + else -> { + DateUtils.getRelativeTimeSpanString( + date.time, + now.timeInMillis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR + ).toString() + } + } + } + fun getCurrentTime(): Long { return Calendar.getInstance().timeInMillis } @@ -170,126 +224,6 @@ object TimeUtils { } return formattedDate } - - /** - * Method to get the formatted time string in terms of relative time with minimum resolution of minutes. - * For example, if the time difference is 1 minute, it will return "1m ago". - * - * @param date Date object to be formatted. - */ - fun getFormattedTime(date: Date): String { - return DateUtils.getRelativeTimeSpanString( - date.time, - getCurrentTime(), - DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_TIME - ).toString() - } - - /** - * Returns a formatted date string for the given date. - */ - fun getCourseFormattedDate(context: Context, date: Date): String { - val inputDate = Calendar.getInstance().also { - it.time = date - it.clearTimeComponents() - } - val daysDifference = getDayDifference(inputDate) - - return when { - daysDifference == 0 -> { - context.getString(R.string.core_date_format_today) - } - - daysDifference == 1 -> { - context.getString(R.string.core_date_format_tomorrow) - } - - daysDifference == -1 -> { - context.getString(R.string.core_date_format_yesterday) - } - - daysDifference in -2 downTo -7 -> { - context.getString( - R.string.core_date_format_days_ago, - ceil(-daysDifference.toDouble()).toInt().toString() - ) - } - - daysDifference in 2..7 -> { - DateUtils.formatDateTime( - context, - date.time, - DateUtils.FORMAT_SHOW_WEEKDAY - ) - } - - inputDate.get(Calendar.YEAR) != Calendar.getInstance().get(Calendar.YEAR) -> { - DateUtils.formatDateTime( - context, - date.time, - DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR - ) - } - - else -> { - DateUtils.formatDateTime( - context, - date.time, - DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR - ) - } - } - } - - fun getAssignmentFormattedDate(context: Context, date: Date): String { - val inputDate = Calendar.getInstance().also { - it.time = date - it.clearTimeComponents() - } - val daysDifference = getDayDifference(inputDate) - - return when { - daysDifference == 0 -> { - context.getString(R.string.core_date_format_assignment_due_today) - } - - daysDifference == 1 -> { - context.getString(R.string.core_date_format_assignment_due_tomorrow) - } - - daysDifference == -1 -> { - context.getString(R.string.core_date_format_assignment_due_yesterday) - } - - daysDifference <= -2 -> { - val numberOfDays = ceil(-daysDifference.toDouble()).toInt() - context.resources.getQuantityString( - R.plurals.core_date_format_assignment_due_days_ago, - numberOfDays, - numberOfDays - ) - } - - else -> { - val numberOfDays = ceil(daysDifference.toDouble()).toInt() - context.resources.getQuantityString( - R.plurals.core_date_format_assignment_due_in, - numberOfDays, - numberOfDays - ) - } - } - } - - /** - * Returns the number of days difference between the given date and the current date. - */ - private fun getDayDifference(inputDate: Calendar): Int { - val currentDate = Calendar.getInstance().also { it.clearTimeComponents() } - val difference = inputDate.timeInMillis - currentDate.timeInMillis - return TimeUnit.MILLISECONDS.toDays(difference).toInt() - } } /** @@ -336,16 +270,6 @@ fun Date.clearTime(): Date { return calendar.time } -/** - * Extension function to check if the time difference between the given date and the current date is less than 24 hours. - */ -fun Date.isTimeLessThan24Hours(): Boolean { - val calendar = Calendar.getInstance() - calendar.time = this - val timeInMillis = (calendar.timeInMillis - TimeUtils.getCurrentTime()).unaryPlus() - return timeInMillis < TimeUnit.DAYS.toMillis(1) -} - fun Date.toCalendar(): Calendar { val calendar = Calendar.getInstance() calendar.time = this diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 00b02502a..0b245c7fa 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -92,21 +92,7 @@ Next Week Upcoming None - Today - Tomorrow - Yesterday - %1$s days ago - Due Today - Due Tomorrow - Due Yesterday - - Due %1$d day ago - Due %1$d days ago - - - Due in %1$d day - Due in %1$d days - + Due %1$s %d Item Hidden %d Items Hidden @@ -193,4 +179,5 @@ To Sync Not Synced Syncing to calendar… + Next 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 b148c8acb..d76eb5eab 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 @@ -74,7 +74,6 @@ 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.HandleUIMessage import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -85,11 +84,12 @@ import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils +import org.openedx.core.utils.TimeUtils.formatToString import org.openedx.core.utils.clearTime import org.openedx.course.R import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet -import java.util.concurrent.atomic.AtomicReference +import java.util.Date import org.openedx.core.R as CoreR @Composable @@ -109,6 +109,7 @@ fun CourseDatesScreen( uiState = uiState, uiMessage = uiMessage, isSelfPaced = viewModel.isSelfPaced, + useRelativeDates = viewModel.useRelativeDates, onItemClick = { block -> if (block.blockId.isNotEmpty()) { viewModel.getVerticalBlock(block.blockId) @@ -178,6 +179,7 @@ private fun CourseDatesUI( uiState: CourseDatesUIState, uiMessage: UIMessage?, isSelfPaced: Boolean, + useRelativeDates: Boolean, onItemClick: (CourseDateBlock) -> Unit, onPLSBannerViewed: () -> Unit, onSyncDates: () -> Unit, @@ -311,6 +313,7 @@ private fun CourseDatesUI( sectionKey = DatesSection.COMPLETED, sectionDates = section, onItemClick = onItemClick, + useRelativeDates = useRelativeDates ) } } @@ -325,6 +328,7 @@ private fun CourseDatesUI( sectionKey = sectionKey, sectionDates = section, onItemClick = onItemClick, + useRelativeDates = useRelativeDates ) } } @@ -420,6 +424,7 @@ fun CalendarSyncCard( @Composable fun ExpandableView( sectionKey: DatesSection = DatesSection.NONE, + useRelativeDates: Boolean, sectionDates: List, onItemClick: (CourseDateBlock) -> Unit, ) { @@ -503,6 +508,7 @@ fun ExpandableView( sectionKey = sectionKey, sectionDates = sectionDates, onItemClick = onItemClick, + useRelativeDates = useRelativeDates ) } } @@ -511,6 +517,7 @@ fun ExpandableView( @Composable private fun CourseDateBlockSection( sectionKey: DatesSection = DatesSection.NONE, + useRelativeDates: Boolean, sectionDates: List, onItemClick: (CourseDateBlock) -> Unit, ) { @@ -533,7 +540,7 @@ private fun CourseDateBlockSection( if (sectionKey != DatesSection.COMPLETED) { DateBullet(section = sectionKey) } - DateBlock(dateBlocks = sectionDates, onItemClick = onItemClick) + DateBlock(dateBlocks = sectionDates, onItemClick = onItemClick, useRelativeDates = useRelativeDates) } } } @@ -565,6 +572,7 @@ private fun DateBullet( @Composable private fun DateBlock( dateBlocks: List, + useRelativeDates: Boolean, onItemClick: (CourseDateBlock) -> Unit, ) { Column( @@ -579,7 +587,7 @@ private fun DateBlock( if (index != 0) { canShowDate = (lastAssignmentDate != dateBlock.date) } - CourseDateItem(dateBlock, canShowDate, index != 0, onItemClick) + CourseDateItem(dateBlock, canShowDate, index != 0, useRelativeDates, onItemClick) lastAssignmentDate = dateBlock.date } } @@ -590,8 +598,10 @@ private fun CourseDateItem( dateBlock: CourseDateBlock, canShowDate: Boolean, isMiddleChild: Boolean, + useRelativeDates: Boolean, onItemClick: (CourseDateBlock) -> Unit, ) { + val context = LocalContext.current Column( modifier = Modifier .wrapContentHeight() @@ -601,11 +611,7 @@ private fun CourseDateItem( Spacer(modifier = Modifier.height(20.dp)) } if (canShowDate) { - val timeTitle = if (dateBlock.isTimeDifferenceLessThan24Hours()) { - TimeUtils.getFormattedTime(dateBlock.date) - } else { - TimeUtils.getCourseFormattedDate(LocalContext.current, dateBlock.date) - } + val timeTitle = formatToString(context, dateBlock.date, useRelativeDates) Text( text = timeTitle, style = MaterialTheme.appTypography.labelMedium, @@ -683,6 +689,7 @@ private fun CourseDatesScreenPreview() { ), uiMessage = null, isSelfPaced = true, + useRelativeDates = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, @@ -704,6 +711,7 @@ private fun CourseDatesScreenTabletPreview() { ), uiMessage = null, isSelfPaced = true, + useRelativeDates = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, @@ -743,7 +751,7 @@ private val mockedResponse: LinkedHashMap> = CourseDateBlock( title = "Homework 1: ABCD", description = "After this date, course content will be archived", - date = TimeUtils.iso8601ToDate("2023-10-20T15:08:07Z")!!, + date = Date(), dateType = DateType.ASSIGNMENT_DUE_DATE, ) ) @@ -793,9 +801,3 @@ private val mockedResponse: LinkedHashMap> = ) ) ) - -val mockCalendarSyncUIState = CalendarSyncUIState( - isCalendarSyncEnabled = true, - isSynced = true, - checkForOutOfSync = AtomicReference() -) 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 589c103fc..48fd0a524 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 @@ -14,6 +14,7 @@ import org.openedx.core.CalendarRouter import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseBannerType @@ -48,11 +49,13 @@ class CourseDatesViewModel( private val config: Config, private val calendarInteractor: CalendarInteractor, private val calendarNotifier: CalendarNotifier, + private val corePreferences: CorePreferences, val courseRouter: CourseRouter, val calendarRouter: CalendarRouter ) : BaseViewModel() { var isSelfPaced = true + var useRelativeDates = corePreferences.isRelativeDatesEnabled private val _uiState = MutableStateFlow(CourseDatesUIState.Loading) val uiState: StateFlow 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 d40ae18b6..3b2ed4988 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 @@ -315,6 +315,7 @@ private fun CourseOutlineUI( modifier = listPadding.padding(vertical = 4.dp), block = section, onItemClick = onExpandClick, + useRelativeDates = uiState.useRelativeDates, courseSectionsState = courseSectionsState, courseSubSections = courseSubSections, downloadedStateMap = uiState.downloadedState, @@ -504,7 +505,8 @@ private fun CourseOutlineScreenPreview() { verifiedUpgradeLink = "", contentTypeGatingEnabled = false, hasEnded = false - ) + ), + true ), uiMessage = null, onExpandClick = {}, @@ -537,7 +539,8 @@ private fun CourseOutlineScreenTabletPreview() { verifiedUpgradeLink = "", contentTypeGatingEnabled = false, hasEnded = false - ) + ), + true ), uiMessage = null, onExpandClick = {}, diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineUIState.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineUIState.kt index 0307b1f8e..389460442 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineUIState.kt @@ -14,6 +14,7 @@ sealed class CourseOutlineUIState { val courseSectionsState: Map, val subSectionsDownloadsCount: Map, val datesBannerInfo: CourseDatesBannerInfo, + val useRelativeDates: Boolean, ) : CourseOutlineUIState() data object Loading : CourseOutlineUIState() diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 193b5c7e9..0acf4f64a 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -125,6 +125,7 @@ class CourseOutlineViewModel( courseSectionsState = state.courseSectionsState, subSectionsDownloadsCount = subSectionsDownloadsCount, datesBannerInfo = state.datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) } } @@ -158,6 +159,7 @@ class CourseOutlineViewModel( courseSectionsState = courseSectionsState, subSectionsDownloadsCount = subSectionsDownloadsCount, datesBannerInfo = state.datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) courseSectionsState[blockId] ?: false @@ -215,6 +217,7 @@ class CourseOutlineViewModel( courseSectionsState = courseSectionsState, subSectionsDownloadsCount = subSectionsDownloadsCount, datesBannerInfo = datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) courseNotifier.send(CourseLoading(false)) } catch (e: Exception) { diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index f1bbe6086..780a7361d 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -591,6 +591,7 @@ fun VideoSubtitles( fun CourseSection( modifier: Modifier = Modifier, block: Block, + useRelativeDates: Boolean, onItemClick: (Block) -> Unit, courseSectionsState: Boolean?, courseSubSections: List?, @@ -634,7 +635,8 @@ fun CourseSection( ) { CourseSubSectionItem( block = subSectionBlock, - onClick = onSubSectionClick + onClick = onSubSectionClick, + useRelativeDates = useRelativeDates ) } } @@ -745,6 +747,7 @@ fun CourseExpandableChapterCard( fun CourseSubSectionItem( modifier: Modifier = Modifier, block: Block, + useRelativeDates: Boolean, onClick: (Block) -> Unit, ) { val context = LocalContext.current @@ -753,7 +756,7 @@ fun CourseSubSectionItem( val iconColor = if (block.isCompleted()) MaterialTheme.appColors.successGreen else MaterialTheme.appColors.onSurface val due by rememberSaveable { - mutableStateOf(block.due?.let { TimeUtils.getAssignmentFormattedDate(context, it) }) + mutableStateOf(block.due?.let { TimeUtils.formatToString(context, it, useRelativeDates) } ?: "") } val isAssignmentEnable = !block.isCompleted() && block.assignmentProgress != null && !due.isNullOrEmpty() Column( @@ -795,7 +798,7 @@ fun CourseSubSectionItem( stringResource( R.string.course_subsection_assignment_info, block.assignmentProgress?.assignmentType ?: "", - due ?: "", + stringResource(id = coreR.string.core_date_format_assignment_due, due), block.assignmentProgress?.numPointsEarned?.toInt() ?: 0, block.assignmentProgress?.numPointsPossible?.toInt() ?: 0 ) 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 64022f498..73afb3d0b 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 @@ -300,6 +300,7 @@ private fun CourseVideosUI( courseSectionsState = courseSectionsState, courseSubSections = courseSubSections, downloadedStateMap = uiState.downloadedState, + useRelativeDates = uiState.useRelativeDates, onSubSectionClick = onSubSectionClick, onDownloadClick = onDownloadClick ) @@ -632,7 +633,8 @@ private fun CourseVideosScreenPreview() { remainingSize = 0, allCount = 1, allSize = 0 - ) + ), + useRelativeDates = true ), courseTitle = "", onExpandClick = { }, @@ -689,7 +691,7 @@ private fun CourseVideosScreenTabletPreview() { remainingSize = 0, allCount = 0, allSize = 0 - ) + ), useRelativeDates = true ), courseTitle = "", onExpandClick = { }, 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 eb2c2d155..053d5a1f4 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 @@ -168,8 +168,13 @@ class CourseVideoViewModel( _uiState.value = CourseVideosUIState.CourseData( - courseStructure, getDownloadModelsStatus(), courseSubSections, - courseSectionsState, subSectionsDownloadsCount, getDownloadModelsSize() + courseStructure = courseStructure, + downloadedState = getDownloadModelsStatus(), + courseSubSections = courseSubSections, + courseSectionsState = courseSectionsState, + subSectionsDownloadsCount = subSectionsDownloadsCount, + downloadModelsSize = getDownloadModelsSize(), + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) } courseNotifier.send(CourseLoading(false)) 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..44f485c98 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 @@ -12,7 +12,8 @@ sealed class CourseVideosUIState { val courseSubSections: Map>, val courseSectionsState: Map, val subSectionsDownloadsCount: Map, - val downloadModelsSize: DownloadModelsSize + val downloadModelsSize: DownloadModelsSize, + val useRelativeDates: Boolean ) : CourseVideosUIState() data class Empty(val message: String) : CourseVideosUIState() 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 2fb055011..ed4e28f58 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 @@ -28,6 +28,7 @@ import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.model.DateType +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.CourseCalendarState import org.openedx.core.domain.model.CourseDateBlock @@ -65,6 +66,7 @@ class CourseDatesViewModelTest { private val calendarRouter = mockk() private val calendarNotifier = mockk() private val calendarInteractor = mockk() + private val preferencesManager = mockk() private val openEdx = "OpenEdx" private val noInternet = "Slow or no internet connection" @@ -138,6 +140,7 @@ class CourseDatesViewModelTest { coEvery { notifier.send(any()) } returns Unit every { calendarNotifier.notifier } returns flowOf(CalendarSynced) coEvery { calendarNotifier.send(any()) } returns Unit + every { preferencesManager.isRelativeDatesEnabled } returns true coEvery { calendarInteractor.getCourseCalendarStateByIdFromCache(any()) } returns CourseCalendarState( 0, "", @@ -162,6 +165,7 @@ class CourseDatesViewModelTest { config, calendarInteractor, calendarNotifier, + preferencesManager, courseRouter, calendarRouter, ) @@ -191,6 +195,7 @@ class CourseDatesViewModelTest { config, calendarInteractor, calendarNotifier, + preferencesManager, courseRouter, calendarRouter, ) @@ -220,6 +225,7 @@ class CourseDatesViewModelTest { config, calendarInteractor, calendarNotifier, + preferencesManager, courseRouter, calendarRouter, ) @@ -249,6 +255,7 @@ class CourseDatesViewModelTest { config, calendarInteractor, calendarNotifier, + preferencesManager, courseRouter, calendarRouter, ) diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 15901d1b3..255cc6379 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -234,6 +234,7 @@ class CourseOutlineViewModelTest { every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload every { config.getApiHostURL() } returns "http://localhost:8000" every { downloadDialogManager.showDownloadFailedPopup(any(), any()) } returns Unit + every { preferencesManager.isRelativeDatesEnabled } returns true coEvery { interactor.getCourseDates(any()) } returns mockedCourseDatesResult } 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 b8a4d543c..562bca77b 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 @@ -198,6 +198,7 @@ class CourseVideoViewModelTest { Dispatchers.setMain(dispatcher) every { config.getApiHostURL() } returns "http://localhost:8000" every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) + every { preferencesManager.isRelativeDatesEnabled } returns true every { downloadDialogManager.showPopup(any(), any(), any(), any(), any(), any(), any()) } returns Unit } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt index e7e22ba1c..ef583112b 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -419,7 +419,7 @@ fun CourseItem( Column { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(course.course.courseImage.toImageLink(apiHostUrl) ?: "") + .data(course.course.courseImage.toImageLink(apiHostUrl)) .error(R.drawable.core_no_image_course) .placeholder(R.drawable.core_no_image_course) .build(), diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt index c4049f463..fdbc5d5db 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt @@ -3,7 +3,7 @@ package org.openedx.courses.presentation import org.openedx.core.domain.model.CourseEnrollments sealed class DashboardGalleryUIState { - data class Courses(val userCourses: CourseEnrollments) : DashboardGalleryUIState() + data class Courses(val userCourses: CourseEnrollments, val useRelativeDates: Boolean) : DashboardGalleryUIState() data object Empty : DashboardGalleryUIState() data object Loading : DashboardGalleryUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index 5de4c78c5..0fd0e2ccd 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -213,6 +213,7 @@ private fun DashboardGalleryView( UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, + useRelativeDates = uiState.useRelativeDates, apiHostUrl = apiHostUrl, openCourse = { onAction(DashboardGalleryScreenAction.OpenCourse(it)) @@ -274,6 +275,7 @@ private fun UserCourses( modifier: Modifier = Modifier, userCourses: CourseEnrollments, apiHostUrl: String, + useRelativeDates: Boolean, openCourse: (EnrolledCourse) -> Unit, navigateToDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, @@ -290,7 +292,8 @@ private fun UserCourses( apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, resumeBlockId = resumeBlockId, - openCourse = openCourse + openCourse = openCourse, + useRelativeDates = useRelativeDates ) } if (userCourses.enrollments.courses.isNotEmpty()) { @@ -505,6 +508,7 @@ private fun AssignmentItem( private fun PrimaryCourseCard( primaryCourse: EnrolledCourse, apiHostUrl: String, + useRelativeDates: Boolean, navigateToDates: (EnrolledCourse) -> Unit, resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, openCourse: (EnrolledCourse) -> Unit, @@ -527,7 +531,7 @@ private fun PrimaryCourseCard( ) { AsyncImage( model = ImageRequest.Builder(context) - .data(apiHostUrl + primaryCourse.course.courseImage) + .data(primaryCourse.course.courseImage.toImageLink(apiHostUrl)) .error(CoreR.drawable.core_no_image_course) .placeholder(CoreR.drawable.core_no_image_course) .build(), @@ -597,7 +601,10 @@ private fun PrimaryCourseCard( info = stringResource( R.string.dashboard_assignment_due, nearestAssignment.assignmentType ?: "", - TimeUtils.getAssignmentFormattedDate(context, nearestAssignment.date) + stringResource( + id = CoreR.string.core_date_format_assignment_due, + TimeUtils.formatToString(context, nearestAssignment.date, useRelativeDates) + ) ) ) } @@ -856,7 +863,7 @@ private fun ViewAllItemPreview() { private fun DashboardGalleryViewPreview() { OpenEdXTheme { DashboardGalleryView( - uiState = DashboardGalleryUIState.Courses(mockUserCourses), + uiState = DashboardGalleryUIState.Courses(mockUserCourses, true), apiHostUrl = "", uiMessage = null, updating = false, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index 7f1036e1d..fdef55ee7 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -14,6 +14,7 @@ import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager @@ -34,7 +35,8 @@ class DashboardGalleryViewModel( private val networkConnection: NetworkConnection, private val fileUtil: FileUtil, private val dashboardRouter: DashboardRouter, - private val windowSize: WindowSize + private val corePreferences: CorePreferences, + private val windowSize: WindowSize, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -76,7 +78,10 @@ class DashboardGalleryViewModel( if (response.primary == null && response.enrollments.courses.isEmpty()) { _uiState.value = DashboardGalleryUIState.Empty } else { - _uiState.value = DashboardGalleryUIState.Courses(response) + _uiState.value = DashboardGalleryUIState.Courses( + response, + corePreferences.isRelativeDatesEnabled + ) } } else { val courseEnrollments = fileUtil.getObjectFromFile() @@ -84,7 +89,10 @@ class DashboardGalleryViewModel( _uiState.value = DashboardGalleryUIState.Empty } else { _uiState.value = - DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) + DashboardGalleryUIState.Courses( + courseEnrollments.mapToDomain(), + corePreferences.isRelativeDatesEnabled + ) } } } catch (e: Exception) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 579076b96..fefcde867 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -392,7 +392,7 @@ private fun CourseItem( ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(enrolledCourse.course.courseImage.toImageLink(apiHostUrl) ?: "") + .data(enrolledCourse.course.courseImage.toImageLink(apiHostUrl)) .error(CoreR.drawable.core_no_image_course) .placeholder(CoreR.drawable.core_no_image_course) .build(), diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt index 5d0f527bb..4ce446e31 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt @@ -68,15 +68,10 @@ fun ImageHeader( } else { ContentScale.Crop } - val imageUrl = if (courseImage?.isLinkValid() == true) { - courseImage - } else { - apiHostUrl + courseImage - } Box(modifier = modifier, contentAlignment = Alignment.Center) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) + .data(courseImage?.toImageLink(apiHostUrl)) .error(CoreR.drawable.core_no_image_course) .placeholder(CoreR.drawable.core_no_image_course) .build(), diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index 112a4e774..fcc6db153 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -59,6 +59,9 @@ class CalendarFragment : Fragment() { onCalendarSyncSwitchClick = { viewModel.setCalendarSyncEnabled(it, requireActivity().supportFragmentManager) }, + onRelativeDateSwitchClick = { + viewModel.setRelativeDateEnabled(it) + }, onChangeSyncOptionClick = { val dialog = NewCalendarDialogFragment.newInstance(NewCalendarDialogType.UPDATE) dialog.show( @@ -84,11 +87,14 @@ private fun CalendarView( onChangeSyncOptionClick: () -> Unit, onCourseToSyncClick: () -> Unit, onCalendarSyncSwitchClick: (Boolean) -> Unit, + onRelativeDateSwitchClick: (Boolean) -> Unit ) { if (!uiState.isCalendarExist) { CalendarSetUpView( windowSize = windowSize, + useRelativeDates = uiState.isRelativeDateEnabled, setUpCalendarSync = setUpCalendarSync, + onRelativeDateSwitchClick = onRelativeDateSwitchClick, onBackClick = onBackClick ) } else { @@ -97,6 +103,7 @@ private fun CalendarView( uiState = uiState, onBackClick = onBackClick, onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, + onRelativeDateSwitchClick = onRelativeDateSwitchClick, onChangeSyncOptionClick = onChangeSyncOptionClick, onCourseToSyncClick = onCourseToSyncClick ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt index 06a842630..7309a42f9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt @@ -55,7 +55,9 @@ import org.openedx.profile.R @Composable fun CalendarSetUpView( windowSize: WindowSize, + useRelativeDates: Boolean, setUpCalendarSync: () -> Unit, + onRelativeDateSwitchClick: (Boolean) -> Unit, onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -192,6 +194,11 @@ fun CalendarSetUpView( Spacer(modifier = Modifier.height(24.dp)) } } + Spacer(modifier = Modifier.height(28.dp)) + OptionsSection( + isRelativeDatesEnabled = useRelativeDates, + onRelativeDateSwitchClick = onRelativeDateSwitchClick + ) } } } @@ -206,7 +213,9 @@ private fun CalendarScreenPreview() { OpenEdXTheme { CalendarSetUpView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + useRelativeDates = true, setUpCalendarSync = {}, + onRelativeDateSwitchClick = { _ -> }, onBackClick = {} ) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index bce3ede77..d8c2e9a55 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -67,6 +67,7 @@ fun CalendarSettingsView( windowSize: WindowSize, uiState: CalendarUIState, onCalendarSyncSwitchClick: (Boolean) -> Unit, + onRelativeDateSwitchClick: (Boolean) -> Unit, onChangeSyncOptionClick: () -> Unit, onCourseToSyncClick: () -> Unit, onBackClick: () -> Unit @@ -155,6 +156,11 @@ fun CalendarSettingsView( onCourseToSyncClick = onCourseToSyncClick ) } + Spacer(modifier = Modifier.height(32.dp)) + OptionsSection( + isRelativeDatesEnabled = uiState.isRelativeDateEnabled, + onRelativeDateSwitchClick = onRelativeDateSwitchClick + ) } } } @@ -312,10 +318,12 @@ private fun CalendarSettingsViewPreview() { calendarData = CalendarData("calendar", Color.Red.toArgb()), calendarSyncState = CalendarSyncState.SYNCED, isCalendarSyncEnabled = false, + isRelativeDateEnabled = true, coursesSynced = 5 ), onBackClick = {}, onCalendarSyncSwitchClick = {}, + onRelativeDateSwitchClick = {}, onChangeSyncOptionClick = {}, onCourseToSyncClick = {} ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index cf99e0fa2..513a5c5e5 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -8,5 +8,6 @@ data class CalendarUIState( val calendarData: CalendarData? = null, val calendarSyncState: CalendarSyncState, val isCalendarSyncEnabled: Boolean, - val coursesSynced: Int? + val coursesSynced: Int?, + val isRelativeDateEnabled: Boolean, ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt new file mode 100644 index 000000000..4cc682dc7 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt @@ -0,0 +1,73 @@ +package org.openedx.profile.presentation.calendar + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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.unit.dp +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography +import org.openedx.core.utils.TimeUtils +import org.openedx.profile.R +import java.util.Date + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun OptionsSection( + isRelativeDatesEnabled: Boolean, + onRelativeDateSwitchClick: (Boolean) -> Unit +) { + val context = LocalContext.current + val textDescription = if (isRelativeDatesEnabled) { + stringResource(R.string.profile_show_relative_dates) + } else { + stringResource( + R.string.profile_show_full_dates, + TimeUtils.formatToString(context, Date(), false) + ) + } + Column { + SectionTitle(stringResource(R.string.profile_options)) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.profile_use_relative_dates), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Switch( + modifier = Modifier + .padding(0.dp), + checked = isRelativeDatesEnabled, + onCheckedChange = onRelativeDateSwitchClick, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.appColors.textAccent + ) + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = textDescription, + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + } +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt index 658d7ca8e..c50bf587c 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState import org.openedx.core.system.CalendarManager @@ -30,6 +31,7 @@ class CalendarViewModel( private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, private val calendarInteractor: CalendarInteractor, + private val corePreferences: CorePreferences, private val profileRouter: ProfileRouter, private val networkConnection: NetworkConnection, ) : BaseViewModel() { @@ -40,7 +42,8 @@ class CalendarViewModel( calendarData = null, calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, - coursesSynced = null + coursesSynced = null, + isRelativeDateEnabled = corePreferences.isRelativeDatesEnabled, ) private val _uiState = MutableStateFlow(calendarInitState) @@ -107,6 +110,11 @@ class CalendarViewModel( } } + fun setRelativeDateEnabled(isEnabled: Boolean) { + corePreferences.isRelativeDatesEnabled = isEnabled + _uiState.update { it.copy(isRelativeDateEnabled = isEnabled) } + } + fun navigateToCoursesToSync(fragmentManager: FragmentManager) { profileRouter.navigateToCoursesToSync(fragmentManager) } diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 41535240c..1adf22c97 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -78,5 +78,6 @@ No %1$s Courses No courses are currently being synced to your calendar. No courses match the current filter. + Show full dates like “%1$s”