From 20313805f39f83d7ea724dfc46bd4d348401796c Mon Sep 17 00:00:00 2001 From: droid Date: Mon, 6 May 2024 13:31:49 +0200 Subject: [PATCH] feat: added the external router for deep links --- app/src/main/AndroidManifest.xml | 3 +- .../main/java/org/openedx/app/AppActivity.kt | 11 +- .../java/org/openedx/app/AppExternalRouter.kt | 299 ++++++++++++++++++ .../main/java/org/openedx/app/AppRouter.kt | 28 +- .../main/java/org/openedx/app/AppViewModel.kt | 6 + .../main/java/org/openedx/app/MainFragment.kt | 32 +- app/src/main/java/org/openedx/app/Screen.kt | 20 ++ .../main/java/org/openedx/app/di/AppModule.kt | 2 + .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../test/java/org/openedx/AppViewModelTest.kt | 8 +- .../openedx/auth/presentation/AuthRouter.kt | 8 +- .../main/java/org/openedx/core/CourseTab.kt | 9 + .../src/main/java/org/openedx/core/HomeTab.kt | 8 + .../course/presentation/CourseRouter.kt | 2 +- .../container/CourseContainerFragment.kt | 23 +- .../handouts/HandoutsWebViewFragment.kt | 20 +- .../presentation/DashboardFragment.kt | 4 +- .../dashboard/presentation/DashboardRouter.kt | 2 + .../discovery/presentation/DiscoveryRouter.kt | 4 +- .../detail/CourseDetailsFragment.kt | 4 +- .../presentation/info/CourseInfoViewModel.kt | 2 + .../presentation/program/ProgramViewModel.kt | 4 +- .../discussion/data/api/DiscussionApi.kt | 15 + .../data/repository/DiscussionRepository.kt | 16 +- .../domain/interactor/DiscussionInteractor.kt | 12 +- .../org/openedx/whatsnew/WhatsNewRouter.kt | 8 +- .../whatsnew/WhatsNewViewModel.kt | 4 +- 27 files changed, 510 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/AppExternalRouter.kt create mode 100644 app/src/main/java/org/openedx/app/Screen.kt create mode 100644 core/src/main/java/org/openedx/core/CourseTab.kt create mode 100644 core/src/main/java/org/openedx/core/HomeTab.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8020f6b74..c50bca35b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,7 +39,8 @@ android:exported="true" android:fitsSystemWindows="true" android:theme="@style/Theme.App.Starting" - android:windowSoftInputMode="adjustPan"> + android:windowSoftInputMode="adjustPan" + android:launchMode="singleInstance"> diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 5ab0d0b0e..37d934d3b 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -145,10 +145,15 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { super.onStart() if (viewModel.isBranchEnabled) { - val callback = BranchUniversalReferralInitListener { _, linkProperties, error -> - if (linkProperties != null) { + val callback = BranchUniversalReferralInitListener { branchUniversalObject, _, error -> + if (branchUniversalObject?.contentMetadata?.customMetadata != null) { branchLogger.i { "Branch init complete." } - branchLogger.i { linkProperties.controlParams.toString() } + branchLogger.i { branchUniversalObject.contentMetadata.customMetadata.toString() } + viewModel.makeExternalRoute( + supportFragmentManager, + branchUniversalObject.contentMetadata.customMetadata + ) + } else if (error != null) { branchLogger.e { "Branch init failed. Caused by -" + error.message } } diff --git a/app/src/main/java/org/openedx/app/AppExternalRouter.kt b/app/src/main/java/org/openedx/app/AppExternalRouter.kt new file mode 100644 index 000000000..ad99856c6 --- /dev/null +++ b/app/src/main/java/org/openedx/app/AppExternalRouter.kt @@ -0,0 +1,299 @@ +package org.openedx.app + +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.openedx.auth.presentation.signin.SignInFragment +import org.openedx.core.CourseTab +import org.openedx.core.FragmentViewType +import org.openedx.core.HomeTab +import org.openedx.core.config.Config +import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.presentation.course.CourseViewMode +import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.course.presentation.handouts.HandoutsType +import org.openedx.discovery.presentation.catalog.WebViewLink +import org.openedx.discussion.domain.interactor.DiscussionInteractor +import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel +import kotlin.coroutines.CoroutineContext + +class AppExternalRouter( + private val appRouter: AppRouter, + private val config: Config, + private val corePreferences: CorePreferences, + private val discussionInteractor: DiscussionInteractor, + private val courseInteractor: CourseInteractor +) : CoroutineScope { + + private val isUserLoggedIn get() = corePreferences.user != null + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + + fun makeRoute(fm: FragmentManager, params: Map) { + val screenName = params["screen_name"] ?: return + when (screenName) { + // Discovery + Screen.DISCOVERY.screenName -> { + navigateToDiscoveryScreen(fm = fm) + } + + Screen.DISCOVERY_COURSE_DETAIL.screenName -> { + params["course_id"]?.let { courseId -> + navigateToDiscoveryScreen(fm = fm) + appRouter.navigateToCourseInfo( + fm = fm, + courseId = courseId, + infoType = WebViewLink.Authority.COURSE_INFO.name + ) + } + } + + Screen.DISCOVERY_PROGRAM_DETAIL.screenName -> { + params["path_id"]?.let { pathId -> + navigateToDiscoveryScreen(fm = fm) + appRouter.navigateToCourseInfo( + fm = fm, + courseId = pathId, + infoType = WebViewLink.Authority.PROGRAM_INFO.name + ) + } + } + } + + if (!isUserLoggedIn) { + if (appRouter.getVisibleFragment(fm = fm) !is SignInFragment) { + appRouter.navigateToSignIn( + fm = fm, + courseId = null, + infoType = null + ) + } + return + } + + when (screenName) { + // Course + Screen.COURSE_DASHBOARD.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToCourseOutline( + fm = fm, + courseId = courseId, + courseTitle = "", + enrollmentMode = "", + selectedTab = CourseTab.COURSE + ) + } + } + + Screen.COURSE_VIDEOS.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToCourseOutline( + fm = fm, + courseId = courseId, + courseTitle = "", + enrollmentMode = "", + selectedTab = CourseTab.VIDEOS + ) + } + } + + Screen.COURSE_DISCUSSION.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToCourseOutline( + fm = fm, + courseId = courseId, + courseTitle = "", + enrollmentMode = "", + selectedTab = CourseTab.DISCUSSION + ) + } + } + + Screen.COURSE_DATES.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToCourseOutline( + fm = fm, + courseId = courseId, + courseTitle = "", + enrollmentMode = "", + selectedTab = CourseTab.DATES + ) + } + } + + Screen.COURSE_HANDOUT.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToHandoutsWebView( + fm = fm, + courseId = courseId, + type = HandoutsType.Handouts + ) + } + } + + Screen.COURSE_ANNOUNCEMENT.screenName -> { + params["course_id"]?.let { courseId -> + appRouter.navigateToHandoutsWebView( + fm = fm, + courseId = courseId, + type = HandoutsType.Announcements + ) + } + } + + Screen.COURSE_COMPONENT.screenName -> { + params["course_id"]?.let { courseId -> + params["component_id"]?.let { componentId -> + launch { + try { + val courseStructure = courseInteractor.getCourseStructure(courseId) + courseStructure.blockData + .find { it.descendants.contains(componentId) }?.let { block -> + appRouter.navigateToCourseContainer( + fm = fm, + courseId = courseId, + unitId = block.id, + componentId = componentId, + mode = CourseViewMode.FULL + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } + + // Program + Screen.PROGRAM.screenName -> { + val pathId = params["path_id"] + if (pathId == null) { + appRouter.navigateToMain( + fm = fm, + courseId = null, + infoType = null, + selectedTab = HomeTab.PROGRAMS + ) + } else { + appRouter.navigateToEnrolledProgramInfo( + fm = fm, + pathId = pathId + ) + } + } + + // Discussions + Screen.DISCUSSION_TOPIC.screenName -> { + params["course_id"]?.let { courseId -> + params["topic_id"]?.let { topicId -> + launch { + try { + discussionInteractor.getCourseTopics(courseId) + .find { it.id == topicId }?.let { topic -> + appRouter.navigateToDiscussionThread( + fm = fm, + action = DiscussionTopicsViewModel.TOPIC, + courseId = courseId, + topicId = topicId, + title = topic.name, + viewType = FragmentViewType.FULL_CONTENT + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } + + Screen.DISCUSSION_POST.screenName -> { + params["course_id"]?.let { courseId -> + params["topic_id"]?.let { topicId -> + params["thread_id"]?.let { threadId -> + launch { + try { + val thread = discussionInteractor.getThread( + threadId, + courseId, + topicId + ) + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionComments( + fm = fm, + thread = thread + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } + } + + Screen.DISCUSSION_COMMENT.screenName -> { + params["comment_id"]?.let { commentId -> + launch { + try { + val commentsData = discussionInteractor.getThreadComment(commentId) + commentsData.results.firstOrNull()?.let { comment -> + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionResponses( + fm = fm, + comment = comment, + isClosed = false + ) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + // Profile + Screen.PROFILE.screenName, + Screen.USER_PROFILE.screenName -> { + appRouter.navigateToMain( + fm = fm, + courseId = null, + infoType = null, + selectedTab = HomeTab.PROFILE + ) + } + } + } + + private fun navigateToDiscoveryScreen(fm: FragmentManager) { + if (isUserLoggedIn) { + fm.popBackStack() + fm.beginTransaction() + .replace(R.id.container, MainFragment.newInstance()) + .commitNow() + } else if (config.isPreLoginExperienceEnabled()) { + if (appRouter.getVisibleFragment(fm = fm) !is SignInFragment) { + appRouter.navigateToSignIn( + fm = fm, + courseId = null, + infoType = null + ) + } + } else if (config.getDiscoveryConfig().isViewTypeWebView()) { + appRouter.navigateToWebDiscoverCourses( + fm = fm, + querySearch = "" + ) + } else { + appRouter.navigateToNativeDiscoverCourses( + fm = fm, + querySearch = "" + ) + } + } +} diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 21f3b5aee..4faec0acf 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -8,7 +8,9 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.restore.RestorePasswordFragment import org.openedx.auth.presentation.signin.SignInFragment import org.openedx.auth.presentation.signup.SignUpFragment +import org.openedx.core.CourseTab import org.openedx.core.FragmentViewType +import org.openedx.core.HomeTab import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment @@ -57,10 +59,15 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di ProfileRouter, AppUpgradeRouter, WhatsNewRouter { //region AuthRouter - override fun navigateToMain(fm: FragmentManager, courseId: String?, infoType: String?) { + override fun navigateToMain( + fm: FragmentManager, + courseId: String?, + infoType: String?, + selectedTab: HomeTab? + ) { fm.popBackStack() fm.beginTransaction() - .replace(R.id.container, MainFragment.newInstance(courseId, infoType)) + .replace(R.id.container, MainFragment.newInstance(courseId, infoType, selectedTab)) .commit() } @@ -138,10 +145,16 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di courseId: String, courseTitle: String, enrollmentMode: String, + selectedTab: CourseTab ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode) + CourseContainerFragment.newInstance( + courseId, + courseTitle, + enrollmentMode, + selectedTab + ) ) } @@ -253,12 +266,11 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToHandoutsWebView( fm: FragmentManager, courseId: String, - title: String, type: HandoutsType ) { replaceFragmentWithBackStack( fm, - HandoutsWebViewFragment.newInstance(title, type.name, courseId) + HandoutsWebViewFragment.newInstance(type.name, courseId) ) } //endregion @@ -372,6 +384,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di } //endregion + fun getVisibleFragment(fm: FragmentManager): Fragment? { + val fragments = fm.fragments + for (fragment in fragments) if (fragment.isVisible) return fragment + return null + } + private fun replaceFragmentWithBackStack(fm: FragmentManager, fragment: Fragment) { fm.beginTransaction() .replace(R.id.container, fragment, fragment.javaClass.simpleName) diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index 1febbd15a..53acd83fc 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.app +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope @@ -21,6 +22,7 @@ class AppViewModel( private val preferencesManager: CorePreferences, private val dispatcher: CoroutineDispatcher, private val analytics: AppAnalytics, + private val appExternalRouter: AppExternalRouter, ) : BaseViewModel() { private val _logoutUser = SingleEventLiveData() @@ -60,6 +62,10 @@ class AppViewModel( ) } + fun makeExternalRoute(fm: FragmentManager, params: Map) { + appExternalRouter.makeRoute(fm, params) + } + private fun setUserId() { preferencesManager.user?.let { analytics.setUserIdForSession(it.id) diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index a798c4a3f..6bf9356e0 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -13,6 +13,7 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.adapter.MainNavigationFragmentAdapter import org.openedx.app.databinding.FragmentMainBinding +import org.openedx.core.HomeTab import org.openedx.core.config.Config import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding @@ -69,8 +70,6 @@ class MainFragment : Fragment(R.layout.fragment_main) { } true } - // Trigger click event for the first tab on initial load - binding.bottomNavView.selectedItemId = binding.bottomNavView.selectedItemId viewModel.isBottomBarEnabled.observe(viewLifecycleOwner) { isBottomBarEnabled -> enableBottomBar(isBottomBarEnabled) @@ -98,6 +97,25 @@ class MainFragment : Fragment(R.layout.fragment_main) { putString(ARG_COURSE_ID, "") putString(ARG_INFO_TYPE, "") } + + when (requireArguments().getInt(ARG_SELECTED_TAB, 0)) { + HomeTab.DISCOVER.ordinal -> { + binding.bottomNavView.selectedItemId = R.id.fragmentHome + } + + HomeTab.DASHBOARD.ordinal -> { + binding.bottomNavView.selectedItemId = R.id.fragmentDashboard + } + + HomeTab.PROGRAMS.ordinal -> { + binding.bottomNavView.selectedItemId = R.id.fragmentPrograms + } + + HomeTab.PROFILE.ordinal -> { + binding.bottomNavView.selectedItemId = R.id.fragmentProfile + } + } + requireArguments().remove(ARG_SELECTED_TAB) } } @@ -132,11 +150,17 @@ class MainFragment : Fragment(R.layout.fragment_main) { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_INFO_TYPE = "info_type" - fun newInstance(courseId: String? = null, infoType: String? = null): MainFragment { + private const val ARG_SELECTED_TAB = "selected_tab" + fun newInstance( + courseId: String? = null, + infoType: String? = null, + selectedTab: HomeTab? = null + ): MainFragment { val fragment = MainFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_INFO_TYPE to infoType + ARG_INFO_TYPE to infoType, + ARG_SELECTED_TAB to (selectedTab?.ordinal ?: HomeTab.DISCOVER.ordinal) ) return fragment } diff --git a/app/src/main/java/org/openedx/app/Screen.kt b/app/src/main/java/org/openedx/app/Screen.kt new file mode 100644 index 000000000..f5ea8ba91 --- /dev/null +++ b/app/src/main/java/org/openedx/app/Screen.kt @@ -0,0 +1,20 @@ +package org.openedx.app + +enum class Screen(val screenName: String) { + DISCOVERY("discovery"), + DISCOVERY_COURSE_DETAIL("discovery_course_detail"), + DISCOVERY_PROGRAM_DETAIL("discovery_program_detail"), + COURSE_DASHBOARD("course_dashboard"), + COURSE_VIDEOS("course_videos"), + COURSE_DISCUSSION("course_discussion"), + COURSE_DATES("course_dates"), + COURSE_HANDOUT("course_handout"), + COURSE_ANNOUNCEMENT("course_announcement"), + COURSE_COMPONENT("course_component"), + PROGRAM("program"), + DISCUSSION_TOPIC("discussion_topic"), + DISCUSSION_POST("discussion_post"), + DISCUSSION_COMMENT("discussion_comment"), + PROFILE("profile"), + USER_PROFILE("user_profile"), +} diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index 16a30c0c6..ad4d6b095 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -12,6 +12,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module import org.openedx.app.AnalyticsManager import org.openedx.app.AppAnalytics +import org.openedx.app.AppExternalRouter import org.openedx.app.AppRouter import org.openedx.app.BuildConfig import org.openedx.app.data.storage.PreferencesManager @@ -108,6 +109,7 @@ val appModule = module { single { get() } single { get() } single { get() } + single { AppExternalRouter(get(), get(), get(), get(), get()) } single { NetworkConnection(get()) } 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 c9c395a01..6150a30e1 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -63,7 +63,7 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel val screenModule = module { - viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get()) } + viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get(), get()) } viewModel { MainViewModel(get(), get(), get()) } factory { AuthRepository(get(), get(), get()) } diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index 40b3e813d..7d1080804 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -21,6 +21,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.app.AppAnalytics +import org.openedx.app.AppExternalRouter import org.openedx.app.AppViewModel import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.room.AppDatabase @@ -42,6 +43,7 @@ class AppViewModelTest { private val room = mockk() private val preferencesManager = mockk() private val analytics = mockk() + private val externalRouter = mockk() private val user = User(0, "", "", "") @@ -61,7 +63,7 @@ class AppViewModelTest { every { preferencesManager.user } returns user every { notifier.notifier } returns flow { } val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics, externalRouter) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -83,7 +85,7 @@ class AppViewModelTest { every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics, externalRouter) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -107,7 +109,7 @@ class AppViewModelTest { every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics, externalRouter) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) diff --git a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt index 9b1266119..bed726af8 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt @@ -1,10 +1,16 @@ package org.openedx.auth.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.HomeTab interface AuthRouter { - fun navigateToMain(fm: FragmentManager, courseId: String?, infoType: String?) + fun navigateToMain( + fm: FragmentManager, + courseId: String?, + infoType: String?, + selectedTab: HomeTab? = null + ) fun navigateToSignIn(fm: FragmentManager, courseId: String?, infoType: String?) diff --git a/core/src/main/java/org/openedx/core/CourseTab.kt b/core/src/main/java/org/openedx/core/CourseTab.kt new file mode 100644 index 000000000..39bbaecf4 --- /dev/null +++ b/core/src/main/java/org/openedx/core/CourseTab.kt @@ -0,0 +1,9 @@ +package org.openedx.core + +enum class CourseTab { + COURSE, + VIDEOS, + DISCUSSION, + DATES, + HANDOUTS +} diff --git a/core/src/main/java/org/openedx/core/HomeTab.kt b/core/src/main/java/org/openedx/core/HomeTab.kt new file mode 100644 index 000000000..1a4c91997 --- /dev/null +++ b/core/src/main/java/org/openedx/core/HomeTab.kt @@ -0,0 +1,8 @@ +package org.openedx.core + +enum class HomeTab { + DISCOVER, + DASHBOARD, + PROGRAMS, + PROFILE +} diff --git a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt index b2f520679..706a06f83 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -56,7 +56,7 @@ interface CourseRouter { ) fun navigateToHandoutsWebView( - fm: FragmentManager, courseId: String, title: String, type: HandoutsType + fm: FragmentManager, courseId: String, type: HandoutsType ) fun navigateToDownloadQueue(fm: FragmentManager, descendants: List = arrayListOf()) diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 669b1f661..4f9559849 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.openedx.core.CourseTab import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.global.viewBinding @@ -134,7 +135,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { snackBar?.show() } - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { viewModel.showProgress.collect { binding.progressBar.isVisible = it } @@ -156,7 +157,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { bundle = requireArguments(), onRefresh = { page -> onRefresh(page) - } + }, + initialPage = requireArguments().getInt(ARG_SELECTED_TAB, 0) ) } } @@ -255,16 +257,19 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_COURSE_ID = "courseId" const val ARG_TITLE = "title" const val ARG_ENROLLMENT_MODE = "enrollmentMode" + const val ARG_SELECTED_TAB = "selectedTab" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, + selectedTab: CourseTab ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, ARG_TITLE to courseTitle, - ARG_ENROLLMENT_MODE to enrollmentMode + ARG_ENROLLMENT_MODE to enrollmentMode, + ARG_SELECTED_TAB to selectedTab.ordinal ) return fragment } @@ -279,7 +284,8 @@ fun CourseDashboard( isNavigationEnabled: Boolean, isResumed: Boolean, fragmentManager: FragmentManager, - bundle: Bundle + bundle: Bundle, + initialPage: Int ) { OpenEdXTheme { val windowSize = rememberWindowSize() @@ -297,7 +303,10 @@ fun CourseDashboard( val uiMessage by viewModel.uiMessage.collectAsState(null) val dataReady = viewModel.dataReady.observeAsState() - val pagerState = rememberPagerState(pageCount = { CourseContainerTab.entries.size }) + val pagerState = rememberPagerState( + initialPage = initialPage, + pageCount = { CourseContainerTab.entries.size } + ) val tabState = rememberLazyListState() val snackState = remember { SnackbarHostState() } val pullRefreshState = rememberPullRefreshState( @@ -499,15 +508,12 @@ fun DashboardPager( } CourseContainerTab.MORE -> { - val announcementsString = stringResource(id = R.string.course_announcements) - val handoutsString = stringResource(id = R.string.course_handouts) HandoutsScreen( windowSize = windowSize, onHandoutsClick = { viewModel.courseRouter.navigateToHandoutsWebView( fragmentManager, bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), - handoutsString, HandoutsType.Handouts ) }, @@ -515,7 +521,6 @@ fun DashboardPager( viewModel.courseRouter.navigateToHandoutsWebView( fragmentManager, bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), - announcementsString, HandoutsType.Announcements ) }) diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt index 7c9d3615e..16cc67b84 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt @@ -22,6 +22,7 @@ import org.openedx.core.ui.WindowType import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors +import org.openedx.course.R import org.openedx.course.presentation.CourseAnalyticsEvent class HandoutsWebViewFragment : Fragment() { @@ -39,6 +40,15 @@ class HandoutsWebViewFragment : Fragment() { savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + val title = if (HandoutsType.valueOf(viewModel.handoutsType) == HandoutsType.Handouts) { + viewModel.logEvent(CourseAnalyticsEvent.HANDOUTS) + getString(R.string.course_handouts) + } else { + viewModel.logEvent(CourseAnalyticsEvent.ANNOUNCEMENTS) + getString(R.string.course_announcements) + } + setContent { OpenEdXTheme { val windowSize = rememberWindowSize() @@ -50,7 +60,7 @@ class HandoutsWebViewFragment : Fragment() { WebContentScreen( windowSize = windowSize, apiHostUrl = viewModel.apiHostUrl, - title = requireArguments().getString(ARG_TITLE, ""), + title = title, htmlBody = viewModel.injectDarkMode( htmlBody, colorBackgroundValue, @@ -61,26 +71,18 @@ class HandoutsWebViewFragment : Fragment() { }) } } - if (HandoutsType.valueOf(viewModel.handoutsType) == HandoutsType.Handouts) { - viewModel.logEvent(CourseAnalyticsEvent.HANDOUTS) - } else { - viewModel.logEvent(CourseAnalyticsEvent.ANNOUNCEMENTS) - } } companion object { - private val ARG_TITLE = "argTitle" private val ARG_TYPE = "argType" private val ARG_COURSE_ID = "argCourse" fun newInstance( - title: String, type: String, courseId: String, ): HandoutsWebViewFragment { val fragment = HandoutsWebViewFragment() fragment.arguments = bundleOf( - ARG_TITLE to title, ARG_TYPE to type, ARG_COURSE_ID to courseId ) diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index f6bc5c56a..3d41b56f8 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -71,6 +71,7 @@ import coil.request.ImageRequest import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.AppUpdateState +import org.openedx.core.CourseTab import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseSharingUtmParameters @@ -140,7 +141,8 @@ class DashboardFragment : Fragment() { requireParentFragment().parentFragmentManager, it.course.id, it.course.name, - it.mode + it.mode, + selectedTab = CourseTab.COURSE ) }, onSwipeRefresh = { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index b0b0740d3..201f308d6 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -1,6 +1,7 @@ package org.openedx.dashboard.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.CourseTab interface DashboardRouter { @@ -9,6 +10,7 @@ interface DashboardRouter { courseId: String, courseTitle: String, enrollmentMode: String, + selectedTab: CourseTab, ) fun navigateToSettings(fm: FragmentManager) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt index e1c4baa74..a54d983f9 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt @@ -1,6 +1,7 @@ package org.openedx.discovery.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.CourseTab interface DiscoveryRouter { @@ -8,7 +9,8 @@ interface DiscoveryRouter { fm: FragmentManager, courseId: String, courseTitle: String, - enrollmentMode: String + enrollmentMode: String, + selectedTab: CourseTab ) fun navigateToLogistration(fm: FragmentManager, courseId: String?) 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 813994307..a6049acd7 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 @@ -78,6 +78,7 @@ import androidx.fragment.app.Fragment import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.openedx.core.CourseTab import org.openedx.core.UIMessage import org.openedx.core.domain.model.Media import org.openedx.core.extension.isEmailValid @@ -162,7 +163,8 @@ class CourseDetailsFragment : Fragment() { requireActivity().supportFragmentManager, currentState.course.courseId, currentState.course.name, - "", + enrollmentMode = "", + selectedTab = CourseTab.COURSE ) } 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 636cb9275..47bb00789 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 @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.openedx.core.BaseViewModel +import org.openedx.core.CourseTab import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences @@ -123,6 +124,7 @@ class CourseInfoViewModel( courseId = courseId, courseTitle = "", enrollmentMode = "", + selectedTab = CourseTab.COURSE, ) } } 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 1bed6d2cd..38a39dcc4 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 @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.CourseTab import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config @@ -94,7 +95,8 @@ class ProgramViewModel( fm = fragmentManager, courseId = courseId, courseTitle = "", - enrollmentMode = "" + enrollmentMode = "", + selectedTab = CourseTab.COURSE ) } } diff --git a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt index ebc911425..75a780d72 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt @@ -5,6 +5,7 @@ import org.openedx.discussion.data.model.request.* import org.openedx.discussion.data.model.response.CommentResult import org.openedx.discussion.data.model.response.CommentsResponse import org.openedx.discussion.data.model.response.ThreadsResponse +import org.openedx.discussion.data.model.response.ThreadsResponse.Thread import org.openedx.discussion.data.model.response.TopicsResponse import retrofit2.http.* @@ -26,6 +27,14 @@ interface DiscussionApi { @Query("requested_fields") requestedFields: List = listOf("profile_image") ): ThreadsResponse + @GET("/api/discussion/v1/threads/{thread_id}") + suspend fun getCourseThread( + @Path("thread_id") threadId: String, + @Query("course_id") courseId: String, + @Query("topic_id") topicId: String, + @Query("requested_fields") requestedFields: List = listOf("profile_image") + ): Thread + @GET("/api/discussion/v1/threads/") suspend fun searchThreads( @Query("course_id") courseId: String, @@ -41,6 +50,12 @@ interface DiscussionApi { @Query("requested_fields") requestedFields: List = listOf("profile_image") ): CommentsResponse + @GET("/api/discussion/v1/comments/{comment_id}") + suspend fun getThreadComment( + @Path("comment_id") commentId: String, + @Query("requested_fields") requestedFields: List = listOf("profile_image") + ): CommentsResponse + @GET("/api/discussion/v1/comments/") suspend fun getThreadQuestionComments( @Query("thread_id") threadId: String, diff --git a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt index 4ca6cde8d..3ee4f74a5 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt @@ -58,6 +58,14 @@ class DiscussionRepository( return api.getCourseThreads(courseId, following, topicId, orderBy, view, page).mapToDomain() } + suspend fun getCourseThread( + threadId: String, + courseId: String, + topicId: String + ): org.openedx.discussion.domain.model.Thread { + return api.getCourseThread(threadId, courseId, topicId).mapToDomain() + } + suspend fun searchThread( courseId: String, query: String, @@ -73,6 +81,12 @@ class DiscussionRepository( return api.getThreadComments(threadId, page).mapToDomain() } + suspend fun getThreadComment( + commentId: String + ): CommentsData { + return api.getThreadComment(commentId).mapToDomain() + } + suspend fun getThreadQuestionComments( threadId: String, endorsed: Boolean, @@ -142,4 +156,4 @@ class DiscussionRepository( return api.markBlocksCompletion(blocksCompletionBody) } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt index 7225cc443..561a75006 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt @@ -1,6 +1,7 @@ package org.openedx.discussion.domain.interactor import org.openedx.discussion.data.repository.DiscussionRepository +import org.openedx.discussion.domain.model.CommentsData class DiscussionInteractor( private val repository: DiscussionRepository @@ -31,12 +32,18 @@ class DiscussionInteractor( ) = repository.getCourseThreads(courseId, null, topicId, orderBy, view, page) + suspend fun getThread(threadId: String, courseId: String, topicId: String) = + repository.getCourseThread(threadId, courseId, topicId) + suspend fun searchThread(courseId: String, query: String, page: Int) = repository.searchThread(courseId, query, page) suspend fun getThreadComments(threadId: String, page: Int) = repository.getThreadComments(threadId, page) + suspend fun getThreadComment(commentId: String): CommentsData = + repository.getThreadComment(commentId) + suspend fun getThreadQuestionComments(threadId: String, endorsed: Boolean, page: Int) = repository.getThreadQuestionComments(threadId, endorsed, page) @@ -87,5 +94,6 @@ class DiscussionInteractor( follow: Boolean ) = repository.createThread(topicId, courseId, type, title, rawBody, follow) - suspend fun markBlocksCompletion(courseId: String, blocksId: List) = repository.markBlocksCompletion(courseId, blocksId) -} \ No newline at end of file + suspend fun markBlocksCompletion(courseId: String, blocksId: List) = + repository.markBlocksCompletion(courseId, blocksId) +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewRouter.kt b/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewRouter.kt index a8d1cd463..f173153f7 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewRouter.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewRouter.kt @@ -1,7 +1,13 @@ package org.openedx.whatsnew import androidx.fragment.app.FragmentManager +import org.openedx.core.HomeTab interface WhatsNewRouter { - fun navigateToMain(fm: FragmentManager, courseId: String? = null, infoType: String? = null) + fun navigateToMain( + fm: FragmentManager, + courseId: String?, + infoType: String?, + selectedTab: HomeTab? + ) } 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 51f0f9646..8e6c3d8f7 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.fragment.app.FragmentManager import org.openedx.core.BaseViewModel +import org.openedx.core.HomeTab import org.openedx.core.presentation.global.AppData import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter @@ -41,7 +42,8 @@ class WhatsNewViewModel( router.navigateToMain( fm, courseId, - infoType + infoType, + HomeTab.DASHBOARD ) }