diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 44de2d2f0..fa0c4dd52 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -336,10 +336,33 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di topicId: String, title: String, viewType: FragmentViewType, + ) { + navigateToDiscussionThread(fm, action, courseId, topicId, "", "", "", title, viewType) + } + + override fun navigateToDiscussionThread( + fm: FragmentManager, + action: String, + courseId: String, + topicId: String, + threadId: String, + responseId: String, + commentId: String, + title: String, + viewType: FragmentViewType, ) { replaceFragmentWithBackStack( fm, - DiscussionThreadsFragment.newInstance(action, courseId, topicId, title, viewType.name) + DiscussionThreadsFragment.newInstance( + action, + courseId, + topicId, + threadId, + responseId, + commentId, + title, + viewType.name + ) ) } @@ -347,10 +370,20 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di fm: FragmentManager, courseId: String, thread: Thread, + ) { + navigateToDiscussionComments(fm, courseId, thread, "", "") + } + + override fun navigateToDiscussionComments( + fm: FragmentManager, + courseId: String, + thread: Thread, + responseId: String, + commentId: String, ) { replaceFragmentWithBackStack( fm, - DiscussionCommentsFragment.newInstance(courseId, thread) + DiscussionCommentsFragment.newInstance(courseId, thread, responseId, commentId) ) } 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 0d8ababb0..3777e25dd 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -434,10 +434,12 @@ val screenModule = module { get(), ) } - viewModel { (courseId: String, thread: Thread) -> + viewModel { (courseId: String, thread: Thread, responseId: String, commentId: String) -> DiscussionCommentsViewModel( courseId, thread, + responseId, + commentId, get(), get(), get(), diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt index 30ca9d6b4..6385f33d2 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt @@ -61,6 +61,8 @@ class CourseUnitContainerAdapter( DiscussionTopicsViewModel.TOPIC, viewModel.courseId, block.studentViewData?.topicId ?: "", + "", + "", block.displayName, FragmentViewType.MAIN_CONTENT.name, block.id diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt index 297d7e59e..1ae82e98a 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt @@ -22,6 +22,14 @@ interface DiscussionRouter { thread: Thread, ) + fun navigateToDiscussionComments( + fm: FragmentManager, + courseId: String, + thread: Thread, + responseId: String, + commentId: String, + ) + fun navigateToDiscussionResponses( fm: FragmentManager, courseId: String, 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 a2b199b31..b491eee35 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 @@ -102,7 +102,8 @@ class DiscussionCommentsFragment : Fragment() { private val viewModel by viewModel { parametersOf( requireArguments().getString(ARG_COURSE_ID, ""), - requireArguments().parcelable(ARG_THREAD)!! + requireArguments().parcelable(ARG_THREAD)!!, + requireArguments().getString(ARG_RESPONSE_ID, ""), ) } private val router by inject() @@ -136,6 +137,8 @@ class DiscussionCommentsFragment : Fragment() { canLoadMore = canLoadMore, scrollToBottom = scrollToBottom, refreshing = refreshing, + responseId = viewModel.responseId, + commentId = viewModel.commentId, onSwipeRefresh = { viewModel.updateThreadComments() }, @@ -181,6 +184,8 @@ class DiscussionCommentsFragment : Fragment() { ) } } + requireArguments().putString(ARG_RESPONSE_ID, "") + requireArguments().putString(ARG_COMMENT_ID, "") } companion object { @@ -192,15 +197,21 @@ class DiscussionCommentsFragment : Fragment() { private const val ARG_COURSE_ID = "argCourseId" private const val ARG_THREAD = "argThread" + private const val ARG_RESPONSE_ID = "argResponseId" + private const val ARG_COMMENT_ID = "argCommentId" fun newInstance( courseId: String, thread: Thread, + responseId: String, + commentId: String, ): DiscussionCommentsFragment { val fragment = DiscussionCommentsFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_THREAD to thread + ARG_THREAD to thread, + ARG_RESPONSE_ID to responseId, + ARG_COMMENT_ID to commentId, ) return fragment } @@ -218,13 +229,15 @@ private fun DiscussionCommentsScreen( canLoadMore: Boolean, scrollToBottom: Boolean, refreshing: Boolean, + responseId: String, + commentId: String, onSwipeRefresh: () -> Unit, paginationCallBack: () -> Unit, onItemClick: (String, String, Boolean) -> Unit, onCommentClick: (DiscussionComment) -> Unit, onAddResponseClick: (String) -> Unit, onBackClick: () -> Unit, - onUserPhotoClick: (String) -> Unit + onUserPhotoClick: (String) -> Unit, ) { val scaffoldState = rememberScaffoldState() val scrollState = rememberLazyListState() @@ -238,6 +251,10 @@ private fun DiscussionCommentsScreen( mutableStateOf("") } + var fromNotificationNavigation by rememberSaveable { + mutableStateOf(responseId.isNotEmpty() && commentId.isNotEmpty()) + } + val sendButtonAlpha = if (responseValue.isEmpty()) 0.3f else 1f Scaffold( @@ -379,6 +396,10 @@ private fun DiscussionCommentsScreen( onUserPhotoClick = { onUserPhotoClick(comment.author) }) + if (fromNotificationNavigation && commentId.isNotEmpty() && responseId == comment.id) { + onCommentClick(comment) + fromNotificationNavigation = false + } } item { if (canLoadMore) { @@ -514,7 +535,9 @@ private fun DiscussionCommentsScreenPreview() { scrollToBottom = false, refreshing = false, onSwipeRefresh = {}, - onUserPhotoClick = {} + onUserPhotoClick = {}, + responseId = "", + commentId = "", ) } } @@ -545,7 +568,9 @@ private fun DiscussionCommentsScreenTabletPreview() { scrollToBottom = false, refreshing = false, onSwipeRefresh = {}, - onUserPhotoClick = {} + onUserPhotoClick = {}, + responseId = "", + commentId = "", ) } } 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 b5a450b6c..ee3a4a4d3 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 @@ -25,6 +25,8 @@ import org.openedx.discussion.system.notifier.DiscussionThreadDataChanged class DiscussionCommentsViewModel( val courseId: String, thread: Thread, + val responseId: String, + val commentId: String, private val interactor: DiscussionInteractor, private val resourceManager: ResourceManager, private val notifier: DiscussionNotifier, @@ -126,6 +128,17 @@ class DiscussionCommentsViewModel( } commentCount = response.pagination.count comments.addAll(response.results) + if (responseId.isNotEmpty()) { + if(comments.find { it.id == responseId } == null) { + val comment = interactor.getResponse(responseId) + comments.add(0, comment) + commentCount.inc() + }else{ + val comment = comments.find { it.id == responseId } + comments.remove(comment) + comments.add(0, comment!!) + } + } _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) 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 04bb82371..9cba66bcb 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 @@ -126,6 +126,9 @@ class DiscussionThreadsFragment : Fragment() { savedInstanceState: Bundle? ) = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + val threadId = requireArguments().getString(ARG_THREAD_ID, "") + val responseId = requireArguments().getString(ARG_RESPONSE_ID, "") + val commentId = requireArguments().getString(ARG_COMMENT_ID, "") setContent { OpenEdXTheme { val windowSize = rememberWindowSize() @@ -138,6 +141,7 @@ class DiscussionThreadsFragment : Fragment() { DiscussionThreadsScreen( windowSize = windowSize, title = requireArguments().getString(ARG_TITLE, ""), + threadId = threadId, uiState = uiState, uiMessage = uiMessage, canLoadMore = canLoadMore, @@ -156,7 +160,9 @@ class DiscussionThreadsFragment : Fragment() { router.navigateToDiscussionComments( requireActivity().supportFragmentManager, viewModel.courseId, - it + it, + responseId, + commentId, ) }, onCreatePostClick = { @@ -176,6 +182,9 @@ class DiscussionThreadsFragment : Fragment() { ) } } + requireArguments().putString(ARG_THREAD_ID, "") + requireArguments().putString(ARG_RESPONSE_ID, "") + requireArguments().putString(ARG_COMMENT_ID, "") } companion object { @@ -183,6 +192,9 @@ class DiscussionThreadsFragment : Fragment() { private const val ARG_COURSE_ID = "courseId" private const val ARG_BLOCK_ID = "blockId" private const val ARG_TOPIC_ID = "topicId" + private const val ARG_THREAD_ID = "threadId" + private const val ARG_RESPONSE_ID = "responseId" + private const val ARG_COMMENT_ID = "commentId" private const val ARG_TITLE = "title" private const val ARG_FRAGMENT_VIEW_TYPE = "fragmentViewType" @@ -190,15 +202,21 @@ class DiscussionThreadsFragment : Fragment() { threadType: String, courseId: String, topicId: String, + threadId: String, + responseId: String, + commentId: String, title: String, viewType: String, - blockId: String = "" + blockId: String = "", ): DiscussionThreadsFragment { val fragment = DiscussionThreadsFragment() fragment.arguments = bundleOf( ARG_THREAD_TYPE to threadType, ARG_COURSE_ID to courseId, ARG_TOPIC_ID to topicId, + ARG_THREAD_ID to threadId, + ARG_RESPONSE_ID to responseId, + ARG_COMMENT_ID to commentId, ARG_TITLE to title, ARG_FRAGMENT_VIEW_TYPE to viewType, ARG_BLOCK_ID to blockId @@ -213,6 +231,7 @@ class DiscussionThreadsFragment : Fragment() { private fun DiscussionThreadsScreen( windowSize: WindowSize, title: String, + threadId: String = "", uiState: DiscussionThreadsUIState, uiMessage: UIMessage?, canLoadMore: Boolean, @@ -224,7 +243,7 @@ private fun DiscussionThreadsScreen( onItemClick: (org.openedx.discussion.domain.model.Thread) -> Unit, onCreatePostClick: () -> Unit, paginationCallback: () -> Unit, - onBackClick: () -> Unit + onBackClick: () -> Unit, ) { val scaffoldState = rememberScaffoldState() @@ -270,6 +289,10 @@ private fun DiscussionThreadsScreen( val isImeVisible by isImeVisibleState() + var fromNotificationNavigation by rememberSaveable { + mutableStateOf(threadId.isNotEmpty()) + } + val scaffoldModifier = if (viewType == FragmentViewType.FULL_CONTENT) { Modifier .fillMaxSize() @@ -563,6 +586,14 @@ private fun DiscussionThreadsScreen( ) { paginationCallback() } + if (fromNotificationNavigation && threadId.isNotEmpty() && uiState.data.isNotEmpty()) { + val index = + uiState.data.indexOfFirst { it.id == threadId } + if (index != -1) { + onItemClick(uiState.data[index]) + } + fromNotificationNavigation = false + } } } else { val noDiscussionsScrollState = rememberScrollState() diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/InboxNotificationsResponse.kt b/notifications/src/main/java/org/openedx/notifications/data/model/InboxNotificationsResponse.kt index 98765c7bf..46b7aacfd 100644 --- a/notifications/src/main/java/org/openedx/notifications/data/model/InboxNotificationsResponse.kt +++ b/notifications/src/main/java/org/openedx/notifications/data/model/InboxNotificationsResponse.kt @@ -65,6 +65,7 @@ data class NotificationItem( @SerializedName("content_context") val contentContext: NotificationContent, @SerializedName("content") val content: String, @SerializedName("content_url") val contentUrl: String, + @SerializedName("course_id") val courseId: String?, @SerializedName("last_read") val lastRead: String?, @SerializedName("last_seen") val lastSeen: String?, @SerializedName("created") val created: String, @@ -76,6 +77,7 @@ data class NotificationItem( contentContext = contentContext.mapToDomain(), content = content, contentUrl = contentUrl, + courseId = courseId.orEmpty(), lastRead = TimeUtils.iso8601ToDate(lastRead ?: ""), lastSeen = TimeUtils.iso8601ToDate(lastSeen ?: ""), created = TimeUtils.iso8601ToDate(created), diff --git a/notifications/src/main/java/org/openedx/notifications/domain/model/InboxNotifications.kt b/notifications/src/main/java/org/openedx/notifications/domain/model/InboxNotifications.kt index 5eba4000e..1078ffbd6 100644 --- a/notifications/src/main/java/org/openedx/notifications/domain/model/InboxNotifications.kt +++ b/notifications/src/main/java/org/openedx/notifications/domain/model/InboxNotifications.kt @@ -24,6 +24,7 @@ data class NotificationItem( val contentContext: NotificationContent, val content: String, val contentUrl: String, + val courseId: String, val lastRead: Date?, val lastSeen: Date?, val created: Date?, @@ -46,4 +47,8 @@ data class NotificationContent( val emailContent: String, val authorName: String, val authorPronoun: String, -) +){ + val responseId = parentId.ifEmpty { commentId } + + val responseCommentId = if(responseId == commentId) "" else commentId +} diff --git a/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt b/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt index c9baf57eb..4f20e3661 100644 --- a/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt +++ b/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt @@ -1,7 +1,20 @@ package org.openedx.notifications.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.FragmentViewType interface NotificationsRouter { fun navigateToPushNotificationsSettings(fm: FragmentManager) + + fun navigateToDiscussionThread( + fm: FragmentManager, + action: String, + courseId: String, + topicId: String, + threadId: String, + responseId: String, + commentId:String, + title: String, + viewType: FragmentViewType + ) } diff --git a/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxFragment.kt b/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxFragment.kt index b53fec2c6..6b25c34a7 100644 --- a/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxFragment.kt +++ b/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxFragment.kt @@ -127,6 +127,7 @@ class NotificationsInboxFragment : Fragment() { }, markNotificationAsRead = { notification, inboxSection -> viewModel.markNotificationAsRead( + fm = requireActivity().supportFragmentManager, notification = notification, inboxSection = inboxSection, ) @@ -533,6 +534,7 @@ private val mockNotificationItem = NotificationItem( notificationType = "", contentUrl = "", created = Date(), + courseId = "", lastRead = null, lastSeen = null, content = "Mock Content", diff --git a/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxViewModel.kt b/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxViewModel.kt index 8693f9ce9..71e6e5589 100644 --- a/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxViewModel.kt +++ b/notifications/src/main/java/org/openedx/notifications/presentation/inbox/NotificationsInboxViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.FragmentViewType import org.openedx.core.UIMessage import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager @@ -112,16 +113,18 @@ class NotificationsInboxViewModel( } fun markNotificationAsRead( + fm: FragmentManager, notification: NotificationItem, inboxSection: InboxSection, ) { viewModelScope.launch { try { - if (notification.isUnread() && interactor.markNotificationAsRead(notification.id)) { - val currentSection = notifications[inboxSection] ?: return@launch + val currentSection = notifications[inboxSection] ?: return@launch + + val index = currentSection.indexOfFirst { it.id == notification.id } + if (index == -1) return@launch - val index = currentSection.indexOfFirst { it.id == notification.id } - if (index == -1) return@launch + if (notification.isUnread() && interactor.markNotificationAsRead(notification.id)) { // Locally update the lastRead timestamp to avoid refreshing the entire list. currentSection[index] = currentSection[index].copy(lastRead = Date()) @@ -133,7 +136,19 @@ class NotificationsInboxViewModel( } // Navigating the user to the related post or response in the Course Discussion Tab - // will be implemented in a separate PR. + if(notification.courseId.isNotEmpty()) { + notificationsRouter.navigateToDiscussionThread( + fm = fm, + action = "Topic", + courseId = notification.courseId, + topicId = notification.contentContext.topicId, + threadId = notification.contentContext.threadId, + responseId = notification.contentContext.responseId, + commentId = notification.contentContext.responseCommentId, + title = notification.contentContext.courseName, + viewType = FragmentViewType.FULL_CONTENT + ) + } } catch (e: Exception) { e.printStackTrace()