From 3af5146508b4034a0238d32ca6f3f7b68d06cb3d Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 14 Feb 2025 20:20:17 +0200 Subject: [PATCH 01/28] feat: dates tab UI --- app/src/main/res/menu/bottom_view_menu.xml | 0 .../presentation/dates/DueDateCategory.kt | 31 +++++++++++++++++++ dates/src/main/res/layout/fragment_dates.xml | 6 ++++ 3 files changed, 37 insertions(+) create mode 100644 app/src/main/res/menu/bottom_view_menu.xml create mode 100644 dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt create mode 100644 dates/src/main/res/layout/fragment_dates.xml diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml new file mode 100644 index 000000000..e69de29bb diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt new file mode 100644 index 000000000..78ebda298 --- /dev/null +++ b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt @@ -0,0 +1,31 @@ +package org.openedx.dates.presentation.dates + +import androidx.annotation.StringRes +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import org.openedx.core.ui.theme.appColors +import org.openedx.dates.R + +enum class DueDateCategory( + @StringRes + val label: Int, +) { + PAST_DUE(R.string.dates_category_past_due), + TODAY(R.string.dates_category_today), + THIS_WEEK(R.string.dates_category_this_week), + NEXT_WEEK(R.string.dates_category_next_week), + UPCOMING(R.string.dates_category_upcoming); + + val color: Color + @Composable + get() { + return when (this) { + PAST_DUE -> MaterialTheme.appColors.warning + TODAY -> MaterialTheme.appColors.info + THIS_WEEK -> MaterialTheme.appColors.textPrimaryVariant + NEXT_WEEK -> MaterialTheme.appColors.textFieldBorder + UPCOMING -> MaterialTheme.appColors.divider + } + } +} diff --git a/dates/src/main/res/layout/fragment_dates.xml b/dates/src/main/res/layout/fragment_dates.xml new file mode 100644 index 000000000..77d9ef65f --- /dev/null +++ b/dates/src/main/res/layout/fragment_dates.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From b54fcc977800fb67af9d39382c8972f59b4ce646 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 17 Feb 2025 12:27:53 +0200 Subject: [PATCH 02/28] feat: added config flag for enabling/disabling dates screen --- core/src/main/java/org/openedx/core/config/DatesConfig.kt | 8 ++++++++ default_config/dev/config.yaml | 3 +++ default_config/prod/config.yaml | 3 +++ default_config/stage/config.yaml | 3 +++ 4 files changed, 17 insertions(+) create mode 100644 core/src/main/java/org/openedx/core/config/DatesConfig.kt diff --git a/core/src/main/java/org/openedx/core/config/DatesConfig.kt b/core/src/main/java/org/openedx/core/config/DatesConfig.kt new file mode 100644 index 000000000..0e48a5ed5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/DatesConfig.kt @@ -0,0 +1,8 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class DatesConfig( + @SerializedName("ENABLED") + val isEnabled: Boolean = true, +) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 952e041de..e6ab8bce2 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,6 +31,9 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' +DATES: + ENABLED: true + FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index 952e041de..e6ab8bce2 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -31,6 +31,9 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' +DATES: + ENABLED: true + FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index 952e041de..e6ab8bce2 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -31,6 +31,9 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' +DATES: + ENABLED: true + FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false From 40b5a9ec718da865b7d098577a965e5e8b89db21 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 17 Mar 2025 20:42:36 +0200 Subject: [PATCH 03/28] feat: paging and caching --- .../dates/data/storage/CourseDateEntity.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt new file mode 100644 index 000000000..558da6870 --- /dev/null +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -0,0 +1,53 @@ +package org.openedx.dates.data.storage + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.openedx.core.data.model.CourseDate +import org.openedx.core.utils.TimeUtils +import org.openedx.core.domain.model.CourseDate as DomainCourseDate + +@Entity(tableName = "course_date_table") +data class CourseDateEntity( + @PrimaryKey + @ColumnInfo("assignmentBlockId") + val assignmentBlockId: String, + @ColumnInfo("courseId") + val courseId: String, + @ColumnInfo("dueDate") + val dueDate: String?, + @ColumnInfo("assignmentTitle") + val assignmentTitle: String?, + @ColumnInfo("learnerHasAccess") + val learnerHasAccess: Boolean?, + @ColumnInfo("courseName") + val courseName: String?, +) { + + fun mapToDomain(): DomainCourseDate? { + val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") + return DomainCourseDate( + courseId = courseId, + assignmentBlockId = assignmentBlockId, + dueDate = dueDate ?: return null, + assignmentTitle = assignmentTitle ?: "", + learnerHasAccess = learnerHasAccess ?: false, + courseName = courseName ?: "" + ) + } + + companion object { + fun createFrom(courseDate: CourseDate): CourseDateEntity { + with(courseDate) { + return CourseDateEntity( + courseId = courseId, + assignmentBlockId = assignmentBlockId, + dueDate = dueDate, + assignmentTitle = assignmentTitle, + learnerHasAccess = learnerHasAccess, + courseName = courseName + ) + } + } + } +} From 839f80a5865bf98f8b1a7c4d64e8d204dd1691fd Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 18 Mar 2025 13:09:46 +0200 Subject: [PATCH 04/28] feat: navigating to block --- .../openedx/dates/presentation/dates/DueDateCategory.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt index 78ebda298..4cd305a56 100644 --- a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt +++ b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt @@ -11,11 +11,11 @@ enum class DueDateCategory( @StringRes val label: Int, ) { - PAST_DUE(R.string.dates_category_past_due), - TODAY(R.string.dates_category_today), - THIS_WEEK(R.string.dates_category_this_week), + UPCOMING(R.string.dates_category_upcoming), NEXT_WEEK(R.string.dates_category_next_week), - UPCOMING(R.string.dates_category_upcoming); + THIS_WEEK(R.string.dates_category_this_week), + TODAY(R.string.dates_category_today), + PAST_DUE(R.string.dates_category_past_due); val color: Color @Composable From f9c025057cf8346d3d77e1f877204d5e435124f4 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 17:23:05 +0200 Subject: [PATCH 05/28] feat: reuse dates UI from CourseDatesScreen --- .../presentation/dates/DueDateCategory.kt | 31 ------------------- default_config/dev/config.yaml | 2 +- default_config/prod/config.yaml | 2 +- default_config/stage/config.yaml | 2 +- 4 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt deleted file mode 100644 index 4cd305a56..000000000 --- a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.openedx.dates.presentation.dates - -import androidx.annotation.StringRes -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import org.openedx.core.ui.theme.appColors -import org.openedx.dates.R - -enum class DueDateCategory( - @StringRes - val label: Int, -) { - UPCOMING(R.string.dates_category_upcoming), - NEXT_WEEK(R.string.dates_category_next_week), - THIS_WEEK(R.string.dates_category_this_week), - TODAY(R.string.dates_category_today), - PAST_DUE(R.string.dates_category_past_due); - - val color: Color - @Composable - get() { - return when (this) { - PAST_DUE -> MaterialTheme.appColors.warning - TODAY -> MaterialTheme.appColors.info - THIS_WEEK -> MaterialTheme.appColors.textPrimaryVariant - NEXT_WEEK -> MaterialTheme.appColors.textFieldBorder - UPCOMING -> MaterialTheme.appColors.divider - } - } -} diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index e6ab8bce2..ac06ef7ba 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,7 +31,7 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -DATES: +APP_LEVEL_DATES: ENABLED: true FIREBASE: diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index e6ab8bce2..ac06ef7ba 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -31,7 +31,7 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -DATES: +APP_LEVEL_DATES: ENABLED: true FIREBASE: diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index e6ab8bce2..ac06ef7ba 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -31,7 +31,7 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -DATES: +APP_LEVEL_DATES: ENABLED: true FIREBASE: From d1d49f27d71c01888f9742707b44c9c83d8f4c11 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 17:54:38 +0200 Subject: [PATCH 06/28] feat: shift due date card --- .../java/org/openedx/dates/data/storage/CourseDateEntity.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt index 558da6870..ec751d1ee 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -20,6 +20,8 @@ data class CourseDateEntity( val assignmentTitle: String?, @ColumnInfo("learnerHasAccess") val learnerHasAccess: Boolean?, + @ColumnInfo("relative") + val relative: Boolean?, @ColumnInfo("courseName") val courseName: String?, ) { @@ -32,6 +34,7 @@ data class CourseDateEntity( dueDate = dueDate ?: return null, assignmentTitle = assignmentTitle ?: "", learnerHasAccess = learnerHasAccess ?: false, + relative = relative ?: false, courseName = courseName ?: "" ) } @@ -45,6 +48,7 @@ data class CourseDateEntity( dueDate = dueDate, assignmentTitle = assignmentTitle, learnerHasAccess = learnerHasAccess, + relative = relative, courseName = courseName ) } From 1555721e18ad316adb731f687523f97222406058 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 18:11:51 +0200 Subject: [PATCH 07/28] feat: shift due date request --- .../java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt new file mode 100644 index 000000000..df6749f24 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt @@ -0,0 +1,7 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName + +data class ShiftDueDatesBody( + @SerializedName("course_keys") val courseKeys: List +) \ No newline at end of file From bc54bbf1e1073c33698f3051e2377b02b05a72d4 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 21 Mar 2025 12:42:48 +0200 Subject: [PATCH 08/28] fix: changes according detekt warnings --- .../main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt index df6749f24..63e66363d 100644 --- a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt +++ b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt @@ -4,4 +4,4 @@ import com.google.gson.annotations.SerializedName data class ShiftDueDatesBody( @SerializedName("course_keys") val courseKeys: List -) \ No newline at end of file +) From 6a6ee73e33efe28953c53e4ebbe938116bb52a7f Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 25 Mar 2025 13:40:28 +0200 Subject: [PATCH 09/28] feat: pagination --- .../dates/data/storage/CourseDateEntity.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt index ec751d1ee..de7705d54 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -9,20 +9,22 @@ import org.openedx.core.domain.model.CourseDate as DomainCourseDate @Entity(tableName = "course_date_table") data class CourseDateEntity( - @PrimaryKey - @ColumnInfo("assignmentBlockId") - val assignmentBlockId: String, - @ColumnInfo("courseId") + @PrimaryKey(autoGenerate = true) + @ColumnInfo("course_date_id") + val id: Int, + @ColumnInfo("course_date_first_component_block_id") + val firstComponentBlockId: String?, + @ColumnInfo("course_date_courseId") val courseId: String, - @ColumnInfo("dueDate") + @ColumnInfo("course_date_dueDate") val dueDate: String?, - @ColumnInfo("assignmentTitle") + @ColumnInfo("course_date_assignmentTitle") val assignmentTitle: String?, - @ColumnInfo("learnerHasAccess") + @ColumnInfo("course_date_learnerHasAccess") val learnerHasAccess: Boolean?, - @ColumnInfo("relative") + @ColumnInfo("course_date_relative") val relative: Boolean?, - @ColumnInfo("courseName") + @ColumnInfo("course_date_courseName") val courseName: String?, ) { @@ -30,7 +32,7 @@ data class CourseDateEntity( val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") return DomainCourseDate( courseId = courseId, - assignmentBlockId = assignmentBlockId, + firstComponentBlockId = firstComponentBlockId ?: "", dueDate = dueDate ?: return null, assignmentTitle = assignmentTitle ?: "", learnerHasAccess = learnerHasAccess ?: false, @@ -43,8 +45,9 @@ data class CourseDateEntity( fun createFrom(courseDate: CourseDate): CourseDateEntity { with(courseDate) { return CourseDateEntity( + id = 0, courseId = courseId, - assignmentBlockId = assignmentBlockId, + firstComponentBlockId = firstComponentBlockId, dueDate = dueDate, assignmentTitle = assignmentTitle, learnerHasAccess = learnerHasAccess, From 4faaa02b0615182e8245d362895611e601cb5dec Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 27 Mar 2025 18:12:09 +0200 Subject: [PATCH 10/28] fix: pagination bugs --- .../java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt deleted file mode 100644 index 63e66363d..000000000 --- a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.openedx.core.data.model - -import com.google.gson.annotations.SerializedName - -data class ShiftDueDatesBody( - @SerializedName("course_keys") val courseKeys: List -) From 6303c91b0a8ccf4d61b451821b349e13319223f4 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 31 Mar 2025 16:53:15 +0300 Subject: [PATCH 11/28] feat: cache-first logic --- app/src/main/res/menu/bottom_view_menu.xml | 0 .../org/openedx/core/config/DatesConfig.kt | 8 --- .../core/data/model/CourseDatesResponse.kt | 4 +- .../model/room/CourseDatesResponseEntity.kt | 51 ++++++++++++++++--- .../course/data/storage/CourseConverter.kt | 13 +++++ default_config/dev/config.yaml | 3 -- 6 files changed, 60 insertions(+), 19 deletions(-) delete mode 100644 app/src/main/res/menu/bottom_view_menu.xml delete mode 100644 core/src/main/java/org/openedx/core/config/DatesConfig.kt rename dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt => core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt (54%) diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/src/main/java/org/openedx/core/config/DatesConfig.kt b/core/src/main/java/org/openedx/core/config/DatesConfig.kt deleted file mode 100644 index 0e48a5ed5..000000000 --- a/core/src/main/java/org/openedx/core/config/DatesConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.openedx.core.config - -import com.google.gson.annotations.SerializedName - -data class DatesConfig( - @SerializedName("ENABLED") - val isEnabled: Boolean = true, -) diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt index c86500671..28a1b28dd 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt @@ -50,7 +50,9 @@ data class CourseDatesResponse( count = count, next = next, previous = previous, - results = results.mapNotNull { it.mapToDomain() } + results = results + .mapNotNull { it.mapToDomain() } + .sortedBy { it.dueDate } ) } } diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt similarity index 54% rename from dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt rename to core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt index de7705d54..5231a5604 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt @@ -1,17 +1,55 @@ -package org.openedx.dates.data.storage +package org.openedx.core.data.model.room import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import org.openedx.core.data.model.CourseDate +import org.openedx.core.data.model.CourseDatesResponse import org.openedx.core.utils.TimeUtils import org.openedx.core.domain.model.CourseDate as DomainCourseDate +import org.openedx.core.domain.model.CourseDatesResponse as DomainCourseDatesResponse -@Entity(tableName = "course_date_table") -data class CourseDateEntity( +@Entity(tableName = "course_dates_response_table") +data class CourseDatesResponseEntity( @PrimaryKey(autoGenerate = true) - @ColumnInfo("course_date_id") + @ColumnInfo("course_date_response_id") val id: Int, + @ColumnInfo("course_date_response_count") + val count: Int, + @ColumnInfo("course_date_response_next") + val next: String?, + @ColumnInfo("course_date_response_previous") + val previous: String?, + @ColumnInfo("course_date_response_results") + val results: List +) { + fun mapToDomain(): DomainCourseDatesResponse { + return DomainCourseDatesResponse( + count = count, + next = next, + previous = previous, + results = results + .mapNotNull { it.mapToDomain() } + .sortedBy { it.dueDate } + ) + } + + companion object { + fun createFrom(courseDatesResponse: CourseDatesResponse): CourseDatesResponseEntity { + with(courseDatesResponse) { + return CourseDatesResponseEntity( + id = 0, + count = count, + next = next, + previous = previous, + results = results.map { CourseDateDB.createFrom(it) } + ) + } + } + } +} + +data class CourseDateDB( @ColumnInfo("course_date_first_component_block_id") val firstComponentBlockId: String?, @ColumnInfo("course_date_courseId") @@ -42,10 +80,9 @@ data class CourseDateEntity( } companion object { - fun createFrom(courseDate: CourseDate): CourseDateEntity { + fun createFrom(courseDate: CourseDate): CourseDateDB { with(courseDate) { - return CourseDateEntity( - id = 0, + return CourseDateDB( courseId = courseId, firstComponentBlockId = firstComponentBlockId, dueDate = dueDate, diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index b49a806e6..68829efd2 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -4,6 +4,7 @@ import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import org.openedx.core.data.model.room.BlockDb +import org.openedx.core.data.model.room.CourseDateDB import org.openedx.core.data.model.room.GradingPolicyDb import org.openedx.core.data.model.room.SectionScoreDb import org.openedx.core.data.model.room.discovery.CourseDateBlockDb @@ -83,4 +84,16 @@ class CourseConverter { @TypeConverter fun toGradeRangeMap(value: String): Map = Gson().fromJson(value, object : TypeToken>() {}.type) + + @TypeConverter + fun fromListOfCourseDateDB(value: List): String { + val json = Gson().toJson(value) + return json.toString() + } + + @TypeConverter + fun toListOfCourseDateDB(value: String): List { + val type = genericType>() + return Gson().fromJson(value, type) + } } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index ac06ef7ba..952e041de 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,9 +31,6 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -APP_LEVEL_DATES: - ENABLED: true - FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false From 961df7616fd2228bf7af885b20bfc6048756e115 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 2 Apr 2025 12:52:13 +0300 Subject: [PATCH 12/28] fix: changes according code review --- .../core/data/model/CourseDatesResponse.kt | 4 +- .../model/room/CourseDatesResponseEntity.kt | 97 ------------------- dates/src/main/res/layout/fragment_dates.xml | 6 -- 3 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt delete mode 100644 dates/src/main/res/layout/fragment_dates.xml diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt index 28a1b28dd..c86500671 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt @@ -50,9 +50,7 @@ data class CourseDatesResponse( count = count, next = next, previous = previous, - results = results - .mapNotNull { it.mapToDomain() } - .sortedBy { it.dueDate } + results = results.mapNotNull { it.mapToDomain() } ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt deleted file mode 100644 index 5231a5604..000000000 --- a/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.openedx.core.data.model.room - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import org.openedx.core.data.model.CourseDate -import org.openedx.core.data.model.CourseDatesResponse -import org.openedx.core.utils.TimeUtils -import org.openedx.core.domain.model.CourseDate as DomainCourseDate -import org.openedx.core.domain.model.CourseDatesResponse as DomainCourseDatesResponse - -@Entity(tableName = "course_dates_response_table") -data class CourseDatesResponseEntity( - @PrimaryKey(autoGenerate = true) - @ColumnInfo("course_date_response_id") - val id: Int, - @ColumnInfo("course_date_response_count") - val count: Int, - @ColumnInfo("course_date_response_next") - val next: String?, - @ColumnInfo("course_date_response_previous") - val previous: String?, - @ColumnInfo("course_date_response_results") - val results: List -) { - fun mapToDomain(): DomainCourseDatesResponse { - return DomainCourseDatesResponse( - count = count, - next = next, - previous = previous, - results = results - .mapNotNull { it.mapToDomain() } - .sortedBy { it.dueDate } - ) - } - - companion object { - fun createFrom(courseDatesResponse: CourseDatesResponse): CourseDatesResponseEntity { - with(courseDatesResponse) { - return CourseDatesResponseEntity( - id = 0, - count = count, - next = next, - previous = previous, - results = results.map { CourseDateDB.createFrom(it) } - ) - } - } - } -} - -data class CourseDateDB( - @ColumnInfo("course_date_first_component_block_id") - val firstComponentBlockId: String?, - @ColumnInfo("course_date_courseId") - val courseId: String, - @ColumnInfo("course_date_dueDate") - val dueDate: String?, - @ColumnInfo("course_date_assignmentTitle") - val assignmentTitle: String?, - @ColumnInfo("course_date_learnerHasAccess") - val learnerHasAccess: Boolean?, - @ColumnInfo("course_date_relative") - val relative: Boolean?, - @ColumnInfo("course_date_courseName") - val courseName: String?, -) { - - fun mapToDomain(): DomainCourseDate? { - val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") - return DomainCourseDate( - courseId = courseId, - firstComponentBlockId = firstComponentBlockId ?: "", - dueDate = dueDate ?: return null, - assignmentTitle = assignmentTitle ?: "", - learnerHasAccess = learnerHasAccess ?: false, - relative = relative ?: false, - courseName = courseName ?: "" - ) - } - - companion object { - fun createFrom(courseDate: CourseDate): CourseDateDB { - with(courseDate) { - return CourseDateDB( - courseId = courseId, - firstComponentBlockId = firstComponentBlockId, - dueDate = dueDate, - assignmentTitle = assignmentTitle, - learnerHasAccess = learnerHasAccess, - relative = relative, - courseName = courseName - ) - } - } - } -} diff --git a/dates/src/main/res/layout/fragment_dates.xml b/dates/src/main/res/layout/fragment_dates.xml deleted file mode 100644 index 77d9ef65f..000000000 --- a/dates/src/main/res/layout/fragment_dates.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From 4f0b1cf205e523ef13b4f09e13da0def14f8c5b8 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 16 Apr 2025 11:14:08 +0300 Subject: [PATCH 13/28] feat: according designer feedback --- .../openedx/course/data/storage/CourseConverter.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index 68829efd2..b49a806e6 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -4,7 +4,6 @@ import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import org.openedx.core.data.model.room.BlockDb -import org.openedx.core.data.model.room.CourseDateDB import org.openedx.core.data.model.room.GradingPolicyDb import org.openedx.core.data.model.room.SectionScoreDb import org.openedx.core.data.model.room.discovery.CourseDateBlockDb @@ -84,16 +83,4 @@ class CourseConverter { @TypeConverter fun toGradeRangeMap(value: String): Map = Gson().fromJson(value, object : TypeToken>() {}.type) - - @TypeConverter - fun fromListOfCourseDateDB(value: List): String { - val json = Gson().toJson(value) - return json.toString() - } - - @TypeConverter - fun toListOfCourseDateDB(value: String): List { - val type = genericType>() - return Gson().fromJson(value, type) - } } From ddc1a984384117f610535cfe7d14d2c8893a90f0 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 4 Dec 2025 16:59:26 +0200 Subject: [PATCH 14/28] fix: empty state icon --- .../main/java/org/openedx/core/presentation/dates/DatesUI.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/openedx/core/presentation/dates/DatesUI.kt b/core/src/main/java/org/openedx/core/presentation/dates/DatesUI.kt index c57874865..2833998f9 100644 --- a/core/src/main/java/org/openedx/core/presentation/dates/DatesUI.kt +++ b/core/src/main/java/org/openedx/core/presentation/dates/DatesUI.kt @@ -36,6 +36,7 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.TimeUtils.formatToString import org.openedx.core.utils.clearTime import org.openedx.core.utils.isToday +import java.util.Date @Composable private fun CourseDateBlockSectionGeneric( @@ -262,7 +263,7 @@ private fun CourseDateItem( if (isMiddleChild) { Spacer(modifier = Modifier.height(20.dp)) } - if (!dateBlock.dueDate.isToday()) { + if (!dateBlock.dueDate.isToday() || dateBlock.dueDate < Date()) { val timeTitle = formatToString(context, dateBlock.dueDate, useRelativeDates) Text( text = timeTitle, From 1fa31de64a9b1e2636e1dc5f7a5e80212b1008cc Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 10 Dec 2025 22:18:00 +0200 Subject: [PATCH 15/28] chore: refactor uiMessage flow --- .../main/java/org/openedx/app/AppViewModel.kt | 6 +- .../java/org/openedx/app/MainViewModel.kt | 4 +- .../java/org/openedx/app/di/ScreenModule.kt | 216 +++++++++++------- .../test/java/org/openedx/AppViewModelTest.kt | 5 + .../logistration/LogistrationViewModel.kt | 4 +- .../restore/RestorePasswordFragment.kt | 3 +- .../restore/RestorePasswordViewModel.kt | 43 ++-- .../presentation/signin/SignInFragment.kt | 2 +- .../presentation/signin/SignInViewModel.kt | 51 +++-- .../presentation/signup/SignUpViewModel.kt | 40 +--- .../restore/RestorePasswordViewModelTest.kt | 40 ++-- .../signin/SignInViewModelTest.kt | 41 ++-- .../signup/SignUpViewModelTest.kt | 9 +- build.gradle | 2 +- .../module/download/BaseDownloadViewModel.kt | 4 +- .../SelectDialogViewModel.kt | 6 +- .../settings/video/VideoQualityViewModel.kt | 4 +- .../container/CourseContainerViewModel.kt | 23 +- .../contenttab/ContentTabViewModel.kt | 4 +- .../dates/CourseDatesViewModel.kt | 34 +-- .../handouts/HandoutsViewModel.kt | 4 +- .../presentation/home/CourseHomeViewModel.kt | 35 +-- .../offline/CourseOfflineViewModel.kt | 3 + .../outline/CourseContentAllViewModel.kt | 35 +-- .../progress/CourseProgressViewModel.kt | 12 +- .../section/CourseSectionFragment.kt | 3 +- .../section/CourseSectionViewModel.kt | 20 +- .../container/CourseUnitContainerViewModel.kt | 4 +- .../unit/html/HtmlUnitViewModel.kt | 6 +- .../unit/video/BaseVideoViewModel.kt | 4 +- .../unit/video/EncodedVideoUnitViewModel.kt | 5 +- .../unit/video/VideoUnitViewModel.kt | 4 +- .../presentation/unit/video/VideoViewModel.kt | 4 +- .../videos/CourseVideoViewModel.kt | 12 +- .../download/DownloadQueueViewModel.kt | 5 +- .../container/CourseContainerViewModelTest.kt | 9 +- .../dates/CourseDatesViewModelTest.kt | 13 +- .../handouts/HandoutsViewModelTest.kt | 35 ++- .../home/CourseHomeViewModelTest.kt | 6 +- .../outline/CourseOutlineViewModelTest.kt | 10 +- .../section/CourseSectionViewModelTest.kt | 24 +- .../CourseUnitContainerViewModelTest.kt | 32 ++- .../unit/video/VideoUnitViewModelTest.kt | 7 +- .../unit/video/VideoViewModelTest.kt | 29 ++- .../AllEnrolledCoursesViewModel.kt | 44 +--- .../presentation/DashboardGalleryViewModel.kt | 28 +-- .../presentation/DashboardListFragment.kt | 3 +- .../presentation/DashboardListViewModel.kt | 30 +-- .../learn/presentation/LearnViewModel.kt | 4 +- .../DashboardListViewModelTest.kt | 32 ++- .../presentation/LearnViewModelTest.kt | 50 +++- .../presentation/dates/DatesViewModel.kt | 28 +-- .../org/openedx/dates/DatesViewModelTest.kt | 7 +- .../presentation/NativeDiscoveryFragment.kt | 3 +- .../presentation/NativeDiscoveryViewModel.kt | 30 +-- .../presentation/WebViewDiscoveryViewModel.kt | 4 +- .../detail/CourseDetailsFragment.kt | 3 +- .../detail/CourseDetailsViewModel.kt | 34 +-- .../presentation/info/CourseInfoViewModel.kt | 15 +- .../presentation/program/ProgramViewModel.kt | 9 +- .../search/CourseSearchFragment.kt | 3 +- .../search/CourseSearchViewModel.kt | 20 +- .../NativeDiscoveryViewModelTest.kt | 42 ++-- .../detail/CourseDetailsViewModelTest.kt | 28 ++- .../search/CourseSearchViewModelTest.kt | 31 ++- .../comments/DiscussionCommentsFragment.kt | 3 +- .../comments/DiscussionCommentsViewModel.kt | 93 +++----- .../responses/DiscussionResponsesFragment.kt | 3 +- .../responses/DiscussionResponsesViewModel.kt | 68 ++---- .../search/DiscussionSearchThreadFragment.kt | 3 +- .../search/DiscussionSearchThreadViewModel.kt | 20 +- .../threads/DiscussionAddThreadFragment.kt | 3 +- .../threads/DiscussionAddThreadViewModel.kt | 25 +- .../threads/DiscussionThreadsFragment.kt | 3 +- .../threads/DiscussionThreadsViewModel.kt | 40 +--- .../topics/DiscussionTopicsViewModel.kt | 17 +- .../DiscussionCommentsViewModelTest.kt | 117 ++++++---- .../DiscussionResponsesViewModelTest.kt | 80 ++++--- .../DiscussionSearchThreadViewModelTest.kt | 28 ++- .../DiscussionAddThreadViewModelTest.kt | 20 +- .../threads/DiscussionThreadsViewModelTest.kt | 34 +-- .../topics/DiscussionTopicsViewModelTest.kt | 6 +- .../download/DownloadsViewModel.kt | 19 +- .../downloads/DownloadsViewModelTest.kt | 10 +- .../AnothersProfileFragment.kt | 3 +- .../AnothersProfileViewModel.kt | 19 +- .../calendar/CalendarViewModel.kt | 4 +- .../calendar/CoursesToSyncViewModel.kt | 36 +-- .../DisableCalendarSyncDialogViewModel.kt | 4 +- .../calendar/NewCalendarDialogFragment.kt | 11 +- .../calendar/NewCalendarDialogViewModel.kt | 31 ++- .../delete/DeleteProfileFragment.kt | 2 +- .../delete/DeleteProfileViewModel.kt | 18 +- .../presentation/edit/EditProfileFragment.kt | 3 +- .../presentation/edit/EditProfileViewModel.kt | 29 +-- .../manageaccount/ManageAccountViewModel.kt | 28 +-- .../presentation/profile/ProfileFragment.kt | 2 +- .../presentation/profile/ProfileViewModel.kt | 19 +- .../settings/SettingsViewModel.kt | 24 +- .../video/VideoSettingsViewModel.kt | 4 +- .../edit/EditProfileViewModelTest.kt | 33 +-- .../profile/AnothersProfileViewModelTest.kt | 22 +- .../profile/CalendarViewModelTest.kt | 14 +- .../profile/ProfileViewModelTest.kt | 26 ++- .../whatsnew/WhatsNewViewModel.kt | 4 +- .../openedx/whatsnew/WhatsNewViewModelTest.kt | 5 +- 106 files changed, 1077 insertions(+), 1166 deletions(-) diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index e195a7940..bafddb19b 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -29,6 +29,7 @@ import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.Directories import org.openedx.foundation.presentation.BaseViewModel import org.openedx.foundation.presentation.SingleEventLiveData +import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @SuppressLint("StaticFieldLeak") @@ -42,8 +43,9 @@ class AppViewModel( private val deepLinkRouter: DeepLinkRouter, private val fileUtil: FileUtil, private val downloadNotifier: DownloadNotifier, - private val context: Context -) : BaseViewModel() { + private val context: Context, + resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _logoutUser = SingleEventLiveData() val logoutUser: LiveData diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index 74f309e68..828b14a39 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -18,13 +18,15 @@ import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class MainViewModel( private val config: Config, private val notifier: DiscoveryNotifier, private val analytics: AppAnalytics, private val appNotifier: AppNotifier, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _isBottomBarEnabled = MutableLiveData(true) val isBottomBarEnabled: LiveData 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 f2d531918..45a4ecc25 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -1,6 +1,6 @@ package org.openedx.app.di -import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.openedx.app.AppViewModel @@ -87,19 +87,20 @@ val screenModule = module { viewModel { AppViewModel( - get(), - get(), - get(), - get(), - get(named("IODispatcher")), - get(), - get(), - get(), - get(), - get(), + config = get(), + appNotifier = get(), + room = get(), + preferencesManager = get(), + dispatcher = get(named("IODispatcher")), + analytics = get(), + deepLinkRouter = get(), + fileUtil = get(), + downloadNotifier = get(), + context = get(), + resourceManager = get(), ) } - viewModel { MainViewModel(get(), get(), get(), get()) } + viewModel { MainViewModel(get(), get(), get(), get(), get()) } factory { AuthRepository(get(), get(), get()) } factory { AuthInteractor(get()) } @@ -112,6 +113,7 @@ val screenModule = module { get(), get(), get(), + get(), ) } @@ -159,20 +161,30 @@ val screenModule = module { viewModel { DashboardListViewModel(get(), get(), get(), get(), get(), get()) } viewModel { (windowSize: WindowSize) -> DashboardGalleryViewModel( - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - windowSize + config = get(), + interactor = get(), + resourceManager = get(), + discoveryNotifier = get(), + networkConnection = get(), + fileUtil = get(), + dashboardRouter = get(), + corePreferences = get(), + windowSize = windowSize + ) + } + viewModel { + AllEnrolledCoursesViewModel( + config = get(), + networkConnection = get(), + interactor = get(), + resourceManager = get(), + discoveryNotifier = get(), + analytics = get(), + dashboardRouter = get(), ) } - viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { (openTab: String) -> - LearnViewModel(openTab, get(), get(), get()) + LearnViewModel(openTab, get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } @@ -180,13 +192,14 @@ val screenModule = module { viewModel { NativeDiscoveryViewModel(get(), get(), get(), get(), get(), get()) } viewModel { (querySearch: String) -> WebViewDiscoveryViewModel( - querySearch, - get(), - get(), - get(), - get(), - get(), - get(), + querySearch = querySearch, + appData = get(), + config = get(), + networkConnection = get(), + corePreferences = get(), + router = get(), + analytics = get(), + resourceManager = get(), ) } @@ -211,8 +224,16 @@ val screenModule = module { account ) } - viewModel { VideoSettingsViewModel(get(), get(), get(), get()) } - viewModel { (qualityType: String) -> VideoQualityViewModel(qualityType, get(), get(), get()) } + viewModel { VideoSettingsViewModel(get(), get(), get(), get(), get()) } + viewModel { (qualityType: String) -> + VideoQualityViewModel( + qualityType, + get(), + get(), + get(), + get() + ) + } viewModel { DeleteProfileViewModel(get(), get(), get(), get(), get()) } viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) } viewModel { @@ -230,11 +251,19 @@ val screenModule = module { get(), ) } - viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { + ManageAccountViewModel( + interactor = get(), + resourceManager = get(), + notifier = get(), + analytics = get(), + profileRouter = get(), + ) + } + viewModel { CalendarViewModel(get(), 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()) } + viewModel { DisableCalendarSyncDialogViewModel(get(), get(), get(), get(), get()) } factory { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } @@ -312,6 +341,7 @@ val screenModule = module { courseId, courseTitle, get(), + get(), ) } viewModel { (courseId: String, courseTitle: String) -> @@ -355,30 +385,31 @@ val screenModule = module { get(), get(), get(), + get(), ) } viewModel { (courseId: String) -> CourseVideoViewModel( - courseId, - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), + courseId = courseId, + config = get(), + interactor = get(), + resourceManager = get(), + networkConnection = get(), + preferencesManager = get(), + courseNotifier = get(), + downloadDialogManager = get(), + fileUtil = get(), + courseRouter = get(), + analytics = get(), + videoPreviewHelper = get(), + coreAnalytics = get(), + downloadDao = get(), + workerController = get(), + downloadHelper = get(), ) } - viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) } - viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get(), get()) } + viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get(), get()) } + viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get(), get(), get()) } viewModel { (courseId: String, videoUrl: String, blockId: String) -> VideoUnitViewModel( courseId, @@ -388,7 +419,8 @@ val screenModule = module { get(), get(), get(), - get() + get(), + get(), ) } viewModel { (courseId: String, videoUrl: String, blockId: String) -> @@ -403,35 +435,37 @@ val screenModule = module { get(), get(), get(), + get() ) } viewModel { (courseId: String, enrollmentMode: String) -> CourseDatesViewModel( - courseId, - enrollmentMode, - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), - get(), + courseId = courseId, + enrollmentMode = enrollmentMode, + courseNotifier = get(), + interactor = get(), + courseAnalytics = get(), + config = get(), + calendarInteractor = get(), + calendarNotifier = get(), + corePreferences = get(), + courseRouter = get(), + calendarRouter = get(), + resourceManager = get(), ) } viewModel { (courseId: String, handoutsType: String) -> HandoutsViewModel( courseId, handoutsType, - get(), - get(), - get(), + config = get(), + interactor = get(), + courseAnalytics = get(), + resourceManager = get(), ) } viewModel { CourseSearchViewModel(get(), get(), get(), get(), get()) } - viewModel { SelectDialogViewModel(get()) } + viewModel { SelectDialogViewModel(get(), get()) } single { DiscussionRepository(get(), get(), get()) } factory { DiscussionInteractor(get()) } @@ -439,11 +473,11 @@ val screenModule = module { DiscussionTopicsViewModel( courseId, courseTitle, - get(), - get(), - get(), - get(), - get() + interactor = get(), + resourceManager = get(), + analytics = get(), + courseNotifier = get(), + discussionRouter = get(), ) } viewModel { (courseId: String, topicId: String, threadType: String) -> @@ -491,6 +525,7 @@ val screenModule = module { get(), get(), get(), + get(), ) } @@ -503,6 +538,7 @@ val screenModule = module { get(), get(), get(), + get() ) } viewModel { (blockId: String, courseId: String) -> @@ -515,10 +551,22 @@ val screenModule = module { get(), get(), get(), + get() ) } - viewModel { ProgramViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { + ProgramViewModel( + appData = get(), + config = get(), + networkConnection = get(), + router = get(), + notifier = get(), + edxCookieManager = get(), + resourceManager = get(), + interactor = get(), + ) + } viewModel { (courseId: String, courseTitle: String) -> CourseOfflineViewModel( @@ -533,6 +581,7 @@ val screenModule = module { get(), get(), get(), + get(), get() ) } @@ -540,7 +589,8 @@ val screenModule = module { CourseProgressViewModel( courseId, get(), - get() + get(), + get(), ) } @@ -562,19 +612,19 @@ val screenModule = module { downloadsRouter = get(), networkConnection = get(), interactor = get(), + downloadDialogManager = get(), resourceManager = get(), + fileUtil = get(), config = get(), + analytics = get(), + discoveryNotifier = get(), + courseNotifier = get(), + router = get(), preferencesManager = get(), coreAnalytics = get(), downloadDao = get(), workerController = get(), downloadHelper = get(), - downloadDialogManager = get(), - fileUtil = get(), - analytics = get(), - discoveryNotifier = get(), - courseNotifier = get(), - router = get() ) } viewModel { (courseId: String) -> diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index 0271aace3..0dca83a93 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -32,6 +32,7 @@ import org.openedx.core.config.FirebaseConfig import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.LogoutEvent +import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @ExperimentalCoroutinesApi @@ -51,6 +52,7 @@ class AppViewModelTest { private val deepLinkRouter = mockk() private val context = mockk() private val downloadNotifier = mockk() + private val resourceManager = mockk() @Before fun before() { @@ -82,6 +84,7 @@ class AppViewModelTest { fileUtil, downloadNotifier, context, + resourceManager, ) val mockLifeCycleOwner: LifecycleOwner = mockk() @@ -118,6 +121,7 @@ class AppViewModelTest { fileUtil, downloadNotifier, context, + resourceManager, ) val mockLifeCycleOwner: LifecycleOwner = mockk() @@ -156,6 +160,7 @@ class AppViewModelTest { fileUtil, downloadNotifier, context, + resourceManager, ) val mockLifeCycleOwner: LifecycleOwner = mockk() diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt index d7ca6e894..0ea8a2f91 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt @@ -13,6 +13,7 @@ import org.openedx.core.config.Config import org.openedx.core.utils.Logger import org.openedx.foundation.extension.takeIfNotEmpty import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class LogistrationViewModel( private val courseId: String, @@ -20,7 +21,8 @@ class LogistrationViewModel( private val config: Config, private val analytics: AuthAnalytics, private val browserAuthHelper: BrowserAuthHelper, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val logger = Logger("LogistrationViewModel") diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt index 81d216c39..adb8da725 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt @@ -28,6 +28,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -90,7 +91,7 @@ class RestorePasswordFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(RestorePasswordUIState.Initial) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val appUpgradeEvent by viewModel.appUpgradeEventUIState.observeAsState(null) if (appUpgradeEvent == null) { diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt index 6c5e3adf1..53e7be439 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt @@ -4,18 +4,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch +import org.openedx.auth.R import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthAnalyticsEvent import org.openedx.auth.presentation.AuthAnalyticsKey -import org.openedx.core.R import org.openedx.core.system.EdxError import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.foundation.extension.isEmailValid -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager @@ -24,16 +22,12 @@ class RestorePasswordViewModel( private val resourceManager: ResourceManager, private val analytics: AuthAnalytics, private val appNotifier: AppNotifier -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _appUpgradeEvent = MutableLiveData() val appUpgradeEventUIState: LiveData get() = _appUpgradeEvent @@ -53,33 +47,30 @@ class RestorePasswordViewModel( logResetPasswordEvent(true) } else { _uiState.value = RestorePasswordUIState.Initial - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + handleErrorUiMessage( + throwable = null, + ) logResetPasswordEvent(false) } } else { _uiState.value = RestorePasswordUIState.Initial - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email) - ) + handleErrorUiMessage( + throwable = null, + defaultErrorRes = R.string.auth_invalid_email, + ) logResetPasswordEvent(false) } } catch (e: Exception) { _uiState.value = RestorePasswordUIState.Initial logResetPasswordEvent(false) - if (e is EdxError.ValidationException) { - _uiMessage.value = UIMessage.SnackBarMessage(e.error) - } else if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) + when (e) { + is EdxError.ValidationException -> sendMessage( + UIMessage.SnackBarMessage(e.error) + ) + + else -> handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt index e5da6fbd9..e271b9044 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt @@ -40,7 +40,7 @@ class SignInFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() val state by viewModel.uiState.collectAsState() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) if (appUpgradeEvent == null) { diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index f271927e1..11cfa2b67 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -35,10 +35,7 @@ import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.Logger -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.core.R as CoreRes @@ -60,7 +57,7 @@ class SignInViewModel( val courseId: String?, val infoType: String?, val authCode: String, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val logger = Logger("SignInViewModel") @@ -79,10 +76,6 @@ class SignInViewModel( ) internal val uiState: StateFlow = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _appUpgradeEvent = MutableLiveData() val appUpgradeEvent: LiveData get() = _appUpgradeEvent @@ -95,13 +88,21 @@ class SignInViewModel( fun login(username: String, password: String) { logEvent(AuthAnalyticsEvent.USER_SIGN_IN_CLICKED) if (!validator.isEmailOrUserNameValid(username)) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email_username)) + viewModelScope.launch { + handleErrorUiMessage( + throwable = null, + defaultErrorRes = R.string.auth_invalid_email_username, + ) + } return } if (!validator.isPasswordValid(password)) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_password)) + viewModelScope.launch { + handleErrorUiMessage( + throwable = null, + defaultErrorRes = R.string.auth_invalid_password, + ) + } return } @@ -126,15 +127,15 @@ class SignInViewModel( ) appNotifier.send(SignInEvent()) } catch (e: Exception) { - if (e is EdxError.InvalidGrantException) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_invalid_grant)) - } else if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_unknown_error)) + when (e) { + is EdxError.InvalidGrantException -> handleErrorUiMessage( + throwable = null, + defaultErrorRes = CoreRes.string.core_error_invalid_grant, + ) + + else -> handleErrorUiMessage( + throwable = e, + ) } } _uiState.update { it.copy(showProgress = false) } @@ -228,9 +229,11 @@ class SignInViewModel( message?.let { logger.e { it() } } - _uiMessage.value = UIMessage.SnackBarMessage( - resourceManager.getString(CoreRes.string.core_error_unknown_error) - ) + viewModelScope.launch { + handleErrorUiMessage( + throwable = null, + ) + } _uiState.update { it.copy(showProgress = false) } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index 21e12029e..07987c90c 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -4,10 +4,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -31,11 +28,8 @@ import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.Logger -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager -import org.openedx.core.R as coreR class SignUpViewModel( private val interactor: AuthInteractor, @@ -49,7 +43,7 @@ class SignUpViewModel( private val router: AuthRouter, val courseId: String?, val infoType: String?, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val logger = Logger("SignUpViewModel") @@ -64,13 +58,6 @@ class SignUpViewModel( ) val uiState = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - val uiMessage = _uiMessage.asSharedFlow() - init { collectAppUpgradeEvent() logRegisterScreenEvent() @@ -82,19 +69,9 @@ class SignUpViewModel( try { updateFields(interactor.getRegistrationFields()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(coreR.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(coreR.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { _uiState.update { state -> state.copy(isLoading = false) @@ -212,12 +189,9 @@ class SignUpViewModel( private suspend fun handleRegistrationError(e: Exception) { _uiState.update { it.copy(isButtonLoading = false) } - val errorMessage = if (e.isInternetError()) { - coreR.string.core_error_no_connection - } else { - coreR.string.core_error_unknown_error - } - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) + handleErrorUiMessage( + throwable = e, + ) } fun socialAuth(fragment: Fragment, authType: AuthType) { diff --git a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt index 4e780121d..3dcb4aa18 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt @@ -22,12 +22,13 @@ import org.junit.Test import org.junit.rules.TestRule import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics -import org.openedx.core.R import org.openedx.core.system.EdxError import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class RestorePasswordViewModelTest { @@ -56,8 +57,12 @@ class RestorePasswordViewModelTest { @Before fun before() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email) } returns invalidEmail every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_password) } returns invalidPassword every { appNotifier.notifier } returns emptyFlow() @@ -80,10 +85,10 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals(invalidEmail, message?.message) + assertEquals(invalidEmail, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -98,10 +103,10 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals(invalidEmail, message?.message) + assertEquals(invalidEmail, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -116,10 +121,9 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals("error", message?.message) + assertEquals("error", (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -134,10 +138,10 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals(noInternet, message?.message) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -152,10 +156,10 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals(somethingWrong, message?.message) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -170,10 +174,10 @@ class RestorePasswordViewModelTest { verify(exactly = 2) { analytics.logEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Initial) - assertEquals(somethingWrong, message?.message) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -189,10 +193,10 @@ class RestorePasswordViewModelTest { verify(exactly = 1) { appNotifier.notifier } val state = viewModel.uiState.value as? RestorePasswordUIState.Success - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assertEquals(correctEmail, state?.email) assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Success) - assertEquals(null, message) + assertEquals(null, message.await()) } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index dee9bde38..b91f8774f 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -43,9 +43,11 @@ import org.openedx.core.system.EdxError import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException import org.openedx.core.R as CoreRes +import org.openedx.foundation.R as foundationR @ExperimentalCoroutinesApi class SignInViewModelTest { @@ -80,8 +82,12 @@ class SignInViewModelTest { fun before() { Dispatchers.setMain(dispatcher) every { resourceManager.getString(CoreRes.string.core_error_invalid_grant) } returns invalidCredential - every { resourceManager.getString(CoreRes.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(CoreRes.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { resourceManager.getString(R.string.auth_invalid_email_username) } returns invalidEmailOrUsername every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword every { appNotifier.notifier } returns emptyFlow() @@ -137,9 +143,9 @@ class SignInViewModelTest { verify(exactly = 1) { analytics.logEvent(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) val uiState = viewModel.uiState.value - assertEquals(invalidEmailOrUsername, message.message) + assertEquals(invalidEmailOrUsername, (message.await() as UIMessage.SnackBarMessage).message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @@ -173,9 +179,9 @@ class SignInViewModelTest { coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) val uiState = viewModel.uiState.value - assertEquals(invalidEmailOrUsername, message.message) + assertEquals(invalidEmailOrUsername, (message.await() as UIMessage.SnackBarMessage).message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @@ -211,9 +217,9 @@ class SignInViewModelTest { verify(exactly = 0) { analytics.setUserIdForSession(any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) val uiState = viewModel.uiState.value - assertEquals(invalidPassword, message.message) + assertEquals(invalidPassword, (message.await() as UIMessage.SnackBarMessage).message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @@ -251,9 +257,9 @@ class SignInViewModelTest { verify(exactly = 1) { analytics.logEvent(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) + assertEquals(invalidPassword, (message.await() as? UIMessage.SnackBarMessage)?.message) val uiState = viewModel.uiState.value - assertEquals(invalidPassword, message.message) assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) } @@ -297,7 +303,8 @@ class SignInViewModelTest { val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) assert(uiState.loginSuccess) - assertEquals(null, viewModel.uiMessage.value) + val message = captureUiMessage(viewModel) + assertEquals(null, message.await()) } @Test @@ -336,11 +343,11 @@ class SignInViewModelTest { verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) - assertEquals(noInternet, message?.message) } @Test @@ -379,11 +386,11 @@ class SignInViewModelTest { verify(exactly = 1) { analytics.logEvent(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) + assertEquals(invalidCredential, (message.await() as? UIMessage.SnackBarMessage)?.message) val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) - assertEquals(invalidCredential, message.message) } @Test @@ -422,10 +429,10 @@ class SignInViewModelTest { verify(exactly = 1) { analytics.logEvent(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) assertFalse(uiState.loginSuccess) - assertEquals(somethingWrong, message.message) } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index 933c57234..6a6a4b2a2 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -46,6 +46,7 @@ import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @ExperimentalCoroutinesApi class SignUpViewModelTest { @@ -107,8 +108,12 @@ class SignUpViewModelTest { fun before() { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_invalid_grant) } returns "Invalid credentials" - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { appNotifier.notifier } returns emptyFlow() every { agreementProvider.getAgreement(false) } returns null every { config.isSocialAuthEnabled() } returns false diff --git a/build.gradle b/build.gradle index 674a1057f..8570eb77d 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ buildscript { play_services_ads_identifier_version = '18.2.0' install_referrer_version = '2.2' snakeyaml_version = '2.4' - openedx_foundation_version = '1.0.2' + openedx_foundation_version = '1.1.0' openedx_firebase_analytics_version = '1.0.1' braze_sdk_version = '37.0.0' diff --git a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt index ba87e6ab0..0180a4845 100644 --- a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt +++ b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt @@ -17,6 +17,7 @@ import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.CoreAnalyticsEvent import org.openedx.core.presentation.CoreAnalyticsKey import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager abstract class BaseDownloadViewModel( private val downloadDao: DownloadDao, @@ -24,7 +25,8 @@ abstract class BaseDownloadViewModel( private val workerController: DownloadWorkerController, private val analytics: CoreAnalytics, private val downloadHelper: DownloadHelper, -) : BaseViewModel() { + resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { val allBlocks = hashMapOf() diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt index f215974ce..db17aa625 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt @@ -6,10 +6,12 @@ import org.openedx.core.domain.model.RegistrationField import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseSubtitleLanguageChanged import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class SelectDialogViewModel( - private val notifier: CourseNotifier -) : BaseViewModel() { + private val notifier: CourseNotifier, + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { var values = mutableListOf() diff --git a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt index 95ecca130..05fc6077e 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt @@ -12,13 +12,15 @@ import org.openedx.core.presentation.CoreAnalyticsKey import org.openedx.core.system.notifier.VideoNotifier import org.openedx.core.system.notifier.VideoQualityChanged import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class VideoQualityViewModel( private val qualityType: String, private val preferencesManager: CorePreferences, private val notifier: VideoNotifier, private val analytics: CoreAnalytics, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _videoQuality = MutableLiveData() val videoQuality: LiveData diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index ff9643bd4..98501ae1e 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -8,11 +8,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -54,7 +51,6 @@ import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.extension.toImageLink import org.openedx.foundation.presentation.BaseViewModel import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.util.concurrent.atomic.AtomicReference import org.openedx.core.R as CoreR @@ -73,7 +69,7 @@ class CourseContainerViewModel( private val imageProcessor: ImageProcessor, private val calendarSyncScheduler: CalendarSyncScheduler, val courseRouter: CourseRouter, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _dataReady = MutableLiveData() val dataReady: LiveData @@ -99,10 +95,6 @@ class CourseContainerViewModel( val isNavigationEnabled: StateFlow = _isNavigationEnabled.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private var _courseDetails: CourseEnrollmentDetails? = null val courseDetails: CourseEnrollmentDetails? get() = _courseDetails @@ -150,7 +142,7 @@ class CourseContainerViewModel( is CourseDatesShifted -> { calendarSyncScheduler.requestImmediateSync(courseId) - _uiMessage.emit(DatesShiftedSnackBar()) + sendMessage(DatesShiftedSnackBar()) } is CourseLoading -> { @@ -249,7 +241,9 @@ class CourseContainerViewModel( private fun handleFetchError(e: Throwable) { e.printStackTrace() if (isNetworkRelatedError(e)) { - _errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection) + _errorMessage.value = resolveErrorMessage( + throwable = e, + ) } else { _courseAccessStatus.value = CourseAccessError.UNKNOWN } @@ -320,9 +314,10 @@ class CourseContainerViewModel( viewModelScope.launch { try { interactor.getCourseStructure(courseId, isNeedRefresh = true) - } catch (_: Exception) { - _errorMessage.value = - resourceManager.getString(CoreR.string.core_error_unknown_error) + } catch (e: Exception) { + _errorMessage.value = resolveErrorMessage( + throwable = e, + ) } _refreshing.value = false courseNotifier.send(CourseStructureUpdated(courseId)) diff --git a/course/src/main/java/org/openedx/course/presentation/contenttab/ContentTabViewModel.kt b/course/src/main/java/org/openedx/course/presentation/contenttab/ContentTabViewModel.kt index 7aebe86f3..3ec98e6bf 100644 --- a/course/src/main/java/org/openedx/course/presentation/contenttab/ContentTabViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/contenttab/ContentTabViewModel.kt @@ -5,12 +5,14 @@ import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.container.CourseContentTab import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class ContentTabViewModel( val courseId: String, private val courseTitle: String, private val analytics: CourseAnalytics, -) : BaseViewModel() { + resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { fun logTabClickEvent(contentTab: CourseContentTab) { analytics.logEvent( 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 91b5c6ee5..c059d1e73 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 @@ -1,11 +1,8 @@ package org.openedx.course.presentation.dates import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -35,24 +32,22 @@ import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.CourseRouter import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager -import org.openedx.core.R as CoreR class CourseDatesViewModel( val courseId: String, private val enrollmentMode: String, private val courseNotifier: CourseNotifier, private val interactor: CourseInteractor, - private val resourceManager: ResourceManager, private val courseAnalytics: CourseAnalytics, private val config: Config, private val calendarInteractor: CalendarInteractor, private val calendarNotifier: CalendarNotifier, private val corePreferences: CorePreferences, val courseRouter: CourseRouter, - val calendarRouter: CalendarRouter -) : BaseViewModel() { + val calendarRouter: CalendarRouter, + resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { var isSelfPaced = true var useRelativeDates = corePreferences.isRelativeDatesEnabled @@ -61,10 +56,6 @@ class CourseDatesViewModel( val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private var courseBannerType: CourseBannerType = CourseBannerType.BLANK private var courseStructure: CourseStructure? = null @@ -112,8 +103,8 @@ class CourseDatesViewModel( } catch (e: Exception) { _uiState.value = CourseDatesUIState.Error if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection)) + handleErrorUiMessage( + throwable = e, ) } } finally { @@ -130,17 +121,10 @@ class CourseDatesViewModel( courseNotifier.send(CourseDatesShifted) onResetDates(true) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection)) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg) - ) - ) - } + handleErrorUiMessage( + throwable = e, + defaultErrorRes = R.string.core_dates_shift_dates_unsuccessful_msg, + ) onResetDates(false) } } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index 85fad2512..38ea669bc 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -13,6 +13,7 @@ import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class HandoutsViewModel( private val courseId: String, @@ -20,7 +21,8 @@ class HandoutsViewModel( private val config: Config, private val interactor: CourseInteractor, private val courseAnalytics: CourseAnalytics, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() diff --git a/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt b/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt index 7d1381505..bd72bc19e 100644 --- a/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt @@ -45,7 +45,6 @@ import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.unit.container.CourseViewMode -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @@ -74,7 +73,8 @@ class CourseHomeViewModel( preferencesManager, workerController, coreAnalytics, - downloadHelper + downloadHelper, + resourceManager, ) { val isCourseDropdownNavigationEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled @@ -82,10 +82,6 @@ class CourseHomeViewModel( val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val _resumeBlockId = MutableSharedFlow() val resumeBlockId: SharedFlow get() = _resumeBlockId.asSharedFlow() @@ -155,7 +151,7 @@ class CourseHomeViewModel( super.saveDownloadModels(folder, courseId, id) } else { viewModelScope.launch { - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage( resourceManager.getString(courseR.string.course_can_download_only_with_wifi) ) @@ -282,11 +278,9 @@ class CourseHomeViewModel( private suspend fun handleCourseDataError(e: Throwable?) { _uiState.value = CourseHomeUIState.Error - val errorMessage = when { - e?.isInternetError() == true -> R.string.core_error_no_connection - else -> R.string.core_error_unknown_error - } - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) + handleErrorUiMessage( + throwable = e, + ) } private fun sortBlocks(blocks: List): List { @@ -379,19 +373,10 @@ class CourseHomeViewModel( courseNotifier.send(CourseDatesShifted) onResetDates(true) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg) - ) - ) - } + handleErrorUiMessage( + throwable = e, + defaultErrorRes = R.string.core_dates_shift_dates_unsuccessful_msg, + ) onResetDates(false) } } diff --git a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt index 58fd12af6..311841c91 100644 --- a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt @@ -28,6 +28,7 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureGot import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil class CourseOfflineViewModel( @@ -39,6 +40,7 @@ class CourseOfflineViewModel( private val fileUtil: FileUtil, private val networkConnection: NetworkConnection, private val courseNotifier: CourseNotifier, + private val resourceManager: ResourceManager, coreAnalytics: CoreAnalytics, downloadDao: DownloadDao, workerController: DownloadWorkerController, @@ -49,6 +51,7 @@ class CourseOfflineViewModel( workerController, coreAnalytics, downloadHelper, + resourceManager, ) { private val _uiState = MutableStateFlow( CourseOfflineUIState( diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt index 13ab7251b..a30cde02f 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt @@ -41,7 +41,6 @@ import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.unit.container.CourseViewMode -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @@ -69,7 +68,8 @@ class CourseContentAllViewModel( preferencesManager, workerController, coreAnalytics, - downloadHelper + downloadHelper, + resourceManager, ) { val isCourseDropdownNavigationEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled @@ -78,10 +78,6 @@ class CourseContentAllViewModel( val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val _resumeBlockId = MutableSharedFlow() val resumeBlockId: SharedFlow get() = _resumeBlockId.asSharedFlow() @@ -138,7 +134,7 @@ class CourseContentAllViewModel( super.saveDownloadModels(folder, courseId, id) } else { viewModelScope.launch { - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage( resourceManager.getString(courseR.string.course_can_download_only_with_wifi) ) @@ -236,11 +232,9 @@ class CourseContentAllViewModel( private suspend fun handleCourseDataError(e: Throwable?) { _uiState.value = CourseContentAllUIState.Error - val errorMessage = when { - e?.isInternetError() == true -> R.string.core_error_no_connection - else -> R.string.core_error_unknown_error - } - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) + handleErrorUiMessage( + throwable = e, + ) } private fun sortBlocks(blocks: List): List { @@ -291,19 +285,10 @@ class CourseContentAllViewModel( getCourseData() courseNotifier.send(CourseDatesShifted) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg) - ) - ) - } + handleErrorUiMessage( + throwable = e, + defaultErrorRes = R.string.core_dates_shift_dates_unsuccessful_msg, + ) } } } diff --git a/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt b/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt index 805f486d1..395ad82f9 100644 --- a/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt @@ -1,11 +1,8 @@ package org.openedx.course.presentation.progress import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -17,22 +14,19 @@ import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.RefreshProgress import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.system.ResourceManager class CourseProgressViewModel( val courseId: String, private val interactor: CourseInteractor, private val courseNotifier: CourseNotifier, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow(CourseProgressUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - init { collectData(false) collectCourseNotifier() diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 7bfe8a24c..f5c7daec4 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -31,6 +31,7 @@ import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -102,7 +103,7 @@ class CourseSectionFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(CourseSectionUIState.Loading) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) CourseSectionScreen( windowSize = windowSize, uiState = uiState, diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt index 8966ee45e..6e88a28fa 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.core.BlockType -import org.openedx.core.R import org.openedx.core.domain.model.Block import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseSectionChanged @@ -15,10 +14,7 @@ import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.unit.container.CourseViewMode -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class CourseSectionViewModel( @@ -27,16 +23,12 @@ class CourseSectionViewModel( private val resourceManager: ResourceManager, private val notifier: CourseNotifier, private val analytics: CourseAnalytics, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData(CourseSectionUIState.Loading) val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - var mode = CourseViewMode.FULL override fun onCreate(owner: LifecycleOwner) { @@ -68,13 +60,9 @@ class CourseSectionViewModel( sectionName = sequentialBlock.displayName ) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 81382f9f3..7ae122b48 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -29,6 +29,7 @@ import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.foundation.extension.clearAndAddAll import org.openedx.foundation.extension.indexOfFirstFromIndex import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class CourseUnitContainerViewModel( val courseId: String, @@ -40,7 +41,8 @@ class CourseUnitContainerViewModel( private val analytics: CourseAnalytics, private val networkConnection: NetworkConnection, private val videoPreviewHelper: VideoPreviewHelper, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val blocks = ArrayList() diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt index 702082746..2e18ccf5c 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt @@ -16,6 +16,7 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.worker.OfflineProgressSyncScheduler import org.openedx.foundation.extension.readAsText import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class HtmlUnitViewModel( private val blockId: String, @@ -25,8 +26,9 @@ class HtmlUnitViewModel( private val networkConnection: NetworkConnection, private val notifier: CourseNotifier, private val courseInteractor: CourseInteractor, - private val offlineProgressSyncScheduler: OfflineProgressSyncScheduler -) : BaseViewModel() { + private val offlineProgressSyncScheduler: OfflineProgressSyncScheduler, + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow(HtmlUnitUIState.Initialization) val uiState = _uiState.asStateFlow() diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt index 7c67329e6..5344d8787 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt @@ -4,11 +4,13 @@ import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager open class BaseVideoViewModel( private val courseId: String, private val courseAnalytics: CourseAnalytics, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { fun logVideoSpeedEvent(videoUrl: String, speed: Float, currentVideoTime: Long, medium: String) { logVideoEvent( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt index 2c2816bc9..87ce7fd2b 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt @@ -30,6 +30,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsKey +import org.openedx.foundation.system.ResourceManager import java.util.concurrent.Executors @SuppressLint("StaticFieldLeak") @@ -44,6 +45,7 @@ class EncodedVideoUnitViewModel( networkConnection: NetworkConnection, transcriptManager: TranscriptManager, courseAnalytics: CourseAnalytics, + resourceManager: ResourceManager, ) : VideoUnitViewModel( courseId, videoUrl, @@ -52,7 +54,8 @@ class EncodedVideoUnitViewModel( notifier, networkConnection, transcriptManager, - courseAnalytics + courseAnalytics, + resourceManager, ) { private val _isVideoEnded = MutableLiveData(false) diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt index bd9199942..e1f2ac93f 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt @@ -17,6 +17,7 @@ import org.openedx.core.system.notifier.CourseSubtitleLanguageChanged import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics +import org.openedx.foundation.system.ResourceManager import subtitleFile.TimedTextObject open class VideoUnitViewModel( @@ -28,7 +29,8 @@ open class VideoUnitViewModel( private val networkConnection: NetworkConnection, private val transcriptManager: TranscriptManager, courseAnalytics: CourseAnalytics, -) : BaseVideoViewModel(courseId, courseAnalytics) { + resourceManager: ResourceManager, +) : BaseVideoViewModel(courseId, courseAnalytics, resourceManager) { var transcripts = emptyMap() var isPlaying = true diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt index c9da7aaec..c0aa13723 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt @@ -9,6 +9,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics +import org.openedx.foundation.system.ResourceManager class VideoViewModel( private val courseId: String, @@ -16,7 +17,8 @@ class VideoViewModel( private val notifier: CourseNotifier, private val preferencesManager: CorePreferences, courseAnalytics: CourseAnalytics, -) : BaseVideoViewModel(courseId, courseAnalytics) { + resourceManager: ResourceManager, +) : BaseVideoViewModel(courseId, courseAnalytics, resourceManager) { var videoUrl = "" var currentVideoTime = 0L 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 456669cc0..b428404f5 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 @@ -3,11 +3,8 @@ package org.openedx.course.presentation.videos import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BlockType @@ -59,15 +56,12 @@ class CourseVideoViewModel( workerController, coreAnalytics, downloadHelper, + resourceManager, ) { private val _uiState = MutableStateFlow(CourseVideoUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val courseVideos = mutableMapOf>() private val courseSubSections = mutableMapOf>() private val subSectionsDownloadsCount = mutableMapOf() @@ -107,7 +101,7 @@ class CourseVideoViewModel( super.saveDownloadModels(folder, courseId, id) } else { viewModelScope.launch { - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage( resourceManager.getString(R.string.course_can_download_only_with_wifi) ) @@ -122,7 +116,7 @@ class CourseVideoViewModel( override fun saveAllDownloadModels(folder: String, courseId: String) { if (preferencesManager.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected()) { viewModelScope.launch { - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage(resourceManager.getString(R.string.course_can_download_only_with_wifi)) ) } diff --git a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt index 67e161378..c97455df5 100644 --- a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt +++ b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt @@ -12,6 +12,7 @@ import org.openedx.core.module.download.DownloadHelper import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.DownloadProgressChanged +import org.openedx.foundation.system.ResourceManager class DownloadQueueViewModel( private val descendants: List, @@ -21,12 +22,14 @@ class DownloadQueueViewModel( private val downloadNotifier: DownloadNotifier, coreAnalytics: CoreAnalytics, downloadHelper: DownloadHelper, + private val resourceManager: ResourceManager, ) : BaseDownloadViewModel( downloadDao, preferencesManager, workerController, coreAnalytics, - downloadHelper + downloadHelper, + resourceManager, ) { private val _uiState = MutableStateFlow(DownloadQueueUIState.Loading) diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index c64ce59a3..ba23dc140 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -40,6 +40,7 @@ import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseRouter import org.openedx.course.utils.ImageProcessor import org.openedx.foundation.system.ResourceManager +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseContainerViewModelTest { @@ -70,8 +71,12 @@ class CourseContainerViewModelTest { fun setUp() { Dispatchers.setMain(dispatcher) every { resourceManager.getString(id = R.string.platform_name) } returns openEdx - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { corePreferences.user } returns CoreMocks.mockUser every { corePreferences.appConfig } returns CoreMocks.mockAppConfig every { courseNotifier.notifier } returns emptyFlow() 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 ca9b996a3..f3ca889db 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 @@ -44,6 +44,7 @@ import org.openedx.course.presentation.CourseRouter import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseDatesViewModelTest { @@ -72,8 +73,8 @@ class CourseDatesViewModelTest { fun setUp() { Dispatchers.setMain(dispatcher) every { resourceManager.getString(id = R.string.platform_name) } returns openEdx - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { resourceManager.getString(foundationR.string.foundation_error_no_connection) } returns noInternet + every { resourceManager.getString(foundationR.string.foundation_error_unknown_error) } returns somethingWrong coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure every { corePreferences.user } returns CoreMocks.mockUser every { corePreferences.appConfig } returns CoreMocks.mockAppConfig @@ -102,7 +103,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - resourceManager, analytics, config, calendarInteractor, @@ -110,6 +110,7 @@ class CourseDatesViewModelTest { preferencesManager, courseRouter, calendarRouter, + resourceManager, ) coEvery { interactor.getCourseDates(any()) } throws UnknownHostException() val message = async { @@ -132,7 +133,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - resourceManager, analytics, config, calendarInteractor, @@ -140,6 +140,7 @@ class CourseDatesViewModelTest { preferencesManager, courseRouter, calendarRouter, + resourceManager, ) coEvery { interactor.getCourseDates(any()) } throws Exception() val message = async { @@ -162,7 +163,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - resourceManager, analytics, config, calendarInteractor, @@ -170,6 +170,7 @@ class CourseDatesViewModelTest { preferencesManager, courseRouter, calendarRouter, + resourceManager, ) coEvery { interactor.getCourseDates(any()) } returns CourseMocks.courseDatesResultWithData val message = async { @@ -192,7 +193,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - resourceManager, analytics, config, calendarInteractor, @@ -200,6 +200,7 @@ class CourseDatesViewModelTest { preferencesManager, courseRouter, calendarRouter, + resourceManager, ) coEvery { interactor.getCourseDates(any()) } returns CourseDatesResult( datesSection = linkedMapOf(), diff --git a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt index 981c88783..33d445fd1 100644 --- a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt @@ -22,6 +22,7 @@ import org.openedx.core.domain.model.AnnouncementModel import org.openedx.core.domain.model.HandoutsModel import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -35,6 +36,7 @@ class HandoutsViewModelTest { private val config = mockk() private val interactor = mockk() private val analytics = mockk() + private val resourceManager = mockk() @Before fun setUp() { @@ -49,7 +51,8 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse no internet connection exception`() = runTest { - val viewModel = HandoutsViewModel("", "Handouts", config, interactor, analytics) + val viewModel = + HandoutsViewModel("", "Handouts", config, interactor, analytics, resourceManager) coEvery { interactor.getHandouts(any()) } throws UnknownHostException() advanceUntilIdle() @@ -58,7 +61,8 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse unknown exception`() = runTest { - val viewModel = HandoutsViewModel("", "Handouts", config, interactor, analytics) + val viewModel = + HandoutsViewModel("", "Handouts", config, interactor, analytics, resourceManager) coEvery { interactor.getHandouts(any()) } throws Exception() advanceUntilIdle() @@ -68,7 +72,14 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse handouts success`() = runTest { val viewModel = - HandoutsViewModel("", HandoutsType.Handouts.name, config, interactor, analytics) + HandoutsViewModel( + "", + HandoutsType.Handouts.name, + config, + interactor, + analytics, + resourceManager + ) coEvery { interactor.getHandouts(any()) } returns HandoutsModel("hello") advanceUntilIdle() @@ -81,7 +92,14 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse announcements success`() = runTest { val viewModel = - HandoutsViewModel("", HandoutsType.Announcements.name, config, interactor, analytics) + HandoutsViewModel( + "", + HandoutsType.Announcements.name, + config, + interactor, + analytics, + resourceManager + ) coEvery { interactor.getAnnouncements(any()) } returns listOf( AnnouncementModel( "date", @@ -99,7 +117,14 @@ class HandoutsViewModelTest { @Test fun `injectDarkMode test`() = runTest { val viewModel = - HandoutsViewModel("", HandoutsType.Announcements.name, config, interactor, analytics) + HandoutsViewModel( + "", + HandoutsType.Announcements.name, + config, + interactor, + analytics, + resourceManager + ) coEvery { interactor.getAnnouncements(any()) } returns listOf( AnnouncementModel( "date", diff --git a/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt index 5387d7965..a150639c3 100644 --- a/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt @@ -46,6 +46,7 @@ import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil import java.net.UnknownHostException import org.openedx.course.R as courseR +import org.openedx.foundation.R as foundationR @Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) @@ -79,9 +80,8 @@ class CourseHomeViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { resourceManager.getString(foundationR.string.foundation_error_no_connection) } returns noInternet + every { resourceManager.getString(foundationR.string.foundation_error_unknown_error) } returns somethingWrong every { resourceManager.getString(courseR.string.course_can_download_only_with_wifi) } returns cantDownload 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 381e09948..33ae2dcb9 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 @@ -29,7 +29,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.CoreMocks -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseComponentStatus @@ -50,6 +49,7 @@ import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseOutlineViewModelTest { @@ -81,8 +81,12 @@ class CourseOutlineViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt index 3f08ae795..a7c994cf9 100644 --- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt @@ -24,7 +24,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.CoreMocks -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.db.DownloadDao @@ -36,8 +35,10 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.unit.container.CourseViewMode import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseSectionViewModelTest { @@ -64,8 +65,8 @@ class CourseSectionViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { resourceManager.getString(foundationR.string.foundation_error_no_connection) } returns noInternet + every { resourceManager.getString(foundationR.string.foundation_error_unknown_error) } returns somethingWrong every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload @@ -96,8 +97,8 @@ class CourseSectionViewModelTest { coVerify(exactly = 1) { interactor.getCourseStructure(any()) } coVerify(exactly = 0) { interactor.getCourseStructureForVideos(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is CourseSectionUIState.Loading) } @@ -121,8 +122,8 @@ class CourseSectionViewModelTest { coVerify(exactly = 1) { interactor.getCourseStructure(any()) } coVerify(exactly = 0) { interactor.getCourseStructureForVideos(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is CourseSectionUIState.Loading) } @@ -151,7 +152,8 @@ class CourseSectionViewModelTest { coVerify(exactly = 0) { interactor.getCourseStructure(any()) } coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is CourseSectionUIState.Blocks) } @@ -174,7 +176,8 @@ class CourseSectionViewModelTest { advanceUntilIdle() - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) } @Test @@ -196,7 +199,8 @@ class CourseSectionViewModelTest { advanceUntilIdle() - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) } @Test diff --git a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt index 9c4f71685..7e9324a9c 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt @@ -25,6 +25,7 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -41,6 +42,7 @@ class CourseUnitContainerViewModelTest { private val analytics = mockk() private val networkConnection = mockk() private val videoPreviewHelper = mockk() + private val resourceManager = mockk() @Before fun setUp() { @@ -65,7 +67,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() @@ -89,7 +92,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() @@ -113,7 +117,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure @@ -139,7 +144,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure @@ -163,7 +169,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure @@ -189,7 +196,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure @@ -215,7 +223,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure @@ -241,7 +250,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure @@ -267,7 +277,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure("") } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos("") } returns CoreMocks.mockCourseStructure @@ -293,7 +304,8 @@ class CourseUnitContainerViewModelTest { notifier, analytics, networkConnection, - videoPreviewHelper + videoPreviewHelper, + resourceManager ) coEvery { interactor.getCourseStructure(any()) } returns CoreMocks.mockCourseStructure coEvery { interactor.getCourseStructureForVideos(any()) } returns CoreMocks.mockCourseStructure diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt index 1d8524a7b..614e3517a 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt @@ -29,6 +29,7 @@ import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent +import org.openedx.foundation.system.ResourceManager @OptIn(ExperimentalCoroutinesApi::class) class VideoUnitViewModelTest { @@ -43,6 +44,7 @@ class VideoUnitViewModelTest { private val networkConnection = mockk() private val transcriptManager = mockk() private val courseAnalytics = mockk() + private val resourceManager = mockk() @Before fun setUp() { @@ -64,7 +66,8 @@ class VideoUnitViewModelTest { notifier, networkConnection, transcriptManager, - courseAnalytics + courseAnalytics, + resourceManager ) coEvery { courseRepository.markBlocksCompletion( @@ -106,6 +109,7 @@ class VideoUnitViewModelTest { networkConnection, transcriptManager, courseAnalytics, + resourceManager ) coEvery { courseRepository.markBlocksCompletion( @@ -147,6 +151,7 @@ class VideoUnitViewModelTest { networkConnection, transcriptManager, courseAnalytics, + resourceManager ) coEvery { notifier.notifier } returns flow { emit( diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt index ae954c5f7..223d9c59c 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt @@ -24,6 +24,7 @@ import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent +import org.openedx.foundation.system.ResourceManager @OptIn(ExperimentalCoroutinesApi::class) class VideoViewModelTest { @@ -37,6 +38,7 @@ class VideoViewModelTest { private val notifier = mockk() private val preferenceManager = mockk() private val courseAnalytics = mockk() + private val resourceManager = mockk() @Before fun setUp() { @@ -51,7 +53,14 @@ class VideoViewModelTest { @Test fun `sendTime test`() = runTest { val viewModel = - VideoViewModel("", courseRepository, notifier, preferenceManager, courseAnalytics) + VideoViewModel( + "", + courseRepository, + notifier, + preferenceManager, + courseAnalytics, + resourceManager + ) coEvery { notifier.send(CourseVideoPositionChanged("", 0, 0L, false)) } returns Unit viewModel.sendTime() advanceUntilIdle() @@ -62,7 +71,14 @@ class VideoViewModelTest { @Test fun `markBlockCompleted exception`() = runTest { val viewModel = - VideoViewModel("", courseRepository, notifier, preferenceManager, courseAnalytics) + VideoViewModel( + "", + courseRepository, + notifier, + preferenceManager, + courseAnalytics, + resourceManager + ) coEvery { courseRepository.markBlocksCompletion( any(), @@ -95,7 +111,14 @@ class VideoViewModelTest { @Test fun `markBlockCompleted success`() = runTest { val viewModel = - VideoViewModel("", courseRepository, notifier, preferenceManager, courseAnalytics) + VideoViewModel( + "", + courseRepository, + notifier, + preferenceManager, + courseAnalytics, + resourceManager + ) coEvery { courseRepository.markBlocksCompletion( any(), diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 237c8f35a..7f1a904a8 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -3,15 +3,11 @@ package org.openedx.courses.presentation import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.system.connection.NetworkConnection @@ -21,9 +17,7 @@ import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardAnalytics import org.openedx.dashboard.presentation.DashboardRouter -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class AllEnrolledCoursesViewModel( @@ -34,7 +28,7 @@ class AllEnrolledCoursesViewModel( private val discoveryNotifier: DiscoveryNotifier, private val analytics: DashboardAnalytics, private val dashboardRouter: DashboardRouter -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() val hasInternetConnection: Boolean @@ -48,10 +42,6 @@ class AllEnrolledCoursesViewModel( val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val currentFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) private var job: Job? = null @@ -98,19 +88,9 @@ class AllEnrolledCoursesViewModel( coursesList.addAll(response.courses) _uiState.update { it.copy(courses = coursesList.toList()) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } _uiState.update { it.copy(refreshing = false, showProgress = false) } isLoading = false @@ -148,19 +128,9 @@ class AllEnrolledCoursesViewModel( } _uiState.update { it.copy(courses = coursesList.toList()) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } _uiState.update { it.copy(refreshing = false, showProgress = false) } isLoading = 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 0ca8f4a6e..0ec58503a 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -2,14 +2,10 @@ package org.openedx.courses.presentation import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments import org.openedx.core.data.storage.CorePreferences @@ -20,9 +16,7 @@ import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.NavigationToDiscovery import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @@ -37,7 +31,7 @@ class DashboardGalleryViewModel( private val dashboardRouter: DashboardRouter, private val corePreferences: CorePreferences, private val windowSize: WindowSize, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() @@ -46,10 +40,6 @@ class DashboardGalleryViewModel( val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val _updating = MutableStateFlow(false) val updating: StateFlow get() = _updating.asStateFlow() @@ -99,19 +89,9 @@ class DashboardGalleryViewModel( } } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { _updating.value = false isLoading = false 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 3e59ee3cd..6eea1d9bf 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -42,6 +42,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -115,7 +116,7 @@ class DashboardListFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(null) val refreshing by viewModel.updating.observeAsState(false) val canLoadMore by viewModel.canLoadMore.observeAsState(false) diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index 58f83b8f2..b09f8446c 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -5,17 +5,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class DashboardListViewModel( @@ -25,7 +21,7 @@ class DashboardListViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val analytics: DashboardAnalytics, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val coursesList = mutableListOf() private var page = 1 @@ -37,10 +33,6 @@ class DashboardListViewModel( val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _updating = MutableLiveData() val updating: LiveData get() = _updating @@ -98,13 +90,9 @@ class DashboardListViewModel( _uiState.value = DashboardUIState.Courses(ArrayList(coursesList)) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _updating.value = false isLoading = false @@ -141,13 +129,9 @@ class DashboardListViewModel( _uiState.value = DashboardUIState.Courses(ArrayList(coursesList)) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _updating.value = false isLoading = false diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt index 21e746374..05dc94c05 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -14,6 +14,7 @@ import org.openedx.dashboard.presentation.DashboardAnalyticsEvent import org.openedx.dashboard.presentation.DashboardAnalyticsKey import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager import org.openedx.learn.LearnType class LearnViewModel( @@ -21,7 +22,8 @@ class LearnViewModel( private val config: Config, private val dashboardRouter: DashboardRouter, private val analytics: DashboardAnalytics, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow( LearnUIState( if (openTab == LearnTab.PROGRAMS.name) { diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt index fae8a9455..123c59d82 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt @@ -22,7 +22,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.Pagination @@ -33,6 +32,7 @@ import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DashboardListViewModelTest { @@ -60,8 +60,12 @@ class DashboardListViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { config.getApiHostURL() } returns "http://localhost:8000" } @@ -70,6 +74,10 @@ class DashboardListViewModelTest { Dispatchers.resetMain() } + private fun DashboardListViewModel.lastUiMessage(): UIMessage? { + return uiMessage.replayCache.lastOrNull() + } + @Test fun `getCourses no internet connection`() = runTest { val viewModel = DashboardListViewModel( @@ -87,7 +95,7 @@ class DashboardListViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.uiState.value is DashboardUIState.Loading) } @@ -109,7 +117,7 @@ class DashboardListViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.uiState.value is DashboardUIState.Loading) } @@ -132,7 +140,7 @@ class DashboardListViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is DashboardUIState.Courses) } @@ -162,7 +170,7 @@ class DashboardListViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is DashboardUIState.Courses) } @@ -184,7 +192,7 @@ class DashboardListViewModelTest { coVerify(exactly = 0) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 1) { interactor.getEnrolledCoursesFromCache() } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is DashboardUIState.Courses) } @@ -208,7 +216,7 @@ class DashboardListViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.updating.value == false) assert(viewModel.uiState.value is DashboardUIState.Loading) @@ -234,7 +242,7 @@ class DashboardListViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.updating.value == false) assert(viewModel.uiState.value is DashboardUIState.Loading) @@ -258,7 +266,7 @@ class DashboardListViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.updating.value == false) assert(viewModel.uiState.value is DashboardUIState.Courses) } @@ -288,7 +296,7 @@ class DashboardListViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.updating.value == false) assert(viewModel.uiState.value is DashboardUIState.Courses) } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt index c82df34d8..6e596b3d4 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt @@ -18,6 +18,7 @@ import org.junit.Test import org.openedx.DashboardNavigator import org.openedx.core.config.Config import org.openedx.core.config.DashboardConfig +import org.openedx.foundation.system.ResourceManager import org.openedx.learn.presentation.LearnTab import org.openedx.learn.presentation.LearnViewModel @@ -29,6 +30,7 @@ class LearnViewModelTest { private val config = mockk() private val dashboardRouter = mockk(relaxed = true) private val analytics = mockk(relaxed = true) + private val resourceManager = mockk() private val fragmentManager = mockk() @Before @@ -43,14 +45,26 @@ class LearnViewModelTest { @Test fun `onSettingsClick calls navigateToSettings`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) viewModel.onSettingsClick(fragmentManager) verify { dashboardRouter.navigateToSettings(fragmentManager) } } @Test fun `getDashboardFragment returns correct fragment based on dashboardType`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) DashboardConfig.DashboardType.entries.forEach { type -> every { config.getDashboardConfig().getType() } returns type val dashboardFragment = viewModel.getDashboardFragment @@ -60,21 +74,39 @@ class LearnViewModelTest { @Test fun `getProgramFragment returns correct program fragment`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) viewModel.getProgramFragment verify { dashboardRouter.getProgramFragment() } } @Test fun `isProgramTypeWebView returns correct view type`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) every { config.getProgramConfig().isViewTypeWebView() } returns true assertTrue(viewModel.isProgramTypeWebView) } @Test fun `logMyCoursesTabClickedEvent logs correct analytics event`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) viewModel.logMyCoursesTabClickedEvent() verify { @@ -89,7 +121,13 @@ class LearnViewModelTest { @Test fun `logMyProgramsTabClickedEvent logs correct analytics event`() = runTest { - val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) + val viewModel = LearnViewModel( + LearnTab.COURSES.name, + config, + dashboardRouter, + analytics, + resourceManager + ) viewModel.logMyProgramsTabClickedEvent() verify { diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DatesViewModel.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DatesViewModel.kt index 518808d49..59fa58d75 100644 --- a/dates/src/main/java/org/openedx/dates/presentation/dates/DatesViewModel.kt +++ b/dates/src/main/java/org/openedx/dates/presentation/dates/DatesViewModel.kt @@ -3,15 +3,11 @@ package org.openedx.dates.presentation.dates import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseDate import org.openedx.core.domain.model.CourseDatesResponse @@ -26,9 +22,7 @@ import org.openedx.dates.presentation.DatesAnalytics import org.openedx.dates.presentation.DatesAnalyticsEvent import org.openedx.dates.presentation.DatesAnalyticsKey import org.openedx.dates.presentation.DatesRouter -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.util.Calendar import java.util.Date @@ -41,16 +35,12 @@ class DatesViewModel( private val analytics: DatesAnalytics, private val calendarSyncScheduler: CalendarSyncScheduler, corePreferences: CorePreferences, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow(DatesUIState()) val uiState: StateFlow get() = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -78,7 +68,7 @@ class DatesViewModel( } catch (e: Exception) { page = -1 updateUIWithCachedResponse() - handleFetchException(e) + handleErrorUiMessage(e) } finally { clearLoadingState() } @@ -133,18 +123,6 @@ class DatesViewModel( } } - private suspend fun handleFetchException(e: Throwable) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - ) - } - } - private fun clearLoadingState() { _uiState.update { state -> state.copy( @@ -167,7 +145,7 @@ class DatesViewModel( refreshData() calendarSyncScheduler.requestImmediateSync() } catch (e: Exception) { - handleFetchException(e) + handleErrorUiMessage(e) } finally { _uiState.update { state -> state.copy( diff --git a/dates/src/test/java/org/openedx/dates/DatesViewModelTest.kt b/dates/src/test/java/org/openedx/dates/DatesViewModelTest.kt index 4bb903753..ee591922a 100644 --- a/dates/src/test/java/org/openedx/dates/DatesViewModelTest.kt +++ b/dates/src/test/java/org/openedx/dates/DatesViewModelTest.kt @@ -25,7 +25,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseDate import org.openedx.core.domain.model.CourseDatesResponse @@ -39,6 +38,7 @@ import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException import java.util.Date +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DatesViewModelTest { @@ -62,9 +62,8 @@ class DatesViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - // By default, assume we have an internet connection + every { resourceManager.getString(foundationR.string.foundation_error_no_connection) } returns noInternet + every { resourceManager.getString(foundationR.string.foundation_error_unknown_error) } returns somethingWrong every { networkConnection.isOnline() } returns true every { corePreferences.isRelativeDatesEnabled } returns true every { analytics.logEvent(any(), any()) } returns Unit diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt index 6f0337d09..720971ff4 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt @@ -33,6 +33,7 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -96,7 +97,7 @@ class NativeDiscoveryFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) val querySearch = arguments?.getString(ARG_SEARCH_QUERY, "") ?: "" diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt index 70acffbd8..32cfde436 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt @@ -4,16 +4,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.connection.NetworkConnection import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.Course -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class NativeDiscoveryViewModel( @@ -23,7 +19,7 @@ class NativeDiscoveryViewModel( private val resourceManager: ResourceManager, private val analytics: DiscoveryAnalytics, private val corePreferences: CorePreferences, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() val isUserLoggedIn get() = corePreferences.user != null @@ -34,10 +30,6 @@ class NativeDiscoveryViewModel( val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _canLoadMore = MutableLiveData() val canLoadMore: LiveData get() = _canLoadMore @@ -86,13 +78,9 @@ class NativeDiscoveryViewModel( } _uiState.value = DiscoveryUIState.Courses(ArrayList(coursesList)) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } finally { isLoading = false } @@ -129,13 +117,9 @@ class NativeDiscoveryViewModel( coursesList.addAll(response.results) _uiState.value = DiscoveryUIState.Courses(ArrayList(coursesList)) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } finally { isLoading = false _isUpdating.value = false diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt index f15588ff9..90a4ecdd1 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt @@ -11,6 +11,7 @@ import org.openedx.core.presentation.global.ErrorType import org.openedx.core.presentation.global.webview.WebViewUIState import org.openedx.core.system.connection.NetworkConnection import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.UrlUtils class WebViewDiscoveryViewModel( @@ -21,7 +22,8 @@ class WebViewDiscoveryViewModel( private val corePreferences: CorePreferences, private val router: DiscoveryRouter, private val analytics: DiscoveryAnalytics, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow(WebViewUIState.Loading) val uiState: StateFlow = _uiState.asStateFlow() diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt index 8e4ba7fb9..e04e00739 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt @@ -45,6 +45,7 @@ import androidx.compose.material.icons.outlined.Report import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableFloatStateOf @@ -126,7 +127,7 @@ class CourseDetailsFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val colorBackgroundValue = MaterialTheme.appColors.background.value val colorTextValue = MaterialTheme.appColors.textPrimary.value diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt index b212c588f..995062c03 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.connection.NetworkConnection @@ -16,10 +15,7 @@ import org.openedx.discovery.domain.model.Course import org.openedx.discovery.presentation.DiscoveryAnalytics import org.openedx.discovery.presentation.DiscoveryAnalyticsEvent import org.openedx.discovery.presentation.DiscoveryAnalyticsKey -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class CourseDetailsViewModel( @@ -32,7 +28,7 @@ class CourseDetailsViewModel( private val notifier: DiscoveryNotifier, private val analytics: DiscoveryAnalytics, private val calendarSyncScheduler: CalendarSyncScheduler, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() val isUserLoggedIn get() = corePreferences.user != null val isRegistrationEnabled: Boolean get() = config.isRegistrationEnabled() @@ -40,9 +36,6 @@ class CourseDetailsViewModel( private val _uiState = MutableLiveData(CourseDetailsUIState.Loading) val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage private var course: Course? = null @@ -68,17 +61,14 @@ class CourseDetailsViewModel( isUserLoggedIn = isUserLoggedIn ) } ?: run { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + handleErrorUiMessage( + throwable = null, + ) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -99,13 +89,9 @@ class CourseDetailsViewModel( notifier.send(CourseDashboardUpdate()) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt index 184001160..985efe871 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt @@ -33,7 +33,6 @@ import org.openedx.foundation.presentation.BaseViewModel import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.util.concurrent.atomic.AtomicReference -import org.openedx.core.R as CoreR class CourseInfoViewModel( val pathId: String, @@ -47,7 +46,7 @@ class CourseInfoViewModel( private val resourceManager: ResourceManager, private val analytics: DiscoveryAnalytics, corePreferences: CorePreferences, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow( @@ -62,10 +61,6 @@ class CourseInfoViewModel( val webViewState get() = _webViewUIState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val _showAlert = MutableSharedFlow() val showAlert: SharedFlow get() = _showAlert.asSharedFlow() @@ -103,7 +98,7 @@ class CourseInfoViewModel( }.isEnrolled if (isCourseEnrolled) { - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage(resourceManager.getString(R.string.discovery_you_are_already_enrolled)) ) _uiState.update { it.copy(enrollmentSuccess = AtomicReference(courseId)) } @@ -113,14 +108,14 @@ class CourseInfoViewModel( interactor.enrollInACourse(courseId) courseEnrollSuccessEvent(courseId) notifier.send(CourseDashboardUpdate()) - _uiMessage.emit( + sendMessage( UIMessage.ToastMessage(resourceManager.getString(R.string.discovery_enrolled_successfully)) ) _uiState.update { it.copy(enrollmentSuccess = AtomicReference(courseId)) } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection)) + handleErrorUiMessage( + throwable = e, ) } else { _showAlert.emit(true) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt index fd954df30..494208851 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.global.ErrorType @@ -31,7 +30,7 @@ class ProgramViewModel( private val edxCookieManager: AppCookieManager, private val resourceManager: ResourceManager, private val interactor: DiscoveryInteractor, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val uriScheme: String get() = config.getUriScheme() val programConfig get() = config.getProgramConfig().webViewConfig @@ -62,7 +61,11 @@ class ProgramViewModel( if (e.isInternetError()) { _uiState.emit( ProgramUIState.UiMessage( - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resolveErrorMessage( + throwable = e, + ) + ) ) ) } else { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt index 77f6aec83..06290673a 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt @@ -32,6 +32,7 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -105,7 +106,7 @@ class CourseSearchFragment : Fragment() { 0 ) ) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) val querySearch = arguments?.getString(ARG_SEARCH_QUERY, "") ?: "" diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt index f001b46eb..3bd4605d1 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt @@ -8,16 +8,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.Course import org.openedx.discovery.presentation.DiscoveryAnalytics -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class CourseSearchViewModel( @@ -26,7 +22,7 @@ class CourseSearchViewModel( private val interactor: DiscoveryInteractor, private val resourceManager: ResourceManager, private val analytics: DiscoveryAnalytics -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val apiHostUrl get() = config.getApiHostURL() val isUserLoggedIn get() = corePreferences.user != null @@ -37,10 +33,6 @@ class CourseSearchViewModel( val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _canLoadMore = MutableLiveData() val canLoadMore: LiveData get() = _canLoadMore @@ -123,13 +115,9 @@ class CourseSearchViewModel( coursesList.addAll(response.results) _uiState.value = CourseSearchUIState.Courses(coursesList, response.pagination.count) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } finally { isLoading = false _isUpdating.value = false diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt index d6270fe7b..f1375fa36 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt @@ -18,7 +18,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Pagination @@ -26,8 +25,10 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.CourseList import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class NativeDiscoveryViewModelTest { @@ -50,8 +51,12 @@ class NativeDiscoveryViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { corePreferences.user } returns null every { config.getApiHostURL() } returns "http://localhost:8000" every { config.isPreLoginExperienceEnabled() } returns false @@ -79,8 +84,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscoveryUIState.Loading) assert(viewModel.canLoadMore.value == null) } @@ -102,8 +107,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscoveryUIState.Loading) assert(viewModel.canLoadMore.value == null) } @@ -125,7 +130,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 0) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 1) { interactor.getCoursesListFromCache() } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscoveryUIState.Courses) assert(viewModel.canLoadMore.value == false) } @@ -155,7 +161,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscoveryUIState.Courses) assert(viewModel.canLoadMore.value == true) } @@ -185,7 +192,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscoveryUIState.Courses) assert(viewModel.canLoadMore.value == false) } @@ -207,8 +215,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 2) { interactor.getCoursesList(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == null) assert(viewModel.uiState.value is DiscoveryUIState.Loading) @@ -231,8 +239,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 2) { interactor.getCoursesList(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == null) assert(viewModel.uiState.value is DiscoveryUIState.Loading) @@ -263,7 +271,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 2) { interactor.getCoursesList(any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == true) assert(viewModel.uiState.value is DiscoveryUIState.Courses) @@ -294,7 +303,8 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 2) { interactor.getCoursesList(any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) assert(viewModel.uiState.value is DiscoveryUIState.Courses) diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt index 2c9f282b3..34f55ac7b 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt @@ -21,7 +21,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.connection.NetworkConnection @@ -35,6 +34,7 @@ import org.openedx.discovery.presentation.DiscoveryAnalyticsEvent import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseDetailsViewModelTest { @@ -59,8 +59,12 @@ class CourseDetailsViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { config.getApiHostURL() } returns "http://localhost:8000" every { calendarSyncScheduler.requestImmediateSync(any()) } returns Unit } @@ -70,6 +74,10 @@ class CourseDetailsViewModelTest { Dispatchers.resetMain() } + private fun CourseDetailsViewModel.lastUiMessage(): UIMessage? { + return uiMessage.replayCache.lastOrNull() + } + @Test fun `getCourseDetails no internet connection exception`() = runTest { val viewModel = CourseDetailsViewModel( @@ -89,7 +97,7 @@ class CourseDetailsViewModelTest { coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.uiState.value is CourseDetailsUIState.Loading) @@ -114,7 +122,7 @@ class CourseDetailsViewModelTest { coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.uiState.value is CourseDetailsUIState.Loading) @@ -142,7 +150,7 @@ class CourseDetailsViewModelTest { coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) } @@ -169,7 +177,7 @@ class CourseDetailsViewModelTest { coVerify(exactly = 0) { interactor.getCourseDetails(any()) } coVerify(exactly = 1) { interactor.getCourseDetailsFromCache(any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) } @@ -200,7 +208,7 @@ class CourseDetailsViewModelTest { coVerify(exactly = 1) { interactor.enrollInACourse(any()) } verify { analytics.logEvent(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) } @@ -242,7 +250,7 @@ class CourseDetailsViewModelTest { ) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) } @@ -297,7 +305,7 @@ class CourseDetailsViewModelTest { ) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) } diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt index 392923eb2..e0c9056fd 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt @@ -15,11 +15,11 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Pagination @@ -28,8 +28,10 @@ import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.CourseList import org.openedx.discovery.presentation.DiscoveryAnalytics import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class CourseSearchViewModelTest { @@ -51,8 +53,8 @@ class CourseSearchViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { resourceManager.getString(foundationR.string.foundation_error_no_connection) } returns noInternet + every { resourceManager.getString(foundationR.string.foundation_error_unknown_error) } returns somethingWrong every { config.getApiHostURL() } returns "http://localhost:8000" } @@ -73,7 +75,8 @@ class CourseSearchViewModelTest { assert(uiState.courses.isEmpty()) assert(uiState.numCourses == 0) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) } @Test @@ -87,9 +90,9 @@ class CourseSearchViewModelTest { coVerify(exactly = 1) { interactor.getCoursesListByQuery(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is CourseSearchUIState.Loading) - assert(message.message == noInternet) + assert((message.await() as UIMessage.SnackBarMessage).message == noInternet) } @Test @@ -103,9 +106,9 @@ class CourseSearchViewModelTest { coVerify(exactly = 1) { interactor.getCoursesListByQuery(any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is CourseSearchUIState.Loading) - assert(message.message == somethingWrong) + assert((message.await() as UIMessage.SnackBarMessage).message == somethingWrong) } @Test @@ -131,7 +134,8 @@ class CourseSearchViewModelTest { verify(exactly = 1) { analytics.discoveryCourseSearchEvent(any(), any()) } assert(viewModel.uiState.value is CourseSearchUIState.Courses) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) } @@ -166,7 +170,8 @@ class CourseSearchViewModelTest { assert(viewModel.uiState.value is CourseSearchUIState.Courses) assert((viewModel.uiState.value as CourseSearchUIState.Courses).courses.size == 3) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) } @@ -203,7 +208,8 @@ class CourseSearchViewModelTest { assert(viewModel.uiState.value is CourseSearchUIState.Courses) assert((viewModel.uiState.value as CourseSearchUIState.Courses).courses.size == 2) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == true) } @@ -229,7 +235,8 @@ class CourseSearchViewModelTest { assert(viewModel.uiState.value is CourseSearchUIState.Courses) assert((viewModel.uiState.value as CourseSearchUIState.Courses).courses.isEmpty()) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.isUpdating.value == null) } } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt index 46f3eab18..36c430340 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -42,6 +42,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -117,7 +118,7 @@ class DiscussionCommentsFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(DiscussionCommentsUIState.Loading) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt index fbd5b464e..2abe78c38 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.domain.model.DiscussionComment import org.openedx.discussion.domain.model.DiscussionType @@ -13,9 +12,7 @@ import org.openedx.discussion.system.notifier.DiscussionCommentAdded import org.openedx.discussion.system.notifier.DiscussionCommentDataChanged import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager @@ -24,7 +21,7 @@ class DiscussionCommentsViewModel( private val resourceManager: ResourceManager, private val notifier: DiscussionNotifier, thread: org.openedx.discussion.domain.model.Thread, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { val title = resourceManager.getString(thread.type.resId) @@ -36,10 +33,6 @@ class DiscussionCommentsViewModel( val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _canLoadMore = MutableLiveData() val canLoadMore: LiveData get() = _canLoadMore @@ -65,10 +58,11 @@ class DiscussionCommentsViewModel( commentCount ) } else { - _uiMessage.value = + sendMessage( UIMessage.ToastMessage( resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) ) + ) } thread = thread.copy(commentCount = thread.commentCount + 1) sendThreadUpdated() @@ -125,13 +119,9 @@ class DiscussionCommentsViewModel( markRead() } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } finally { isLoading = false _isUpdating.value = false @@ -181,13 +171,9 @@ class DiscussionCommentsViewModel( DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -201,13 +187,9 @@ class DiscussionCommentsViewModel( DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -221,13 +203,9 @@ class DiscussionCommentsViewModel( DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -236,21 +214,15 @@ class DiscussionCommentsViewModel( viewModelScope.launch { try { val response = interactor.setCommentVoted(commentId, vote) - val index = comments.indexOfFirst { - it.id == response.id - } + val index = comments.indexOfFirst { it.id == response.id } comments[index] = comments[index].copy(voted = response.voted, voteCount = response.voteCount) _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -259,20 +231,14 @@ class DiscussionCommentsViewModel( viewModelScope.launch { try { val response = interactor.setCommentFlagged(commentId, vote) - val index = comments.indexOfFirst { - it.id == response.id - } + val index = comments.indexOfFirst { it.id == response.id } comments[index] = comments[index].copy(abuseFlagged = response.abuseFlagged) _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -286,21 +252,18 @@ class DiscussionCommentsViewModel( if (page == -1) { comments.add(response) } else { - _uiMessage.value = + sendMessage( UIMessage.ToastMessage( resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) ) + ) } _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt index 171d5ff31..ecb7c5fa8 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt @@ -43,6 +43,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -124,7 +125,7 @@ class DiscussionResponsesFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(DiscussionResponsesUIState.Loading) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt index e4c675609..9097f307b 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt @@ -4,14 +4,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.domain.model.DiscussionComment import org.openedx.discussion.system.notifier.DiscussionCommentDataChanged import org.openedx.discussion.system.notifier.DiscussionNotifier -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager @@ -20,16 +17,12 @@ class DiscussionResponsesViewModel( private val resourceManager: ResourceManager, private val notifier: DiscussionNotifier, private var comment: DiscussionComment, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _canLoadMore = MutableLiveData() val canLoadMore: LiveData get() = _canLoadMore @@ -85,17 +78,9 @@ class DiscussionResponsesViewModel( comments.addAll(response.results) _uiState.value = DiscussionResponsesUIState.Success(comment, comments.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { isLoading = false _isUpdating.value = false @@ -119,17 +104,9 @@ class DiscussionResponsesViewModel( } _uiState.value = DiscussionResponsesUIState.Success(comment, comments.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -149,17 +126,9 @@ class DiscussionResponsesViewModel( } _uiState.value = DiscussionResponsesUIState.Success(comment, comments.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -173,25 +142,18 @@ class DiscussionResponsesViewModel( if (page == -1) { comments.add(response) } else { - _uiMessage.value = + sendMessage( UIMessage.ToastMessage( resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) ) + ) } _uiState.value = DiscussionResponsesUIState.Success(comment, comments.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - } + handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt index 6e69f2a4f..eee3115fa 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt @@ -32,6 +32,7 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -108,7 +109,7 @@ class DiscussionSearchThreadFragment : Fragment() { 0 ) ) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt index d95dcba9e..0fe2929dc 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt @@ -15,14 +15,10 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class DiscussionSearchThreadViewModel( @@ -30,7 +26,7 @@ class DiscussionSearchThreadViewModel( private val resourceManager: ResourceManager, private val notifier: DiscussionNotifier, val courseId: String -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData( DiscussionSearchThreadUIState.Threads( @@ -41,10 +37,6 @@ class DiscussionSearchThreadViewModel( val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _canLoadMore = MutableLiveData() val canLoadMore: LiveData get() = _canLoadMore @@ -155,13 +147,9 @@ class DiscussionSearchThreadViewModel( isLoading = false _isUpdating.value = false }.catch { e -> - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) isLoading = false _isUpdating.value = false } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt index bda4e3730..7c7215e35 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt @@ -43,6 +43,7 @@ import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -109,7 +110,7 @@ class DiscussionAddThreadFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val isLoading by viewModel.isLoading.observeAsState(false) val success by viewModel.newThread.observeAsState() diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt index b16b9f300..1c9e52d21 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt @@ -4,14 +4,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.discussion.domain.interactor.DiscussionInteractor +import org.openedx.discussion.domain.model.Thread import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.discussion.system.notifier.DiscussionThreadAdded -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class DiscussionAddThreadViewModel( @@ -19,16 +16,12 @@ class DiscussionAddThreadViewModel( private val resourceManager: ResourceManager, private val notifier: DiscussionNotifier, private val courseId: String -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { - private val _newThread = MutableLiveData() - val newThread: LiveData + private val _newThread = MutableLiveData() + val newThread: LiveData get() = _newThread - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _isLoading = MutableLiveData() val isLoading: LiveData get() = _isLoading @@ -45,13 +38,9 @@ class DiscussionAddThreadViewModel( try { _newThread.value = interactor.createThread(topicId, courseId, type, title, rawBody, follow) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _isLoading.value = false } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt index 65a1f24bc..7e7af161e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -43,6 +43,7 @@ import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -132,7 +133,7 @@ class DiscussionThreadsFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(DiscussionThreadsUIState.Loading) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt index e79c7672b..b60582e1c 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt @@ -5,16 +5,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.discussion.system.notifier.DiscussionThreadAdded import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.SingleEventLiveData -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class DiscussionThreadsViewModel( @@ -24,16 +20,12 @@ class DiscussionThreadsViewModel( val courseId: String, val topicId: String, private val threadType: String -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _isUpdating = MutableLiveData() val isUpdating: LiveData get() = _isUpdating @@ -161,13 +153,9 @@ class DiscussionThreadsViewModel( threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _isUpdating.value = false isLoading = false @@ -188,13 +176,9 @@ class DiscussionThreadsViewModel( threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _isUpdating.value = false isLoading = false @@ -216,13 +200,9 @@ class DiscussionThreadsViewModel( threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } _isUpdating.value = false isLoading = false diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 84a5d3e15..abe52f2ae 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -3,11 +3,7 @@ package org.openedx.discussion.presentation.topics import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.RefreshDiscussions @@ -16,7 +12,6 @@ import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class DiscussionTopicsViewModel( @@ -27,16 +22,12 @@ class DiscussionTopicsViewModel( private val analytics: DiscussionAnalytics, private val courseNotifier: CourseNotifier, val discussionRouter: DiscussionRouter, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - init { collectCourseNotifier() @@ -55,10 +46,8 @@ class DiscussionTopicsViewModel( } catch (e: Exception) { _uiState.value = DiscussionTopicsUIState.Error if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) + handleErrorUiMessage( + throwable = e, ) } } finally { diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt index 74f940396..797452c39 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt @@ -19,12 +19,11 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After -import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Pagination import org.openedx.discussion.DiscussionMocks @@ -36,8 +35,10 @@ import org.openedx.discussion.system.notifier.DiscussionCommentDataChanged import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) @@ -71,8 +72,12 @@ class DiscussionCommentsViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) } returns commentAddedSuccessfully @@ -100,9 +105,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Loading) assert(viewModel.isUpdating.value == false) } @@ -125,9 +129,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadComments(any(), any()) } coVerify(exactly = 1) { interactor.getThreadQuestionComments(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Loading) assert(viewModel.isUpdating.value == false) } @@ -155,7 +158,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.getThreadQuestionComments(any(), any(), any()) } coVerify(exactly = 1) { interactor.setThreadRead(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == true) @@ -188,7 +192,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } coVerify(exactly = 1) { interactor.setThreadRead(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) @@ -222,7 +227,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } coVerify(exactly = 1) { interactor.setThreadRead(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) @@ -256,7 +262,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } coVerify(exactly = 1) { interactor.setThreadRead(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) @@ -289,7 +296,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 2) { interactor.getThreadComments(any(), any()) } coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) @@ -318,8 +326,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -347,8 +355,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -376,7 +384,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadVoted(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -404,8 +413,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -433,8 +442,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -463,7 +472,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -490,8 +500,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -518,8 +528,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -551,7 +561,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -578,8 +589,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -606,8 +617,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -634,7 +645,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFlagged(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -661,8 +673,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFollowed(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -689,8 +701,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFollowed(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -717,7 +729,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.setThreadFollowed(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -750,7 +763,8 @@ class DiscussionCommentsViewModelTest { advanceUntilIdle() - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -783,8 +797,11 @@ class DiscussionCommentsViewModelTest { advanceUntilIdle() - val message = viewModel.uiMessage.value as? UIMessage.ToastMessage - assert(commentAddedSuccessfully == message?.message) + val message = captureUiMessage(viewModel) + assertEquals( + commentAddedSuccessfully, + (message.await() as? UIMessage.ToastMessage)?.message + ) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -817,7 +834,8 @@ class DiscussionCommentsViewModelTest { advanceUntilIdle() - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } @@ -843,8 +861,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - Assert.assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -869,8 +887,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - Assert.assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -896,7 +914,8 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - assert(viewModel.uiMessage.value != null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt index 90b83a448..7c11ace6f 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt @@ -8,18 +8,21 @@ import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.withTimeoutOrNull import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Pagination import org.openedx.discussion.DiscussionMocks @@ -29,6 +32,7 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DiscussionResponsesViewModelTest { @@ -55,8 +59,12 @@ class DiscussionResponsesViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) } returns commentAddedSuccessfully @@ -68,6 +76,10 @@ class DiscussionResponsesViewModelTest { clearAllMocks() } + private fun TestScope.captureUiMessage(viewModel: DiscussionResponsesViewModel) = async { + withTimeoutOrNull(5_000) { viewModel.uiMessage.first() } + } + @Test fun `loadCommentResponses no internet connection exception`() = runTest { coEvery { interactor.getCommentsResponses(any(), any()) } throws UnknownHostException() @@ -82,8 +94,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.getCommentsResponses(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionResponsesUIState.Loading) } @@ -103,8 +115,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.getCommentsResponses(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionResponsesUIState.Loading) } @@ -126,7 +138,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.getCommentsResponses(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == true) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) @@ -148,7 +161,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.getCommentsResponses(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) @@ -171,7 +185,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.getCommentsResponses(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) @@ -198,7 +213,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 2) { interactor.getCommentsResponses(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) @@ -222,8 +238,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -244,8 +260,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -272,7 +288,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } @@ -300,7 +317,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentVoted(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } @@ -322,8 +340,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(noInternet == message?.message) + val message = captureUiMessage(viewModel) + assert(noInternet == (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -344,8 +362,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assert(somethingWrong == message?.message) + val message = captureUiMessage(viewModel) + assert(somethingWrong == (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -368,7 +386,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } @@ -394,7 +413,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.setCommentFlagged(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } @@ -417,8 +437,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - Assert.assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + Assert.assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -440,8 +460,11 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - Assert.assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + Assert.assertEquals( + somethingWrong, + (message.await() as? UIMessage.SnackBarMessage)?.message + ) } @Test @@ -463,7 +486,8 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - assert(viewModel.uiMessage.value != null) + val message = captureUiMessage(viewModel) + assert(message.await() != null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt index 9817ea242..6687d3400 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt @@ -22,7 +22,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.domain.model.Pagination import org.openedx.discussion.DiscussionMocks import org.openedx.discussion.domain.interactor.DiscussionInteractor @@ -32,6 +31,7 @@ import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DiscussionSearchThreadViewModelTest { @@ -51,8 +51,12 @@ class DiscussionSearchThreadViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong } @After @@ -60,6 +64,10 @@ class DiscussionSearchThreadViewModelTest { Dispatchers.resetMain() } + private fun DiscussionSearchThreadViewModel.lastUiMessage(): UIMessage? { + return uiMessage.replayCache.lastOrNull() + } + @Test fun `search empty query`() = runTest { val viewModel = DiscussionSearchThreadViewModel(interactor, resourceManager, notifier, "") @@ -71,7 +79,7 @@ class DiscussionSearchThreadViewModelTest { assert(uiState.data.isEmpty()) assert(uiState.count == 0) - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) } @Test @@ -84,7 +92,7 @@ class DiscussionSearchThreadViewModelTest { coVerify(exactly = 1) { interactor.searchThread(any(), any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as UIMessage.SnackBarMessage assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Loading) assert(message.message == noInternet) } @@ -99,7 +107,7 @@ class DiscussionSearchThreadViewModelTest { coVerify(exactly = 1) { interactor.searchThread(any(), any(), any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as UIMessage.SnackBarMessage assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Loading) assert(message.message == somethingWrong) } @@ -125,7 +133,7 @@ class DiscussionSearchThreadViewModelTest { coVerify(exactly = 1) { interactor.searchThread(any(), any(), any()) } assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Threads) - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) } @@ -159,7 +167,7 @@ class DiscussionSearchThreadViewModelTest { assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Threads) assert((viewModel.uiState.value as DiscussionSearchThreadUIState.Threads).data.size == 3) - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == false) } @@ -195,7 +203,7 @@ class DiscussionSearchThreadViewModelTest { assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Threads) assert((viewModel.uiState.value as DiscussionSearchThreadUIState.Threads).data.size == 2) - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.canLoadMore.value == true) } @@ -221,7 +229,7 @@ class DiscussionSearchThreadViewModelTest { assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Threads) assert((viewModel.uiState.value as DiscussionSearchThreadUIState.Threads).data.isEmpty()) - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == null) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt index d46df5e53..37800eb74 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt @@ -18,7 +18,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.discussion.DiscussionMocks import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.system.notifier.DiscussionNotifier @@ -26,6 +25,7 @@ import org.openedx.discussion.system.notifier.DiscussionThreadAdded import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DiscussionAddThreadViewModelTest { @@ -57,8 +57,12 @@ class DiscussionAddThreadViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong } @After @@ -67,6 +71,10 @@ class DiscussionAddThreadViewModelTest { clearAllMocks() } + private fun DiscussionAddThreadViewModel.lastUiMessage(): UIMessage? { + return uiMessage.replayCache.lastOrNull() + } + @Test fun `createThread no internet connection exception`() = runTest { val viewModel = DiscussionAddThreadViewModel(interactor, resourceManager, notifier, "") @@ -86,7 +94,7 @@ class DiscussionAddThreadViewModelTest { coVerify(exactly = 1) { interactor.createThread(any(), any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assert(noInternet == message?.message) assert(viewModel.newThread.value == null) assert(viewModel.isLoading.value == false) @@ -111,7 +119,7 @@ class DiscussionAddThreadViewModelTest { coVerify(exactly = 1) { interactor.createThread(any(), any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assert(somethingWrong == message?.message) assert(viewModel.newThread.value == null) assert(viewModel.isLoading.value == false) @@ -136,7 +144,7 @@ class DiscussionAddThreadViewModelTest { coVerify(exactly = 1) { interactor.createThread(any(), any(), any(), any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.newThread.value != null) assert(viewModel.isLoading.value == false) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt index ecb7e5f53..52e2bbdaa 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt @@ -24,7 +24,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.domain.model.Pagination import org.openedx.discussion.DiscussionMocks import org.openedx.discussion.domain.interactor.DiscussionInteractor @@ -37,6 +36,7 @@ import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DiscussionThreadsViewModelTest { @@ -65,8 +65,12 @@ class DiscussionThreadsViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong } @After @@ -75,6 +79,10 @@ class DiscussionThreadsViewModelTest { clearAllMocks() } + private fun DiscussionThreadsViewModel.lastUiMessage(): UIMessage? { + return uiMessage.replayCache.lastOrNull() + } + @Test fun `getThreadByType AllThreads no internet connection`() = runTest { coEvery { @@ -97,7 +105,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -118,7 +126,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -148,7 +156,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) } @@ -176,7 +184,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -205,7 +213,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -251,7 +259,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) } @@ -279,7 +287,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -300,7 +308,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = viewModel.lastUiMessage() as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Loading) @@ -330,7 +338,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) } @@ -420,7 +428,7 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 2) { interactor.getThreads(any(), any(), any(), any(), any()) } - assert(viewModel.uiMessage.value == null) + assert(viewModel.lastUiMessage() == null) assert(viewModel.isUpdating.value == false) assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) } diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index 3a180c7ab..ab4ea8cc2 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -23,7 +23,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.discussion.DiscussionMocks @@ -33,6 +32,7 @@ import org.openedx.discussion.presentation.DiscussionRouter import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class DiscussionTopicsViewModelTest { @@ -53,7 +53,9 @@ class DiscussionTopicsViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) coEvery { courseNotifier.send(any()) } returns Unit } diff --git a/downloads/src/main/java/org/openedx/downloads/presentation/download/DownloadsViewModel.kt b/downloads/src/main/java/org/openedx/downloads/presentation/download/DownloadsViewModel.kt index 24381a2a5..c0f17dcf7 100644 --- a/downloads/src/main/java/org/openedx/downloads/presentation/download/DownloadsViewModel.kt +++ b/downloads/src/main/java/org/openedx/downloads/presentation/download/DownloadsViewModel.kt @@ -5,18 +5,14 @@ import androidx.compose.material.icons.filled.School import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BlockType -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseStructure @@ -40,8 +36,6 @@ import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.downloads.domain.interactor.DownloadInteractor import org.openedx.downloads.presentation.DownloadsRouter -import org.openedx.foundation.extension.isInternetError -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil @@ -68,15 +62,13 @@ class DownloadsViewModel( workerController, coreAnalytics, downloadHelper, + resourceManager, ) { val apiHostUrl get() = config.getApiHostURL() private val _uiState = MutableStateFlow(DownloadsUIState()) val uiState: StateFlow = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow = _uiMessage.asSharedFlow() - private val courseBlockIds = mutableMapOf>() val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -207,13 +199,8 @@ class DownloadsViewModel( private fun emitErrorMessage(e: Throwable) { viewModelScope.launch { - val text = if (e.isInternetError()) { - R.string.core_error_no_connection - } else { - R.string.core_error_unknown_error - } - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(text)) + handleErrorUiMessage( + throwable = e, ) } } diff --git a/downloads/src/test/java/org/openedx/downloads/DownloadsViewModelTest.kt b/downloads/src/test/java/org/openedx/downloads/DownloadsViewModelTest.kt index 42506c7a3..3d080b589 100644 --- a/downloads/src/test/java/org/openedx/downloads/DownloadsViewModelTest.kt +++ b/downloads/src/test/java/org/openedx/downloads/DownloadsViewModelTest.kt @@ -24,7 +24,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.CoreMocks -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.DownloadCoursePreview @@ -45,6 +44,7 @@ import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR class DownloadsViewModelTest { @@ -87,8 +87,12 @@ class DownloadsViewModelTest { fun setUp() { Dispatchers.setMain(dispatcher) every { config.getApiHostURL() } returns "http://localhost:8000" - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns unknownError + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns unknownError every { networkConnection.isOnline() } returns true coEvery { interactor.getDownloadCoursesPreview(any()) } returns flow { diff --git a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt index 489695eb2..b65401dd2 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt @@ -23,6 +23,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -78,7 +79,7 @@ class AnothersProfileFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState - val uiMessage by viewModel.uiMessage + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) AnothersProfileScreen( windowSize = windowSize, diff --git a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt index 90559aa9b..16bbbe355 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt @@ -4,10 +4,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor @@ -15,16 +12,12 @@ class AnothersProfileViewModel( private val interactor: ProfileInteractor, private val resourceManager: ResourceManager, val username: String -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = mutableStateOf(AnothersProfileUIState.Loading) val uiState: State get() = _uiState - private val _uiMessage = mutableStateOf(null) - val uiMessage: State - get() = _uiMessage - init { getAccount(username) } @@ -36,13 +29,9 @@ class AnothersProfileViewModel( val account = interactor.getAccount(username) _uiState.value = AnothersProfileUIState.Data(account) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } 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 dcc31d04e..1bf4d10a3 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 @@ -23,6 +23,7 @@ import org.openedx.core.system.notifier.calendar.CalendarSynced import org.openedx.core.system.notifier.calendar.CalendarSyncing import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager import org.openedx.profile.presentation.ProfileRouter class CalendarViewModel( @@ -34,7 +35,8 @@ class CalendarViewModel( private val corePreferences: CorePreferences, private val profileRouter: ProfileRouter, private val networkConnection: NetworkConnection, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val calendarInitState: CalendarUIState get() = CalendarUIState( diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt index 015df8e2b..3a8ee86c1 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt @@ -1,21 +1,15 @@ package org.openedx.profile.presentation.calendar import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.worker.CalendarSyncScheduler -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager class CoursesToSyncViewModel( @@ -23,7 +17,7 @@ class CoursesToSyncViewModel( private val calendarPreferences: CalendarPreferences, private val calendarSyncScheduler: CalendarSyncScheduler, private val resourceManager: ResourceManager, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableStateFlow( CoursesToSyncUIState( @@ -34,10 +28,6 @@ class CoursesToSyncViewModel( ) ) - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - val uiState: StateFlow get() = _uiState.asStateFlow() @@ -69,10 +59,8 @@ class CoursesToSyncViewModel( _uiState.update { it.copy(coursesCalendarState = coursesCalendarState) } } catch (e: Exception) { e.printStackTrace() - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) + handleErrorUiMessage( + throwable = e, ) } } @@ -84,21 +72,9 @@ class CoursesToSyncViewModel( val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus) } } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString( - R.string.core_error_no_connection - ) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { _uiState.update { it.copy(isLoading = false) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt index 3d0cf94a8..976157f69 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt @@ -14,13 +14,15 @@ import org.openedx.core.system.CalendarManager import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.core.system.notifier.calendar.CalendarSyncDisabled import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager class DisableCalendarSyncDialogViewModel( private val calendarNotifier: CalendarNotifier, private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, private val calendarInteractor: CalendarInteractor, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _deletionState = MutableStateFlow(null) val deletionState: StateFlow = _deletionState.asStateFlow() diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt index 162f0d10b..d52cca4ff 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt @@ -96,10 +96,8 @@ class NewCalendarDialogFragment : DialogFragment() { val viewModel: NewCalendarDialogViewModel = koinViewModel() LaunchedEffect(Unit) { - viewModel.uiMessage.collect { message -> - if (message.isNotEmpty()) { - context.toastMessage(message) - } + viewModel.uiMessage.collect { uiMessage -> + context.toastMessage(uiMessage.message) } } @@ -115,9 +113,8 @@ class NewCalendarDialogFragment : DialogFragment() { val showLocalCalendarSection by viewModel.showLocalCalendarSection.collectAsState() NewCalendarDialog( - newCalendarDialogType = requireArguments().parcelable( - ARG_DIALOG_TYPE - ) + newCalendarDialogType = requireArguments() + .parcelable(ARG_DIALOG_TYPE) ?: NewCalendarDialogType.CREATE_NEW, googleCalendars = googleCalendars, showLocalCalendarSection = showLocalCalendarSection, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index eb95d1650..a98f74b44 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -20,7 +20,9 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.calendar.CalendarCreated import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager +import java.net.UnknownHostException class NewCalendarDialogViewModel( private val calendarManager: CalendarManager, @@ -29,12 +31,7 @@ class NewCalendarDialogViewModel( private val calendarInteractor: CalendarInteractor, private val networkConnection: NetworkConnection, private val resourceManager: ResourceManager, -) : BaseViewModel() { - - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - +) : BaseViewModel(resourceManager) { private val _isSuccess = MutableSharedFlow() val isSuccess: SharedFlow get() = _isSuccess.asSharedFlow() @@ -86,10 +83,14 @@ class NewCalendarDialogViewModel( } _isSuccess.emit(true) } else { - _uiMessage.emit(resourceManager.getString(R.string.core_error_unknown_error)) + handleErrorUiMessage( + throwable = null, + ) } } else { - _uiMessage.emit(resourceManager.getString(R.string.core_error_no_connection)) + handleErrorUiMessage( + throwable = UnknownHostException(), + ) } } } @@ -97,12 +98,22 @@ class NewCalendarDialogViewModel( fun syncWithGoogleCalendar(calendarId: Long) { viewModelScope.launch { if (!networkConnection.isOnline()) { - _uiMessage.emit(resourceManager.getString(R.string.core_error_no_connection)) + sendMessage( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) + ) return@launch } if (!calendarManager.isCalendarExist(calendarId)) { - _uiMessage.emit(resourceManager.getString(R.string.core_error_unknown_error)) + sendMessage( + UIMessage.SnackBarMessage( + resourceManager.getString( + R.string.core_error_unknown_error + ) + ) + ) return@launch } diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt index 770e67b40..fa0bbdd25 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt @@ -98,7 +98,7 @@ class DeleteProfileFragment : Fragment() { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.observeAsState(DeleteProfileFragmentUIState.Initial) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val logoutSuccess by logoutViewModel.successLogout.collectAsState(false) DeleteProfileScreen( diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt index 8ab22c87e..648efe291 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt @@ -24,16 +24,12 @@ class DeleteProfileViewModel( private val notifier: ProfileNotifier, private val validator: Validator, private val analytics: ProfileAnalytics, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = MutableLiveData() - val uiMessage: LiveData - get() = _uiMessage - fun deleteProfile(password: String) { logDeleteProfileClickedEvent() if (!validator.isPasswordValid(password)) { @@ -52,12 +48,16 @@ class DeleteProfileViewModel( notifier.send(AccountDeactivated()) } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + handleErrorUiMessage( + throwable = e, + ) _uiState.value = DeleteProfileFragmentUIState.Initial } else if (e is EdxError.UserNotActiveException) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_user_not_active)) + sendMessage( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_user_not_active) + ) + ) _uiState.value = DeleteProfileFragmentUIState.Initial } else { _uiState.value = diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt index abc042aff..b95663681 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt @@ -58,6 +58,7 @@ import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateMapOf @@ -172,7 +173,7 @@ class EditProfileFragment : Fragment() { isLimited = viewModel.isLimitedProfile ) ) - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val selectedImageUri by viewModel.selectedImageUri.observeAsState() val isImageDeleted by viewModel.deleteImage.observeAsState(false) val leaveDialog by viewModel.showLeaveDialog.observeAsState(false) diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt index dd8781cf9..8ce70cebc 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt @@ -5,11 +5,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.openedx.core.R import org.openedx.core.config.Config -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account @@ -27,16 +24,12 @@ class EditProfileViewModel( private val analytics: ProfileAnalytics, val config: Config, account: Account, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState - private val _uiMessage = MutableLiveData() - val uiMessage: LiveData - get() = _uiMessage - var account = account private set @@ -93,13 +86,9 @@ class EditProfileViewModel( _selectedImageUri.value = null } catch (e: Exception) { _uiState.value = EditProfileUIState(account.copy(), isLimited = isLimitedProfile) - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } @@ -118,13 +107,9 @@ class EditProfileViewModel( sendAccountUpdated() } catch (e: Exception) { _uiState.value = EditProfileUIState(account.copy(), isLimited = isLimitedProfile) - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt index d8297d1bd..56bba040f 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt @@ -3,17 +3,11 @@ package org.openedx.profile.presentation.manageaccount import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.openedx.core.R -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.presentation.ProfileAnalytics @@ -29,15 +23,11 @@ class ManageAccountViewModel( private val notifier: ProfileNotifier, private val analytics: ProfileAnalytics, val profileRouter: ProfileRouter -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState: MutableStateFlow = MutableStateFlow(ManageAccountUIState.Loading) internal val uiState: StateFlow = _uiState.asStateFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - private val _isUpdating = MutableStateFlow(false) val isUpdating: StateFlow get() = _isUpdating.asStateFlow() @@ -74,19 +64,9 @@ class ManageAccountViewModel( account = account ) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { _isUpdating.value = false } diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt index 581bdc63f..6940055ec 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt @@ -34,7 +34,7 @@ class ProfileFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() val uiState by viewModel.uiState.collectAsState() - val uiMessage by viewModel.uiMessage.observeAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val refreshing by viewModel.isUpdating.observeAsState(false) ProfileView( diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt index b2d4ccb4e..38c681bb5 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt @@ -9,10 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.openedx.core.R -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.presentation.ProfileAnalytics @@ -28,15 +25,11 @@ class ProfileViewModel( private val notifier: ProfileNotifier, private val analytics: ProfileAnalytics, val profileRouter: ProfileRouter -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState: MutableStateFlow = MutableStateFlow(ProfileUIState.Loading) internal val uiState: StateFlow = _uiState.asStateFlow() - private val _uiMessage = MutableLiveData() - val uiMessage: LiveData - get() = _uiMessage - private val _isUpdating = MutableLiveData() val isUpdating: LiveData get() = _isUpdating @@ -73,13 +66,9 @@ class ProfileViewModel( account = account ) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) - } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - } + handleErrorUiMessage( + throwable = e, + ) } finally { _isUpdating.value = false } diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt index c21f72df3..4c418f705 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt @@ -23,9 +23,7 @@ import org.openedx.core.system.AppCookieManager import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.core.utils.EmailUtil -import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel -import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Configuration @@ -48,7 +46,7 @@ class SettingsViewModel( private val calendarRouter: CalendarRouter, private val appNotifier: AppNotifier, private val profileNotifier: ProfileNotifier, -) : BaseViewModel() { +) : BaseViewModel(resourceManager) { private val _uiState: MutableStateFlow = MutableStateFlow(SettingsUIState.Data(configuration)) internal val uiState: StateFlow = _uiState.asStateFlow() @@ -57,10 +55,6 @@ class SettingsViewModel( val successLogout: SharedFlow get() = _successLogout.asSharedFlow() - private val _uiMessage = MutableSharedFlow() - val uiMessage: SharedFlow - get() = _uiMessage.asSharedFlow() - val isLogistrationEnabled get() = config.isPreLoginExperienceEnabled() private val configuration @@ -90,19 +84,9 @@ class SettingsViewModel( } ) } catch (e: Exception) { - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) - ) - ) - } + handleErrorUiMessage( + throwable = e, + ) } finally { cookieManager.clearWebViewCookie() appNotifier.send(LogoutEvent(false)) diff --git a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt index 670447ddb..0b1778ded 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt @@ -13,6 +13,7 @@ import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.core.system.notifier.VideoNotifier import org.openedx.core.system.notifier.VideoQualityChanged import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey @@ -23,7 +24,8 @@ class VideoSettingsViewModel( private val notifier: VideoNotifier, private val analytics: ProfileAnalytics, private val router: ProfileRouter, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _videoSettings = MutableLiveData() val videoSettings: LiveData diff --git a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt index 131ec237b..e5d5784e9 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt @@ -19,9 +19,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.ProfileMocks import org.openedx.profile.domain.interactor.ProfileInteractor @@ -30,6 +30,7 @@ import org.openedx.profile.system.notifier.account.AccountUpdated import org.openedx.profile.system.notifier.profile.ProfileNotifier import java.io.File import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class EditProfileViewModelTest { @@ -53,8 +54,12 @@ class EditProfileViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { analytics.logScreenEvent(any(), any()) } returns Unit } @@ -80,8 +85,8 @@ class EditProfileViewModelTest { coVerify(exactly = 1) { interactor.updateAccount(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value?.isUpdating == false) } @@ -103,8 +108,8 @@ class EditProfileViewModelTest { coVerify(exactly = 1) { interactor.updateAccount(any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.uiState.value?.isUpdating == false) } @@ -128,7 +133,8 @@ class EditProfileViewModelTest { verify { analytics.logEvent(any(), any()) } coVerify(exactly = 1) { interactor.updateAccount(any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) assert(viewModel.uiState.value?.isUpdating == false) } @@ -153,8 +159,8 @@ class EditProfileViewModelTest { coVerify(exactly = 0) { interactor.updateAccount(any()) } coVerify(exactly = 1) { interactor.setProfileImage(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(noInternet, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.selectedImageUri.value == null) assert(viewModel.uiState.value?.isUpdating == false) } @@ -180,8 +186,8 @@ class EditProfileViewModelTest { coVerify(exactly = 0) { interactor.updateAccount(any()) } coVerify(exactly = 1) { interactor.setProfileImage(any(), any()) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(somethingWrong, message?.message) + val message = captureUiMessage(viewModel) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.selectedImageUri.value == null) assert(viewModel.uiState.value?.isUpdating == false) } @@ -210,7 +216,8 @@ class EditProfileViewModelTest { coVerify(exactly = 1) { interactor.updateAccount(any()) } coVerify(exactly = 1) { interactor.setProfileImage(any(), any()) } - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assertEquals(null, (message.await() as? UIMessage.SnackBarMessage)?.message) assert(viewModel.selectedImageUri.value == null) assert(viewModel.uiState.value?.isUpdating == false) } diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt index fa0b67bdc..f0d24dd0e 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt @@ -18,8 +18,8 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.ProfileMocks import org.openedx.profile.domain.interactor.ProfileInteractor @@ -27,6 +27,7 @@ import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.anothersaccount.AnothersProfileUIState import org.openedx.profile.presentation.anothersaccount.AnothersProfileViewModel import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class AnothersProfileViewModelTest { @@ -46,8 +47,12 @@ class AnothersProfileViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong } @After @@ -67,9 +72,9 @@ class AnothersProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount(username) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is AnothersProfileUIState.Loading) - assertEquals(noInternet, message?.message) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -84,9 +89,9 @@ class AnothersProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount(username) } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is AnothersProfileUIState.Loading) - assertEquals(somethingWrong, message?.message) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -104,6 +109,7 @@ class AnothersProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount(username) } assert(viewModel.uiState.value is AnothersProfileUIState.Data) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) } } diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/CalendarViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/CalendarViewModelTest.kt index 7fd8977a1..80dfac5bb 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/CalendarViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/CalendarViewModelTest.kt @@ -27,6 +27,7 @@ import org.openedx.core.system.notifier.calendar.CalendarCreated import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.core.system.notifier.calendar.CalendarSynced import org.openedx.core.worker.CalendarSyncScheduler +import org.openedx.foundation.system.ResourceManager import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.presentation.calendar.CalendarViewModel @@ -43,6 +44,7 @@ class CalendarViewModelTest { private val calendarInteractor = mockk(relaxed = true) private val corePreferences = mockk(relaxed = true) private val profileRouter = mockk() + private val resourceManager = mockk() private val networkConnection = mockk() private val permissionLauncher = mockk>>() private val fragmentManager = mockk() @@ -59,7 +61,8 @@ class CalendarViewModelTest { calendarInteractor = calendarInteractor, corePreferences = corePreferences, profileRouter = profileRouter, - networkConnection = networkConnection + networkConnection = networkConnection, + resourceManager = resourceManager, ) } @@ -111,7 +114,8 @@ class CalendarViewModelTest { calendarInteractor, corePreferences, profileRouter, - networkConnection + networkConnection, + resourceManager, ) assertEquals(CalendarSyncState.OFFLINE, viewModel.uiState.value.calendarSyncState) @@ -129,7 +133,8 @@ class CalendarViewModelTest { calendarInteractor, corePreferences, profileRouter, - networkConnection + networkConnection, + resourceManager, ) assertEquals(CalendarSyncState.SYNCED, viewModel.uiState.value.calendarSyncState) @@ -150,7 +155,8 @@ class CalendarViewModelTest { calendarInteractor, corePreferences, profileRouter, - networkConnection + networkConnection, + resourceManager, ) assertTrue(viewModel.uiState.value.isCalendarExist) diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt index 6ffcf1355..2b1cdc077 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt @@ -23,10 +23,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import org.openedx.core.R import org.openedx.core.config.Config import org.openedx.core.domain.model.AgreementUrls import org.openedx.foundation.presentation.UIMessage +import org.openedx.foundation.presentation.captureUiMessage import org.openedx.foundation.system.ResourceManager import org.openedx.profile.ProfileMocks import org.openedx.profile.domain.interactor.ProfileInteractor @@ -35,6 +35,7 @@ import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.notifier.account.AccountUpdated import org.openedx.profile.system.notifier.profile.ProfileNotifier import java.net.UnknownHostException +import org.openedx.foundation.R as foundationR @OptIn(ExperimentalCoroutinesApi::class) class ProfileViewModelTest { @@ -57,8 +58,12 @@ class ProfileViewModelTest { @Before fun setUp() { Dispatchers.setMain(dispatcher) - every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet - every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { + resourceManager.getString(foundationR.string.foundation_error_no_connection) + } returns noInternet + every { + resourceManager.getString(foundationR.string.foundation_error_unknown_error) + } returns somethingWrong every { config.isPreLoginExperienceEnabled() } returns false every { config.getFeedbackEmailAddress() } returns "" every { config.getAgreement(Locale.current.language) } returns AgreementUrls() @@ -85,9 +90,9 @@ class ProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is ProfileUIState.Loading) - assertEquals(noInternet, message?.message) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -107,9 +112,9 @@ class ProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is ProfileUIState.Data) - assertEquals(noInternet, message?.message) + assertEquals(noInternet, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -127,9 +132,9 @@ class ProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount() } - val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage + val message = captureUiMessage(viewModel) assert(viewModel.uiState.value is ProfileUIState.Loading) - assertEquals(somethingWrong, message?.message) + assertEquals(somethingWrong, (message.await() as? UIMessage.SnackBarMessage)?.message) } @Test @@ -150,7 +155,8 @@ class ProfileViewModelTest { coVerify(exactly = 1) { interactor.getAccount() } assert(viewModel.uiState.value is ProfileUIState.Data) - assert(viewModel.uiMessage.value == null) + val message = captureUiMessage(viewModel) + assert(message.await() == null) } @Test diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt index dbbbdda2f..986a58fcb 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.fragment.app.FragmentManager import org.openedx.core.presentation.global.AppData import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.foundation.system.ResourceManager import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.data.storage.WhatsNewPreferences @@ -21,7 +22,8 @@ class WhatsNewViewModel( private val router: WhatsNewRouter, private val preferencesManager: WhatsNewPreferences, private val appData: AppData, -) : BaseViewModel() { + private val resourceManager: ResourceManager, +) : BaseViewModel(resourceManager) { private val _whatsNewItem = mutableStateOf(null) val whatsNewItem: State diff --git a/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt b/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt index d99555c49..015fa7490 100644 --- a/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt +++ b/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt @@ -6,6 +6,7 @@ import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Test import org.openedx.core.presentation.global.AppData +import org.openedx.foundation.system.ResourceManager import org.openedx.whatsnew.data.storage.WhatsNewPreferences import org.openedx.whatsnew.domain.model.WhatsNewItem import org.openedx.whatsnew.presentation.WhatsNewAnalytics @@ -18,6 +19,7 @@ class WhatsNewViewModelTest { private val router = mockk() private val preferencesManager = mockk() private val appData = mockk() + private val resourceManager = mockk() private val whatsNewItem = WhatsNewItem( version = "1.0.0", @@ -35,7 +37,8 @@ class WhatsNewViewModelTest { analytics, router, preferencesManager, - appData + appData, + resourceManager ) verify(exactly = 1) { whatsNewManager.getNewestData() } From b4556a75552efec5c961f86c0475fc04d5936b04 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 14 Feb 2025 20:20:17 +0200 Subject: [PATCH 16/28] feat: dates tab UI --- app/src/main/res/menu/bottom_view_menu.xml | 0 .../presentation/dates/DueDateCategory.kt | 31 +++++++++++++++++++ dates/src/main/res/layout/fragment_dates.xml | 6 ++++ 3 files changed, 37 insertions(+) create mode 100644 app/src/main/res/menu/bottom_view_menu.xml create mode 100644 dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt create mode 100644 dates/src/main/res/layout/fragment_dates.xml diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml new file mode 100644 index 000000000..e69de29bb diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt new file mode 100644 index 000000000..78ebda298 --- /dev/null +++ b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt @@ -0,0 +1,31 @@ +package org.openedx.dates.presentation.dates + +import androidx.annotation.StringRes +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import org.openedx.core.ui.theme.appColors +import org.openedx.dates.R + +enum class DueDateCategory( + @StringRes + val label: Int, +) { + PAST_DUE(R.string.dates_category_past_due), + TODAY(R.string.dates_category_today), + THIS_WEEK(R.string.dates_category_this_week), + NEXT_WEEK(R.string.dates_category_next_week), + UPCOMING(R.string.dates_category_upcoming); + + val color: Color + @Composable + get() { + return when (this) { + PAST_DUE -> MaterialTheme.appColors.warning + TODAY -> MaterialTheme.appColors.info + THIS_WEEK -> MaterialTheme.appColors.textPrimaryVariant + NEXT_WEEK -> MaterialTheme.appColors.textFieldBorder + UPCOMING -> MaterialTheme.appColors.divider + } + } +} diff --git a/dates/src/main/res/layout/fragment_dates.xml b/dates/src/main/res/layout/fragment_dates.xml new file mode 100644 index 000000000..77d9ef65f --- /dev/null +++ b/dates/src/main/res/layout/fragment_dates.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From e30651531b1a1b847f9829278f42bfca837d1d72 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 17 Feb 2025 12:27:53 +0200 Subject: [PATCH 17/28] feat: added config flag for enabling/disabling dates screen --- core/src/main/java/org/openedx/core/config/DatesConfig.kt | 8 ++++++++ default_config/dev/config.yaml | 3 +++ 2 files changed, 11 insertions(+) create mode 100644 core/src/main/java/org/openedx/core/config/DatesConfig.kt diff --git a/core/src/main/java/org/openedx/core/config/DatesConfig.kt b/core/src/main/java/org/openedx/core/config/DatesConfig.kt new file mode 100644 index 000000000..0e48a5ed5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/DatesConfig.kt @@ -0,0 +1,8 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class DatesConfig( + @SerializedName("ENABLED") + val isEnabled: Boolean = true, +) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 952e041de..e6ab8bce2 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,6 +31,9 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' +DATES: + ENABLED: true + FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false From bb356f4ca2eebd91b46e9e033ffaac9610786f3b Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 17 Mar 2025 20:42:36 +0200 Subject: [PATCH 18/28] feat: paging and caching --- .../dates/data/storage/CourseDateEntity.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt new file mode 100644 index 000000000..558da6870 --- /dev/null +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -0,0 +1,53 @@ +package org.openedx.dates.data.storage + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.openedx.core.data.model.CourseDate +import org.openedx.core.utils.TimeUtils +import org.openedx.core.domain.model.CourseDate as DomainCourseDate + +@Entity(tableName = "course_date_table") +data class CourseDateEntity( + @PrimaryKey + @ColumnInfo("assignmentBlockId") + val assignmentBlockId: String, + @ColumnInfo("courseId") + val courseId: String, + @ColumnInfo("dueDate") + val dueDate: String?, + @ColumnInfo("assignmentTitle") + val assignmentTitle: String?, + @ColumnInfo("learnerHasAccess") + val learnerHasAccess: Boolean?, + @ColumnInfo("courseName") + val courseName: String?, +) { + + fun mapToDomain(): DomainCourseDate? { + val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") + return DomainCourseDate( + courseId = courseId, + assignmentBlockId = assignmentBlockId, + dueDate = dueDate ?: return null, + assignmentTitle = assignmentTitle ?: "", + learnerHasAccess = learnerHasAccess ?: false, + courseName = courseName ?: "" + ) + } + + companion object { + fun createFrom(courseDate: CourseDate): CourseDateEntity { + with(courseDate) { + return CourseDateEntity( + courseId = courseId, + assignmentBlockId = assignmentBlockId, + dueDate = dueDate, + assignmentTitle = assignmentTitle, + learnerHasAccess = learnerHasAccess, + courseName = courseName + ) + } + } + } +} From a9c14d80a55b375a45168eb6a827ab2b1ae1eee7 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 18 Mar 2025 13:09:46 +0200 Subject: [PATCH 19/28] feat: navigating to block --- .../openedx/dates/presentation/dates/DueDateCategory.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt index 78ebda298..4cd305a56 100644 --- a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt +++ b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt @@ -11,11 +11,11 @@ enum class DueDateCategory( @StringRes val label: Int, ) { - PAST_DUE(R.string.dates_category_past_due), - TODAY(R.string.dates_category_today), - THIS_WEEK(R.string.dates_category_this_week), + UPCOMING(R.string.dates_category_upcoming), NEXT_WEEK(R.string.dates_category_next_week), - UPCOMING(R.string.dates_category_upcoming); + THIS_WEEK(R.string.dates_category_this_week), + TODAY(R.string.dates_category_today), + PAST_DUE(R.string.dates_category_past_due); val color: Color @Composable From c8246fb3b355d123d145e6d175ff4ccd8b3cf9dc Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 17:23:05 +0200 Subject: [PATCH 20/28] feat: reuse dates UI from CourseDatesScreen --- .../presentation/dates/DueDateCategory.kt | 31 ------------------- default_config/dev/config.yaml | 2 +- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt diff --git a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt b/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt deleted file mode 100644 index 4cd305a56..000000000 --- a/dates/src/main/java/org/openedx/dates/presentation/dates/DueDateCategory.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.openedx.dates.presentation.dates - -import androidx.annotation.StringRes -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import org.openedx.core.ui.theme.appColors -import org.openedx.dates.R - -enum class DueDateCategory( - @StringRes - val label: Int, -) { - UPCOMING(R.string.dates_category_upcoming), - NEXT_WEEK(R.string.dates_category_next_week), - THIS_WEEK(R.string.dates_category_this_week), - TODAY(R.string.dates_category_today), - PAST_DUE(R.string.dates_category_past_due); - - val color: Color - @Composable - get() { - return when (this) { - PAST_DUE -> MaterialTheme.appColors.warning - TODAY -> MaterialTheme.appColors.info - THIS_WEEK -> MaterialTheme.appColors.textPrimaryVariant - NEXT_WEEK -> MaterialTheme.appColors.textFieldBorder - UPCOMING -> MaterialTheme.appColors.divider - } - } -} diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index e6ab8bce2..ac06ef7ba 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,7 +31,7 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -DATES: +APP_LEVEL_DATES: ENABLED: true FIREBASE: From 79fe2e9f5d77c1e03e8b2b05e7d6d9781cec8835 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 17:54:38 +0200 Subject: [PATCH 21/28] feat: shift due date card --- .../java/org/openedx/dates/data/storage/CourseDateEntity.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt index 558da6870..ec751d1ee 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -20,6 +20,8 @@ data class CourseDateEntity( val assignmentTitle: String?, @ColumnInfo("learnerHasAccess") val learnerHasAccess: Boolean?, + @ColumnInfo("relative") + val relative: Boolean?, @ColumnInfo("courseName") val courseName: String?, ) { @@ -32,6 +34,7 @@ data class CourseDateEntity( dueDate = dueDate ?: return null, assignmentTitle = assignmentTitle ?: "", learnerHasAccess = learnerHasAccess ?: false, + relative = relative ?: false, courseName = courseName ?: "" ) } @@ -45,6 +48,7 @@ data class CourseDateEntity( dueDate = dueDate, assignmentTitle = assignmentTitle, learnerHasAccess = learnerHasAccess, + relative = relative, courseName = courseName ) } From a3fea90273721cb4e4764fdd0fb5afd069573220 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 20 Mar 2025 18:11:51 +0200 Subject: [PATCH 22/28] feat: shift due date request --- .../java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt new file mode 100644 index 000000000..df6749f24 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt @@ -0,0 +1,7 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName + +data class ShiftDueDatesBody( + @SerializedName("course_keys") val courseKeys: List +) \ No newline at end of file From 0c3ba327f21971638e95630b0930d828b9a12e2d Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 21 Mar 2025 12:42:48 +0200 Subject: [PATCH 23/28] fix: changes according detekt warnings --- .../main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt index df6749f24..63e66363d 100644 --- a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt +++ b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt @@ -4,4 +4,4 @@ import com.google.gson.annotations.SerializedName data class ShiftDueDatesBody( @SerializedName("course_keys") val courseKeys: List -) \ No newline at end of file +) From 3e81efd4d5e9c1b16c0cb888c523470d62740299 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 25 Mar 2025 13:40:28 +0200 Subject: [PATCH 24/28] feat: pagination --- .../dates/data/storage/CourseDateEntity.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt index ec751d1ee..de7705d54 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt @@ -9,20 +9,22 @@ import org.openedx.core.domain.model.CourseDate as DomainCourseDate @Entity(tableName = "course_date_table") data class CourseDateEntity( - @PrimaryKey - @ColumnInfo("assignmentBlockId") - val assignmentBlockId: String, - @ColumnInfo("courseId") + @PrimaryKey(autoGenerate = true) + @ColumnInfo("course_date_id") + val id: Int, + @ColumnInfo("course_date_first_component_block_id") + val firstComponentBlockId: String?, + @ColumnInfo("course_date_courseId") val courseId: String, - @ColumnInfo("dueDate") + @ColumnInfo("course_date_dueDate") val dueDate: String?, - @ColumnInfo("assignmentTitle") + @ColumnInfo("course_date_assignmentTitle") val assignmentTitle: String?, - @ColumnInfo("learnerHasAccess") + @ColumnInfo("course_date_learnerHasAccess") val learnerHasAccess: Boolean?, - @ColumnInfo("relative") + @ColumnInfo("course_date_relative") val relative: Boolean?, - @ColumnInfo("courseName") + @ColumnInfo("course_date_courseName") val courseName: String?, ) { @@ -30,7 +32,7 @@ data class CourseDateEntity( val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") return DomainCourseDate( courseId = courseId, - assignmentBlockId = assignmentBlockId, + firstComponentBlockId = firstComponentBlockId ?: "", dueDate = dueDate ?: return null, assignmentTitle = assignmentTitle ?: "", learnerHasAccess = learnerHasAccess ?: false, @@ -43,8 +45,9 @@ data class CourseDateEntity( fun createFrom(courseDate: CourseDate): CourseDateEntity { with(courseDate) { return CourseDateEntity( + id = 0, courseId = courseId, - assignmentBlockId = assignmentBlockId, + firstComponentBlockId = firstComponentBlockId, dueDate = dueDate, assignmentTitle = assignmentTitle, learnerHasAccess = learnerHasAccess, From a448954e8e1735d98e3dd141e15a302a2bc8910a Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 27 Mar 2025 18:12:09 +0200 Subject: [PATCH 25/28] fix: pagination bugs --- .../java/org/openedx/core/data/model/ShiftDueDatesBody.kt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt diff --git a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt b/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt deleted file mode 100644 index 63e66363d..000000000 --- a/core/src/main/java/org/openedx/core/data/model/ShiftDueDatesBody.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.openedx.core.data.model - -import com.google.gson.annotations.SerializedName - -data class ShiftDueDatesBody( - @SerializedName("course_keys") val courseKeys: List -) From 0c309314aa5207bea9adb10321987a151076a1b1 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 31 Mar 2025 16:53:15 +0300 Subject: [PATCH 26/28] feat: cache-first logic --- app/src/main/res/menu/bottom_view_menu.xml | 0 .../org/openedx/core/config/DatesConfig.kt | 8 --- .../core/data/model/CourseDatesResponse.kt | 4 +- .../model/room/CourseDatesResponseEntity.kt | 51 ++++++++++++++++--- .../course/data/storage/CourseConverter.kt | 13 +++++ default_config/dev/config.yaml | 3 -- 6 files changed, 60 insertions(+), 19 deletions(-) delete mode 100644 app/src/main/res/menu/bottom_view_menu.xml delete mode 100644 core/src/main/java/org/openedx/core/config/DatesConfig.kt rename dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt => core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt (54%) diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/src/main/java/org/openedx/core/config/DatesConfig.kt b/core/src/main/java/org/openedx/core/config/DatesConfig.kt deleted file mode 100644 index 0e48a5ed5..000000000 --- a/core/src/main/java/org/openedx/core/config/DatesConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.openedx.core.config - -import com.google.gson.annotations.SerializedName - -data class DatesConfig( - @SerializedName("ENABLED") - val isEnabled: Boolean = true, -) diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt index c86500671..28a1b28dd 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt @@ -50,7 +50,9 @@ data class CourseDatesResponse( count = count, next = next, previous = previous, - results = results.mapNotNull { it.mapToDomain() } + results = results + .mapNotNull { it.mapToDomain() } + .sortedBy { it.dueDate } ) } } diff --git a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt similarity index 54% rename from dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt rename to core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt index de7705d54..5231a5604 100644 --- a/dates/src/main/java/org/openedx/dates/data/storage/CourseDateEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt @@ -1,17 +1,55 @@ -package org.openedx.dates.data.storage +package org.openedx.core.data.model.room import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import org.openedx.core.data.model.CourseDate +import org.openedx.core.data.model.CourseDatesResponse import org.openedx.core.utils.TimeUtils import org.openedx.core.domain.model.CourseDate as DomainCourseDate +import org.openedx.core.domain.model.CourseDatesResponse as DomainCourseDatesResponse -@Entity(tableName = "course_date_table") -data class CourseDateEntity( +@Entity(tableName = "course_dates_response_table") +data class CourseDatesResponseEntity( @PrimaryKey(autoGenerate = true) - @ColumnInfo("course_date_id") + @ColumnInfo("course_date_response_id") val id: Int, + @ColumnInfo("course_date_response_count") + val count: Int, + @ColumnInfo("course_date_response_next") + val next: String?, + @ColumnInfo("course_date_response_previous") + val previous: String?, + @ColumnInfo("course_date_response_results") + val results: List +) { + fun mapToDomain(): DomainCourseDatesResponse { + return DomainCourseDatesResponse( + count = count, + next = next, + previous = previous, + results = results + .mapNotNull { it.mapToDomain() } + .sortedBy { it.dueDate } + ) + } + + companion object { + fun createFrom(courseDatesResponse: CourseDatesResponse): CourseDatesResponseEntity { + with(courseDatesResponse) { + return CourseDatesResponseEntity( + id = 0, + count = count, + next = next, + previous = previous, + results = results.map { CourseDateDB.createFrom(it) } + ) + } + } + } +} + +data class CourseDateDB( @ColumnInfo("course_date_first_component_block_id") val firstComponentBlockId: String?, @ColumnInfo("course_date_courseId") @@ -42,10 +80,9 @@ data class CourseDateEntity( } companion object { - fun createFrom(courseDate: CourseDate): CourseDateEntity { + fun createFrom(courseDate: CourseDate): CourseDateDB { with(courseDate) { - return CourseDateEntity( - id = 0, + return CourseDateDB( courseId = courseId, firstComponentBlockId = firstComponentBlockId, dueDate = dueDate, diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index b49a806e6..68829efd2 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -4,6 +4,7 @@ import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import org.openedx.core.data.model.room.BlockDb +import org.openedx.core.data.model.room.CourseDateDB import org.openedx.core.data.model.room.GradingPolicyDb import org.openedx.core.data.model.room.SectionScoreDb import org.openedx.core.data.model.room.discovery.CourseDateBlockDb @@ -83,4 +84,16 @@ class CourseConverter { @TypeConverter fun toGradeRangeMap(value: String): Map = Gson().fromJson(value, object : TypeToken>() {}.type) + + @TypeConverter + fun fromListOfCourseDateDB(value: List): String { + val json = Gson().toJson(value) + return json.toString() + } + + @TypeConverter + fun toListOfCourseDateDB(value: String): List { + val type = genericType>() + return Gson().fromJson(value, type) + } } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index ac06ef7ba..952e041de 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -31,9 +31,6 @@ PROGRAM: DASHBOARD: TYPE: 'gallery' -APP_LEVEL_DATES: - ENABLED: true - FIREBASE: ENABLED: false CLOUD_MESSAGING_ENABLED: false From 0c0f8429cc655ba4fe799307314de3fdd1d1ff64 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 2 Apr 2025 12:52:13 +0300 Subject: [PATCH 27/28] fix: changes according code review --- .../core/data/model/CourseDatesResponse.kt | 4 +- .../model/room/CourseDatesResponseEntity.kt | 97 ------------------- dates/src/main/res/layout/fragment_dates.xml | 6 -- 3 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt delete mode 100644 dates/src/main/res/layout/fragment_dates.xml diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt index 28a1b28dd..c86500671 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDatesResponse.kt @@ -50,9 +50,7 @@ data class CourseDatesResponse( count = count, next = next, previous = previous, - results = results - .mapNotNull { it.mapToDomain() } - .sortedBy { it.dueDate } + results = results.mapNotNull { it.mapToDomain() } ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt deleted file mode 100644 index 5231a5604..000000000 --- a/core/src/main/java/org/openedx/core/data/model/room/CourseDatesResponseEntity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.openedx.core.data.model.room - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import org.openedx.core.data.model.CourseDate -import org.openedx.core.data.model.CourseDatesResponse -import org.openedx.core.utils.TimeUtils -import org.openedx.core.domain.model.CourseDate as DomainCourseDate -import org.openedx.core.domain.model.CourseDatesResponse as DomainCourseDatesResponse - -@Entity(tableName = "course_dates_response_table") -data class CourseDatesResponseEntity( - @PrimaryKey(autoGenerate = true) - @ColumnInfo("course_date_response_id") - val id: Int, - @ColumnInfo("course_date_response_count") - val count: Int, - @ColumnInfo("course_date_response_next") - val next: String?, - @ColumnInfo("course_date_response_previous") - val previous: String?, - @ColumnInfo("course_date_response_results") - val results: List -) { - fun mapToDomain(): DomainCourseDatesResponse { - return DomainCourseDatesResponse( - count = count, - next = next, - previous = previous, - results = results - .mapNotNull { it.mapToDomain() } - .sortedBy { it.dueDate } - ) - } - - companion object { - fun createFrom(courseDatesResponse: CourseDatesResponse): CourseDatesResponseEntity { - with(courseDatesResponse) { - return CourseDatesResponseEntity( - id = 0, - count = count, - next = next, - previous = previous, - results = results.map { CourseDateDB.createFrom(it) } - ) - } - } - } -} - -data class CourseDateDB( - @ColumnInfo("course_date_first_component_block_id") - val firstComponentBlockId: String?, - @ColumnInfo("course_date_courseId") - val courseId: String, - @ColumnInfo("course_date_dueDate") - val dueDate: String?, - @ColumnInfo("course_date_assignmentTitle") - val assignmentTitle: String?, - @ColumnInfo("course_date_learnerHasAccess") - val learnerHasAccess: Boolean?, - @ColumnInfo("course_date_relative") - val relative: Boolean?, - @ColumnInfo("course_date_courseName") - val courseName: String?, -) { - - fun mapToDomain(): DomainCourseDate? { - val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "") - return DomainCourseDate( - courseId = courseId, - firstComponentBlockId = firstComponentBlockId ?: "", - dueDate = dueDate ?: return null, - assignmentTitle = assignmentTitle ?: "", - learnerHasAccess = learnerHasAccess ?: false, - relative = relative ?: false, - courseName = courseName ?: "" - ) - } - - companion object { - fun createFrom(courseDate: CourseDate): CourseDateDB { - with(courseDate) { - return CourseDateDB( - courseId = courseId, - firstComponentBlockId = firstComponentBlockId, - dueDate = dueDate, - assignmentTitle = assignmentTitle, - learnerHasAccess = learnerHasAccess, - relative = relative, - courseName = courseName - ) - } - } - } -} diff --git a/dates/src/main/res/layout/fragment_dates.xml b/dates/src/main/res/layout/fragment_dates.xml deleted file mode 100644 index 77d9ef65f..000000000 --- a/dates/src/main/res/layout/fragment_dates.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From fa7ea9724e21b648d9a6e5eac99e39131602b7eb Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 16 Apr 2025 11:14:08 +0300 Subject: [PATCH 28/28] feat: according designer feedback --- .../openedx/course/data/storage/CourseConverter.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index 68829efd2..b49a806e6 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -4,7 +4,6 @@ import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import org.openedx.core.data.model.room.BlockDb -import org.openedx.core.data.model.room.CourseDateDB import org.openedx.core.data.model.room.GradingPolicyDb import org.openedx.core.data.model.room.SectionScoreDb import org.openedx.core.data.model.room.discovery.CourseDateBlockDb @@ -84,16 +83,4 @@ class CourseConverter { @TypeConverter fun toGradeRangeMap(value: String): Map = Gson().fromJson(value, object : TypeToken>() {}.type) - - @TypeConverter - fun fromListOfCourseDateDB(value: List): String { - val json = Gson().toJson(value) - return json.toString() - } - - @TypeConverter - fun toListOfCourseDateDB(value: String): List { - val type = genericType>() - return Gson().fromJson(value, type) - } }