Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Notifications Inbox Screen #77

Merged
merged 7 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
farhan-arshad-dev marked this conversation as resolved.
Show resolved Hide resolved
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,
farhan-arshad-dev marked this conversation as resolved.
Show resolved Hide resolved
@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
Loading