From 01118a67b7dc41e682b5a5a7940c87d720e769cf Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 18 Jul 2024 15:58:38 +0300 Subject: [PATCH 1/2] feat: relative dates --- .../app/data/storage/PreferencesManager.kt | 7 + .../java/org/openedx/app/di/ScreenModule.kt | 7 +- .../core/data/storage/CorePreferences.kt | 1 + .../core/domain/model/CourseDateBlock.kt | 6 - .../org/openedx/core/extension/StringExt.kt | 7 + .../java/org/openedx/core/utils/TimeUtils.kt | 149 ++---------------- core/src/main/res/values/strings.xml | 16 +- .../presentation/dates/CourseDatesScreen.kt | 33 ++-- .../dates/CourseDatesViewModel.kt | 3 + .../outline/CourseOutlineScreen.kt | 7 +- .../outline/CourseOutlineUIState.kt | 1 + .../outline/CourseOutlineViewModel.kt | 3 + .../course/presentation/ui/CourseUI.kt | 11 +- .../course/presentation/ui/CourseVideosUI.kt | 6 +- .../videos/CourseVideoViewModel.kt | 14 +- .../videos/CourseVideosUIState.kt | 3 +- .../dates/CourseDatesViewModelTest.kt | 7 + .../outline/CourseOutlineViewModelTest.kt | 1 + .../videos/CourseVideoViewModelTest.kt | 19 +-- .../presentation/AllEnrolledCoursesView.kt | 3 +- .../presentation/DashboardGalleryUIState.kt | 2 +- .../presentation/DashboardGalleryView.kt | 18 ++- .../presentation/DashboardGalleryViewModel.kt | 14 +- .../presentation/DashboardListFragment.kt | 4 +- .../discovery/presentation/ui/DiscoveryUI.kt | 9 +- .../presentation/calendar/CalendarFragment.kt | 7 + .../calendar/CalendarSetUpView.kt | 9 ++ .../calendar/CalendarSettingsView.kt | 8 + .../presentation/calendar/CalendarUIState.kt | 3 +- .../presentation/calendar/CalendarView.kt | 71 +++++++++ .../calendar/CalendarViewModel.kt | 10 +- profile/src/main/res/values/strings.xml | 1 + 32 files changed, 231 insertions(+), 229 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 ae36968d2..88612fa15 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 @@ -193,6 +193,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) @@ -223,6 +229,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 ae550922c..29f8da272 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -150,6 +150,7 @@ val screenModule = module { get(), get(), get(), + get(), windowSize ) } @@ -202,7 +203,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()) } @@ -271,7 +272,7 @@ val screenModule = module { get(), get(), get(), - get() + get(), ) } viewModel { (courseId: String) -> @@ -313,7 +314,6 @@ val screenModule = module { get(), get(), get(), - get() ) } viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) } @@ -354,6 +354,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 343398782..d383cf57f 100644 --- a/core/src/main/java/org/openedx/core/extension/StringExt.kt +++ b/core/src/main/java/org/openedx/core/extension/StringExt.kt @@ -37,3 +37,10 @@ fun String.tagId(): String = this.replaceSpace("_").lowercase(Locale.getDefault( fun String.takeIfNotEmpty(): String? { return if (this.isEmpty().not()) this else null } + +fun String.toImageLink(apiHostURL: String): String = + if (this.isLinkValid()) { + this + } else { + (apiHostURL + this).replace(Regex("(? { - 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 +229,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 9aded8c31..10acbe297 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 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 69f6e0559..84032b599 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: DatesUIState, 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,6 +598,7 @@ private fun CourseDateItem( dateBlock: CourseDateBlock, canShowDate: Boolean, isMiddleChild: Boolean, + useRelativeDates: Boolean, onItemClick: (CourseDateBlock) -> Unit, ) { Column( @@ -601,11 +610,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(dateBlock.date, useRelativeDates) Text( text = timeTitle, style = MaterialTheme.appTypography.labelMedium, @@ -683,6 +688,7 @@ private fun CourseDatesScreenPreview() { ), uiMessage = null, isSelfPaced = true, + useRelativeDates = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, @@ -704,6 +710,7 @@ private fun CourseDatesScreenTabletPreview() { ), uiMessage = null, isSelfPaced = true, + useRelativeDates = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, @@ -743,7 +750,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 +800,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 addad3199..1054e79e5 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(DatesUIState.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 1f31b32de..d69da74aa 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 b65b3b62a..5755cf0f4 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 @@ -118,6 +118,7 @@ class CourseOutlineViewModel( courseSectionsState = state.courseSectionsState, subSectionsDownloadsCount = subSectionsDownloadsCount, datesBannerInfo = state.datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) } } @@ -165,6 +166,7 @@ class CourseOutlineViewModel( courseSectionsState = courseSectionsState, subSectionsDownloadsCount = subSectionsDownloadsCount, datesBannerInfo = state.datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled ) courseSectionsState[blockId] ?: false @@ -221,6 +223,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 b111dd1f0..3bc1779b0 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 @@ -65,7 +65,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -591,6 +590,7 @@ fun VideoSubtitles( fun CourseSection( modifier: Modifier = Modifier, block: Block, + useRelativeDates: Boolean, onItemClick: (Block) -> Unit, courseSectionsState: Boolean?, courseSubSections: List?, @@ -635,7 +635,8 @@ fun CourseSection( ) { CourseSubSectionItem( block = subSectionBlock, - onClick = onSubSectionClick + onClick = onSubSectionClick, + useRelativeDates = useRelativeDates ) } } @@ -746,15 +747,15 @@ fun CourseExpandableChapterCard( fun CourseSubSectionItem( modifier: Modifier = Modifier, block: Block, + useRelativeDates: Boolean, onClick: (Block) -> Unit, ) { - val context = LocalContext.current val icon = if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource(coreR.drawable.ic_core_chapter_icon) 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(it, useRelativeDates) } ?: "") } val isAssignmentEnable = !block.isCompleted() && block.assignmentProgress != null && !due.isNullOrEmpty() Column( @@ -796,7 +797,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 1a406181d..bfdb86a23 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 @@ -292,6 +292,7 @@ private fun CourseVideosUI( courseSectionsState = courseSectionsState, courseSubSections = courseSubSections, downloadedStateMap = uiState.downloadedState, + useRelativeDates = uiState.useRelativeDates, onSubSectionClick = onSubSectionClick, onDownloadClick = onDownloadClick ) @@ -624,7 +625,8 @@ private fun CourseVideosScreenPreview() { remainingSize = 0, allCount = 1, allSize = 0 - ) + ), + useRelativeDates = true ), courseTitle = "", onExpandClick = { }, @@ -681,7 +683,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 a5bf069cd..138f3c468 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 @@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BlockType import org.openedx.core.UIMessage -import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.VideoSettings @@ -36,7 +35,6 @@ import org.openedx.course.presentation.CourseRouter class CourseVideoViewModel( val courseId: String, val courseTitle: String, - private val config: Config, private val interactor: CourseInteractor, private val resourceManager: ResourceManager, private val networkConnection: NetworkConnection, @@ -55,9 +53,6 @@ class CourseVideoViewModel( workerController, coreAnalytics ) { - - val isCourseNestedListEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled - private val _uiState = MutableStateFlow(CourseVideosUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -163,8 +158,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 ca0c18c79..a7acd1d17 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 aad650b28..1a71e7a29 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 @@ -223,6 +223,7 @@ class CourseOutlineViewModelTest { every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload every { config.getApiHostURL() } returns "http://localhost:8000" + 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 9bb8d0f5f..925265140 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 @@ -30,7 +30,6 @@ import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.BlockType import org.openedx.core.UIMessage -import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block @@ -64,7 +63,6 @@ class CourseVideoViewModelTest { private val dispatcher = StandardTestDispatcher() - private val config = mockk() private val resourceManager = mockk() private val interactor = mockk() private val courseNotifier = spyk() @@ -186,8 +184,8 @@ class CourseVideoViewModelTest { 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" every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) + every { preferencesManager.isRelativeDatesEnabled } returns true } @After @@ -197,7 +195,6 @@ class CourseVideoViewModelTest { @Test fun `getVideos empty list`() = runTest { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure.copy(blockData = emptyList()) every { downloadDao.readAllData() } returns flow { emit(emptyList()) } @@ -205,7 +202,6 @@ class CourseVideoViewModelTest { val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -229,7 +225,6 @@ class CourseVideoViewModelTest { @Test fun `getVideos success`() = runTest { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure every { downloadDao.readAllData() } returns flow { emit(emptyList()) } every { preferencesManager.videoSettings } returns VideoSettings.default @@ -237,7 +232,6 @@ class CourseVideoViewModelTest { val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -262,7 +256,6 @@ class CourseVideoViewModelTest { @Test fun `updateVideos success`() = runTest { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { courseNotifier.notifier } returns flow { emit(CourseStructureUpdated("")) @@ -277,7 +270,6 @@ class CourseVideoViewModelTest { val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -305,7 +297,6 @@ class CourseVideoViewModelTest { @Test fun `setIsUpdating success`() = runTest { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } @@ -314,12 +305,10 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels test`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -351,12 +340,10 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels only wifi download, with connection`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -392,12 +379,10 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels only wifi download, without connection`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", "", - config, interactor, resourceManager, networkConnection, @@ -428,6 +413,4 @@ class CourseVideoViewModelTest { assert(message.await()?.message.isNullOrEmpty()) } - - } 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 c2668f766..ef583112b 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -81,6 +81,7 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress +import org.openedx.core.extension.toImageLink import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog @@ -418,7 +419,7 @@ fun CourseItem( Column { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(apiHostUrl + course.course.courseImage) + .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 a6d375569..d7cb36fad 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -83,6 +83,7 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress +import org.openedx.core.extension.toImageLink import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.OpenEdXButton @@ -212,6 +213,7 @@ private fun DashboardGalleryView( UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, + useRelativeDates = uiState.useRelativeDates, apiHostUrl = apiHostUrl, openCourse = { onAction(DashboardGalleryScreenAction.OpenCourse(it)) @@ -273,6 +275,7 @@ private fun UserCourses( modifier: Modifier = Modifier, userCourses: CourseEnrollments, apiHostUrl: String, + useRelativeDates: Boolean, openCourse: (EnrolledCourse) -> Unit, navigateToDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, @@ -289,7 +292,8 @@ private fun UserCourses( apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, resumeBlockId = resumeBlockId, - openCourse = openCourse + openCourse = openCourse, + useRelativeDates = useRelativeDates ) } if (userCourses.enrollments.courses.isNotEmpty()) { @@ -420,7 +424,7 @@ private fun CourseListItem( Column { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(apiHostUrl + course.course.courseImage) + .data(course.course.courseImage.toImageLink(apiHostUrl)) .error(CoreR.drawable.core_no_image_course) .placeholder(CoreR.drawable.core_no_image_course) .build(), @@ -504,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, @@ -526,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(), @@ -596,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(nearestAssignment.date, useRelativeDates) + ) ) ) } @@ -855,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 2d8e81d6b..fefcde867 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -81,6 +81,7 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress +import org.openedx.core.extension.toImageLink import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage @@ -373,7 +374,6 @@ private fun CourseItem( ) ) } - val imageUrl = apiHostUrl + enrolledCourse.course.courseImage val context = LocalContext.current Surface( modifier = Modifier @@ -392,7 +392,7 @@ private fun CourseItem( ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) + .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 30c2a63d2..72118bcf2 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 @@ -39,7 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest -import org.openedx.core.extension.isLinkValid +import org.openedx.core.extension.toImageLink import org.openedx.core.ui.WindowSize import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -67,15 +67,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..423ac9726 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt @@ -0,0 +1,71 @@ +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.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 textDescription = if (isRelativeDatesEnabled) { + stringResource(R.string.profile_show_relative_dates) + } else { + stringResource( + R.string.profile_show_full_dates, + TimeUtils.formatToString(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” From 9b466797b39184f28e545403a1e23ccbdee530b2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 25 Jul 2024 13:15:14 +0300 Subject: [PATCH 2/2] fix: Fixes according to designer feedback --- .../java/org/openedx/core/utils/TimeUtils.kt | 57 ++++++++++++++++--- core/src/main/res/values/strings.xml | 1 + .../presentation/dates/CourseDatesScreen.kt | 3 +- .../course/presentation/ui/CourseUI.kt | 4 +- .../presentation/DashboardGalleryView.kt | 2 +- default_config/stage/config.yaml | 4 +- .../presentation/calendar/CalendarView.kt | 4 +- 7 files changed, 61 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index bb2d3b13c..bb7c320e5 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -20,18 +20,59 @@ object TimeUtils { private const val FORMAT_ISO_8601_WITH_TIME_ZONE = "yyyy-MM-dd'T'HH:mm:ssXXX" private const val SEVEN_DAYS_IN_MILLIS = 604800000L - fun formatToString(date: Date, useRelativeDates: Boolean): String { - return if (useRelativeDates) { - DateUtils.getRelativeTimeSpanString( + fun formatToString(context: Context, date: Date, useRelativeDates: Boolean): String { + if (!useRelativeDates) { + val locale = Locale(Locale.getDefault().language) + val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale) + return dateFormat.format(date) + } + + val now = Calendar.getInstance() + val inputDate = Calendar.getInstance().apply { time = date } + val daysDiff = ((now.timeInMillis - inputDate.timeInMillis) / (1000 * 60 * 60 * 24)).toInt() + return when { + daysDiff in -5..-1 -> 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, - getCurrentTime(), + now.timeInMillis, DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_ABBREV_TIME ).toString() - } else { - val locale = Locale(Locale.getDefault().language) - val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale) - dateFormat.format(date) + + 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() + } } } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 10acbe297..9baa7672e 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -175,4 +175,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 84032b599..1e1e0bd7f 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 @@ -601,6 +601,7 @@ private fun CourseDateItem( useRelativeDates: Boolean, onItemClick: (CourseDateBlock) -> Unit, ) { + val context = LocalContext.current Column( modifier = Modifier .wrapContentHeight() @@ -610,7 +611,7 @@ private fun CourseDateItem( Spacer(modifier = Modifier.height(20.dp)) } if (canShowDate) { - val timeTitle = formatToString(dateBlock.date, useRelativeDates) + val timeTitle = formatToString(context, dateBlock.date, useRelativeDates) Text( text = timeTitle, style = MaterialTheme.appTypography.labelMedium, 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 3bc1779b0..cb03eb5ac 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 @@ -65,6 +65,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -750,12 +751,13 @@ fun CourseSubSectionItem( useRelativeDates: Boolean, onClick: (Block) -> Unit, ) { + val context = LocalContext.current val icon = if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource(coreR.drawable.ic_core_chapter_icon) val iconColor = if (block.isCompleted()) MaterialTheme.appColors.successGreen else MaterialTheme.appColors.onSurface val due by rememberSaveable { - mutableStateOf(block.due?.let { TimeUtils.formatToString(it, useRelativeDates) } ?: "") + mutableStateOf(block.due?.let { TimeUtils.formatToString(context, it, useRelativeDates) } ?: "") } val isAssignmentEnable = !block.isCompleted() && block.assignmentProgress != null && !due.isNullOrEmpty() Column( 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 d7cb36fad..1eb0cb5cf 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -603,7 +603,7 @@ private fun PrimaryCourseCard( nearestAssignment.assignmentType ?: "", stringResource( id = CoreR.string.core_date_format_assignment_due, - TimeUtils.formatToString(nearestAssignment.date, useRelativeDates) + TimeUtils.formatToString(context, nearestAssignment.date, useRelativeDates) ) ) ) diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index a97d7c351..72f718240 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -1,10 +1,10 @@ -API_HOST_URL: 'http://localhost:8000' +API_HOST_URL: 'https://axim-mobile.raccoongang.net' APPLICATION_ID: 'org.openedx.app' ENVIRONMENT_DISPLAY_NAME: 'Localhost' URI_SCHEME: '' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' FAQ_URL: '' -OAUTH_CLIENT_ID: 'OAUTH_CLIENT_ID' +OAUTH_CLIENT_ID: 'zP3vPz00c8fTRpYjNbVSlA1fxt9LnCxTM4JK1KQ0' # Keep empty to hide setting AGREEMENT_URLS: 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 index 423ac9726..4cc682dc7 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarView.kt @@ -15,6 +15,7 @@ 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 @@ -29,12 +30,13 @@ 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(Date(), false) + TimeUtils.formatToString(context, Date(), false) ) } Column {