Skip to content

Commit

Permalink
feat: Notifications Inbox Screen (openedx#77)
Browse files Browse the repository at this point in the history
Fixes: LEARNER-10345
  • Loading branch information
HamzaIsrar12 authored Jan 8, 2025
1 parent 9221aec commit 0a65db2
Show file tree
Hide file tree
Showing 21 changed files with 965 additions and 15 deletions.
5 changes: 5 additions & 0 deletions app/src/main/java/org/openedx/app/AppRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import org.openedx.discussion.presentation.responses.DiscussionResponsesFragment
import org.openedx.discussion.presentation.search.DiscussionSearchThreadFragment
import org.openedx.discussion.presentation.threads.DiscussionAddThreadFragment
import org.openedx.discussion.presentation.threads.DiscussionThreadsFragment
import org.openedx.notifications.presentation.inbox.NotificationsInboxFragment
import org.openedx.profile.domain.model.Account
import org.openedx.profile.presentation.ProfileRouter
import org.openedx.profile.presentation.anothersaccount.AnothersProfileFragment
Expand Down Expand Up @@ -149,6 +150,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
return ProgramFragment.newInstance(isNestedFragment = true)
}

override fun navigateToNotificationsInbox(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, NotificationsInboxFragment())
}

override fun navigateToCourseInfo(
fm: FragmentManager,
courseId: String,
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel
import org.openedx.learn.presentation.LearnViewModel
import org.openedx.notifications.data.repository.NotificationsRepository
import org.openedx.notifications.domain.interactor.NotificationsInteractor
import org.openedx.notifications.presentation.inbox.NotificationsInboxViewModel
import org.openedx.profile.data.repository.ProfileRepository
import org.openedx.profile.domain.interactor.ProfileInteractor
import org.openedx.profile.domain.model.Account
Expand Down Expand Up @@ -485,6 +486,8 @@ val screenModule = module {
single { NotificationsRepository(get()) }
factory { NotificationsInteractor(get()) }

viewModel { NotificationsInboxViewModel(get()) }

single { IAPRepository(get()) }
factory { IAPInteractor(get(), get(), get(), get(), get()) }
viewModel { (purchaseFlowData: PurchaseFlowData) ->
Expand Down
3 changes: 3 additions & 0 deletions core/src/edx/org/openedx/core/ui/theme/Colors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ val light_progress_bar_background_color = Color(0xFFE7E4DB)
val light_primary_card_caution_background = Color(0xFFF3F1ED)
val light_primary_card_info_background = Color(0xFFE7E4DB)

val light_inbox_time_marker_color = Color(0xFF707070)

// Dark theme colors scheme
val dark_primary = Color(0xFFFBFAF9) // Light 200
Expand Down Expand Up @@ -175,3 +176,5 @@ val dark_progress_bar_background_color = Color(0xFF707070)

val dark_primary_card_caution_background = Color(0xFF2D494E)
val dark_primary_card_info_background = Color(0xFF0E3639)

val dark_inbox_time_marker_color = Color(0xFFADADAD)
2 changes: 2 additions & 0 deletions core/src/main/java/org/openedx/core/ui/theme/AppColors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ data class AppColors(

val primaryCardCautionBackground: Color,
val primaryCardInfoBackground: Color,

val inboxTimeMarkerColor: Color,
) {
val primary: Color get() = material.primary
val primaryVariant: Color get() = material.primaryVariant
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/org/openedx/core/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ private val DarkColorPalette = AppColors(

primaryCardCautionBackground = dark_primary_card_caution_background,
primaryCardInfoBackground = dark_primary_card_info_background,

inboxTimeMarkerColor = dark_inbox_time_marker_color,
)

private val LightColorPalette = AppColors(
Expand Down Expand Up @@ -194,6 +196,8 @@ private val LightColorPalette = AppColors(

primaryCardCautionBackground = light_primary_card_caution_background,
primaryCardInfoBackground = light_primary_card_info_background,

inboxTimeMarkerColor = light_inbox_time_marker_color,
)

val MaterialTheme.appColors: AppColors
Expand Down
2 changes: 2 additions & 0 deletions core/src/openedx/org/openedx/core/ui/theme/Colors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ val light_progress_bar_background_color = Color(0xFF97A5BB)
val light_primary_card_caution_background = Color(0xFFF3F1ED)
val light_primary_card_info_background = Color(0xFFE7E4DB)
val light_social_auth_divider = light_divider
val light_inbox_time_marker_color = Color(0xFF707070)


val dark_primary = Color(0xFF3F68F8)
Expand Down Expand Up @@ -154,3 +155,4 @@ val dark_progress_bar_background_color = Color(0xFF8E9BAE)
val dark_primary_card_caution_background = Color(0xFF2D494E)
val dark_primary_card_info_background = Color(0xFF0E3639)
val dark_social_auth_divider = dark_divider
val dark_inbox_time_marker_color = Color(0xFFADADAD)
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ interface DashboardRouter {
fun navigateToAllEnrolledCourses(fm: FragmentManager)

fun getProgramFragment(): Fragment

fun navigateToNotificationsInbox(fm: FragmentManager)
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class LearnFragment : Fragment(R.layout.fragment_learn) {
viewModel.updateLearnType(learnType)
},
onNotificationBadgeClick = {
viewModel.onNotificationBadgeClick()
viewModel.onNotificationBadgeClick(requireActivity().supportFragmentManager)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.openedx.learn.presentation

import androidx.fragment.app.FragmentManager
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -50,20 +51,18 @@ class LearnViewModel(

init {
viewModelScope.launch {
launch {
_uiState.collect { uiState ->
if (uiState.learnType == LearnType.COURSES) {
logMyCoursesTabClickedEvent()
} else {
logMyProgramsTabClickedEvent()
}
_uiState.collect { uiState ->
if (uiState.learnType == LearnType.COURSES) {
logMyCoursesTabClickedEvent()
} else {
logMyProgramsTabClickedEvent()
}
}
launch {
pushNotifier.notifier.collect { event ->
if (event is PushEvent.RefreshBadgeCount) {
checkNotificationCount()
}
}
viewModelScope.launch {
pushNotifier.notifier.collect { event ->
if (event is PushEvent.RefreshBadgeCount) {
checkNotificationCount()
}
}
}
Expand All @@ -85,7 +84,8 @@ class LearnViewModel(
}
}

fun onNotificationBadgeClick() {
fun onNotificationBadgeClick(fm: FragmentManager) {
dashboardRouter.navigateToNotificationsInbox(fm)
_uiState.update { it.copy(hasUnreadNotifications = false) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ package org.openedx.notifications.data.api

object APIConstants {
const val NOTIFICATION_COUNT = "/api/notifications/count/"
const val NOTIFICATIONS_INBOX = "/api/notifications/"

const val APP_NAME_DISCUSSION = "discussion"
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package org.openedx.notifications.data.api

import org.openedx.notifications.data.model.InboxNotificationsResponse
import org.openedx.notifications.data.model.NotificationsCountResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface NotificationsApi {
@GET(APIConstants.NOTIFICATION_COUNT)
suspend fun getUnreadNotificationsCount(): NotificationsCountResponse

@GET(APIConstants.NOTIFICATIONS_INBOX)
suspend fun getInboxNotifications(
@Query("app_name") appName: String,
@Query("page") page: Int,
): InboxNotificationsResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.openedx.notifications.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.utils.TimeUtils
import org.openedx.notifications.domain.model.InboxNotifications
import org.openedx.notifications.domain.model.InboxSection
import org.openedx.notifications.domain.model.Pagination
import java.util.Date
import org.openedx.notifications.domain.model.NotificationContent as DomainNotificationContent
import org.openedx.notifications.domain.model.NotificationItem as DomainNotificationItem


data class InboxNotificationsResponse(
@SerializedName("next") val next: String?,
@SerializedName("previous") val previous: String?,
@SerializedName("count") val count: Int,
@SerializedName("num_pages") val numPages: Int,
@SerializedName("current_page") val currentPage: Int,
@SerializedName("start") val start: Int,
@SerializedName("results") val results: List<NotificationItem>,
) {
fun mapToDomain(): InboxNotifications = InboxNotifications(
pagination = Pagination(
next = next.orEmpty(),
previous = previous.orEmpty(),
count = count,
numPages = numPages,
currentPage = currentPage,
start = start,
),
notifications = organizeNotificationsBySection()
)

private fun organizeNotificationsBySection(): Map<InboxSection, List<DomainNotificationItem>> {
val currentDate = Date()
val recentThresholdMillis = currentDate.time - DAY_IN_MILLIS
val weekThresholdMillis = currentDate.time - WEEK_IN_MILLIS

val notifications = results.map { it.mapToDomain() }

return mapOf(
InboxSection.RECENT to notifications.filter {
(it.created?.time ?: 0L) >= recentThresholdMillis
},
InboxSection.THIS_WEEK to notifications.filter {
val createdTime = it.created?.time ?: 0L
createdTime in weekThresholdMillis until recentThresholdMillis
},
InboxSection.OLDER to notifications.filter {
(it.created?.time ?: 0L) < weekThresholdMillis
}
)
}

companion object {
private const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000L
private const val WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS
}
}

data class NotificationItem(
@SerializedName("id") val id: Int,
@SerializedName("app_name") val appName: String,
@SerializedName("notification_type") val notificationType: String,
@SerializedName("content_context") val contentContext: NotificationContent,
@SerializedName("content") val content: String,
@SerializedName("content_url") val contentUrl: String,
@SerializedName("last_read") val lastRead: String?,
@SerializedName("last_seen") val lastSeen: String?,
@SerializedName("created") val created: String,
) {
fun mapToDomain(): DomainNotificationItem = DomainNotificationItem(
id = id,
appName = appName,
notificationType = notificationType,
contentContext = contentContext.mapToDomain(),
content = content,
contentUrl = contentUrl,
lastRead = TimeUtils.iso8601ToDate(lastRead ?: ""),
lastSeen = TimeUtils.iso8601ToDate(lastSeen ?: ""),
created = TimeUtils.iso8601ToDate(created),
)
}

data class NotificationContent(
@SerializedName("p") val paragraph: String,
@SerializedName("strong") val strongText: String,
@SerializedName("topic_id") val topicId: String?,
@SerializedName("parent_id") val parentId: String?,
@SerializedName("thread_id") val threadId: String?,
@SerializedName("comment_id") val commentId: String?,
@SerializedName("post_title") val postTitle: String,
@SerializedName("course_name") val courseName: String,
@SerializedName("replier_name") val replierName: String,
@SerializedName("email_content") val emailContent: String,
@SerializedName("author_name") val authorName: String?,
@SerializedName("author_pronoun") val authorPronoun: String?,
) {
fun mapToDomain(): DomainNotificationContent = DomainNotificationContent(
paragraph = paragraph,
strongText = strongText,
topicId = topicId.orEmpty(),
parentId = parentId.orEmpty(),
threadId = threadId.orEmpty(),
commentId = commentId.orEmpty(),
postTitle = postTitle,
courseName = courseName,
replierName = replierName,
emailContent = emailContent,
authorName = authorName.orEmpty(),
authorPronoun = authorPronoun.orEmpty(),
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package org.openedx.notifications.data.repository

import org.openedx.notifications.data.api.APIConstants
import org.openedx.notifications.data.api.NotificationsApi
import org.openedx.notifications.domain.model.InboxNotifications
import org.openedx.notifications.domain.model.NotificationsCount

class NotificationsRepository(private val api: NotificationsApi) {
suspend fun getUnreadNotificationsCount(): NotificationsCount {
return api.getUnreadNotificationsCount().mapToDomain()
}

suspend fun getInboxNotifications(page: Int): InboxNotifications {
return api.getInboxNotifications(
appName = APIConstants.APP_NAME_DISCUSSION,
page = page
).mapToDomain()
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package org.openedx.notifications.domain.interactor

import org.openedx.notifications.data.repository.NotificationsRepository
import org.openedx.notifications.domain.model.InboxNotifications
import org.openedx.notifications.domain.model.NotificationsCount

class NotificationsInteractor(private val repository: NotificationsRepository) {

suspend fun getUnreadNotificationsCount(): NotificationsCount {
return repository.getUnreadNotificationsCount()
}

suspend fun getInboxNotifications(page: Int): InboxNotifications {
return repository.getInboxNotifications(page)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.openedx.notifications.domain.model

import java.util.Date

data class InboxNotifications(
val pagination: Pagination,
val notifications: Map<InboxSection, List<NotificationItem>>,
)

data class Pagination(
val next: String,
val previous: String,
val count: Int,
val numPages: Int,
val currentPage: Int,
val start: Int,
)

data class NotificationItem(
val id: Int,
val appName: String,
val notificationType: String,
val contentContext: NotificationContent,
val content: String,
val contentUrl: String,
val lastRead: Date?,
val lastSeen: Date?,
val created: Date?,
)

data class NotificationContent(
val paragraph: String,
val strongText: String,
val topicId: String,
val parentId: String,
val threadId: String,
val commentId: String,
val postTitle: String,
val courseName: String,
val replierName: String,
val emailContent: String,
val authorName: String,
val authorPronoun: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.openedx.notifications.domain.model

import org.openedx.notifications.R

enum class InboxSection(val titleResId: Int) {
RECENT(R.string.notifications_recent),

THIS_WEEK(R.string.notifications_this_week),

OLDER(R.string.notifications_older);
}
Loading

0 comments on commit 0a65db2

Please sign in to comment.