From 00280ecf5291a6b0eb4ae74970398860202eba52 Mon Sep 17 00:00:00 2001 From: Farhan Arshad <43750646+farhan-arshad-dev@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:08:17 +0500 Subject: [PATCH] feat: Notifications indicator on the learn tab (#75) - Notification badge on the learn tab - Check notification count on pull to refresh Fixes: LEARNER-10347 --- .../main/java/org/openedx/app/di/AppModule.kt | 5 ++- .../org/openedx/app/di/NetworkingModule.kt | 2 +- .../java/org/openedx/app/di/ScreenModule.kt | 9 +++- .../java/org/openedx/core/config/Config.kt | 5 +++ .../openedx/core/system/PushGlobalManager.kt | 4 +- .../openedx/core/system/notifier/PushEvent.kt | 7 +++ .../core/system/notifier/PushNotifier.kt | 13 ++++++ .../presentation/DashboardGalleryView.kt | 1 + .../presentation/DashboardGalleryViewModel.kt | 9 +++- .../presentation/DashboardListFragment.kt | 1 + .../presentation/DashboardListViewModel.kt | 9 +++- .../learn/presentation/LearnFragment.kt | 45 ++++++++++++++++--- .../learn/presentation/LearnUIState.kt | 6 ++- .../learn/presentation/LearnViewModel.kt | 42 ++++++++++++++--- .../dashboard_ic_notification_badge.xml | 9 ++++ ...dashboard_ic_notification_bubble_badge.xml | 13 ++++++ .../dashboard_ic_notification_badge.xml | 9 ++++ ...dashboard_ic_notification_bubble_badge.xml | 13 ++++++ dashboard/src/main/res/values/strings.xml | 1 + .../presentation/DashboardViewModelTest.kt | 35 ++++++++++----- .../org/openedx/notifications/PushManager.kt | 8 +++- .../notifications/data/api/APIConstants.kt | 5 +++ .../data/api/NotificationsApi.kt | 10 ++++- .../data/model/CountByAppNameModel.kt | 12 +++++ .../data/model/NotificaitonCountResponse.kt | 23 ++++++++++ .../notifications/data/model/delete_me.kt | 1 - .../repository/NotificationsRepository.kt | 10 +++++ .../data/repository/delete_me.kt | 1 - .../interactor/NotificationsInteractor.kt | 11 +++++ .../interactor/Notificationsinteractor.kt | 2 - .../domain/model/NotificationsCount.kt | 5 +++ 31 files changed, 292 insertions(+), 34 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/PushEvent.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/PushNotifier.kt create mode 100644 dashboard/src/main/res/drawable-night/dashboard_ic_notification_badge.xml create mode 100644 dashboard/src/main/res/drawable-night/dashboard_ic_notification_bubble_badge.xml create mode 100644 dashboard/src/main/res/drawable/dashboard_ic_notification_badge.xml create mode 100644 dashboard/src/main/res/drawable/dashboard_ic_notification_bubble_badge.xml create mode 100644 notifications/src/main/java/org/openedx/notifications/data/api/APIConstants.kt create mode 100644 notifications/src/main/java/org/openedx/notifications/data/model/CountByAppNameModel.kt create mode 100644 notifications/src/main/java/org/openedx/notifications/data/model/NotificaitonCountResponse.kt delete mode 100644 notifications/src/main/java/org/openedx/notifications/data/model/delete_me.kt create mode 100644 notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt delete mode 100644 notifications/src/main/java/org/openedx/notifications/data/repository/delete_me.kt create mode 100644 notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt delete mode 100644 notifications/src/main/java/org/openedx/notifications/domain/interactor/Notificationsinteractor.kt create mode 100644 notifications/src/main/java/org/openedx/notifications/domain/model/NotificationsCount.kt 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 39f392543..fea99a467 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -7,7 +7,6 @@ import com.google.android.play.core.review.ReviewManagerFactory import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.Dispatchers -import org.openedx.notifications.PushManager import org.koin.android.ext.koin.androidApplication import org.koin.core.qualifier.named import org.koin.dsl.module @@ -48,6 +47,7 @@ import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter import org.openedx.core.system.AppCookieManager import org.openedx.core.system.CalendarManager import org.openedx.core.system.PushGlobalManager +import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier @@ -67,6 +67,7 @@ import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter import org.openedx.discussion.system.notifier.DiscussionNotifier +import org.openedx.notifications.PushManager import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter @@ -115,6 +116,7 @@ val appModule = module { single { DownloadNotifier() } single { VideoNotifier() } single { DiscoveryNotifier() } + single { PushNotifier() } single { IAPNotifier() } single { AppRouter() } @@ -206,6 +208,7 @@ val appModule = module { single { get() } single { get() } + single { PushManager(get()) } single { get() } factory { AgreementProvider(get(), get()) } diff --git a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt index be6b18916..8736acaa4 100644 --- a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt +++ b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt @@ -4,7 +4,6 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.core.qualifier.named import org.koin.dsl.module -import org.openedx.app.data.api.NotificationsApi import org.openedx.app.data.networking.AppUpgradeInterceptor import org.openedx.app.data.networking.HandleErrorInterceptor import org.openedx.app.data.networking.HeadersInterceptor @@ -17,6 +16,7 @@ import org.openedx.core.data.api.CourseApi import org.openedx.core.data.api.iap.InAppPurchasesApi import org.openedx.discovery.data.api.DiscoveryApi import org.openedx.discussion.data.api.DiscussionApi +import org.openedx.notifications.data.api.NotificationsApi import org.openedx.profile.data.api.ProfileApi import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory 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 516856208..03c668c52 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -58,6 +58,8 @@ import org.openedx.discussion.presentation.threads.DiscussionAddThreadViewModel import org.openedx.discussion.presentation.threads.DiscussionThreadsViewModel 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.profile.data.repository.ProfileRepository import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account @@ -152,6 +154,7 @@ val screenModule = module { get(), get(), get(), + get(), ) } @@ -169,11 +172,12 @@ val screenModule = module { get(), get(), windowSize, + get(), ) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { (openTab: String) -> - LearnViewModel(openTab, get(), get(), get()) + LearnViewModel(openTab, get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } @@ -478,6 +482,9 @@ val screenModule = module { ) } + single { NotificationsRepository(get()) } + factory { NotificationsInteractor(get()) } + single { IAPRepository(get()) } factory { IAPInteractor(get(), get(), get(), get(), get()) } viewModel { (purchaseFlowData: PurchaseFlowData) -> diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index 6c495a569..184e6f0ea 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -103,6 +103,10 @@ class Config(context: Context) { return getObjectOrNewInstance(DASHBOARD, DashboardConfig::class.java) } + fun isPushNotificationsEnabled(): Boolean { + return getBoolean(PUSH_NOTIFICATIONS_ENABLED, false) + } + fun getBranchConfig(): BranchConfig { return getObjectOrNewInstance(BRANCH, BranchConfig::class.java) } @@ -176,6 +180,7 @@ class Config(context: Context) { private const val DISCOVERY = "DISCOVERY" private const val PROGRAM = "PROGRAM" private const val DASHBOARD = "DASHBOARD" + private const val PUSH_NOTIFICATIONS_ENABLED = "PUSH_NOTIFICATIONS_ENABLED" private const val BRANCH = "BRANCH" private const val UI_COMPONENTS = "UI_COMPONENTS" private const val PLATFORM_NAME = "PLATFORM_NAME" diff --git a/core/src/main/java/org/openedx/core/system/PushGlobalManager.kt b/core/src/main/java/org/openedx/core/system/PushGlobalManager.kt index 861e0882b..9278dede9 100644 --- a/core/src/main/java/org/openedx/core/system/PushGlobalManager.kt +++ b/core/src/main/java/org/openedx/core/system/PushGlobalManager.kt @@ -1,3 +1,5 @@ package org.openedx.core.system -interface PushGlobalManager +interface PushGlobalManager { + suspend fun getUnreadNotificationsCount(): Int +} diff --git a/core/src/main/java/org/openedx/core/system/notifier/PushEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/PushEvent.kt new file mode 100644 index 000000000..7199785a0 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/PushEvent.kt @@ -0,0 +1,7 @@ +package org.openedx.core.system.notifier + +import org.openedx.core.system.notifier.app.AppEvent + +sealed class PushEvent : AppEvent { + data object RefreshBadgeCount : PushEvent() +} diff --git a/core/src/main/java/org/openedx/core/system/notifier/PushNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/PushNotifier.kt new file mode 100644 index 000000000..bd6e7d1fe --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/PushNotifier.kt @@ -0,0 +1,13 @@ +package org.openedx.core.system.notifier + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class PushNotifier { + private val channel = MutableSharedFlow(replay = 0, extraBufferCapacity = 0) + + val notifier: Flow = channel.asSharedFlow() + + suspend fun send(event: PushEvent.RefreshBadgeCount) = channel.emit(event) +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index cc7db920f..7958694e9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -139,6 +139,7 @@ fun DashboardGalleryView( when (action) { DashboardGalleryScreenAction.SwipeRefresh -> { viewModel.updateCourses() + viewModel.refreshPushBadgeCount() } DashboardGalleryScreenAction.ViewAll -> { 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 7ceab5669..78631b9b7 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -40,6 +40,8 @@ import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.IAPNotifier import org.openedx.core.system.notifier.NavigationToDiscovery +import org.openedx.core.system.notifier.PushEvent +import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.ui.WindowSize import org.openedx.core.utils.FileUtil @@ -57,9 +59,10 @@ class DashboardGalleryViewModel( private val fileUtil: FileUtil, private val dashboardRouter: DashboardRouter, private val iapNotifier: IAPNotifier, + private val pushNotifier: PushNotifier, private val iapInteractor: IAPInteractor, - private val iapAnalytics: IAPAnalytics, private val windowSize: WindowSize, + iapAnalytics: IAPAnalytics, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -147,6 +150,10 @@ class DashboardGalleryViewModel( getCourses(isIAPFlow = isIAPFlow) } + fun refreshPushBadgeCount() { + viewModelScope.launch { pushNotifier.send(PushEvent.RefreshBadgeCount) } + } + fun navigateToDiscovery() { viewModelScope.launch { discoveryNotifier.send(NavigationToDiscovery()) } } 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 0043a8d2c..41ea04a09 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -157,6 +157,7 @@ class DashboardListFragment : Fragment() { }, onSwipeRefresh = { viewModel.updateCourses() + viewModel.refreshPushBadgeCount() }, paginationCallback = { viewModel.fetchMore() 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 028497268..bb54964ed 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -40,6 +40,8 @@ import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.IAPNotifier +import org.openedx.core.system.notifier.PushEvent +import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent @@ -54,11 +56,12 @@ class DashboardListViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val iapNotifier: IAPNotifier, + private val pushNotifier: PushNotifier, private val analytics: DashboardAnalytics, private val appNotifier: AppNotifier, private val preferencesManager: CorePreferences, - private val iapAnalytics: IAPAnalytics, private val iapInteractor: IAPInteractor, + iapAnalytics: IAPAnalytics, ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -174,6 +177,10 @@ class DashboardListViewModel( } } + fun refreshPushBadgeCount() { + viewModelScope.launch { pushNotifier.send(PushEvent.RefreshBadgeCount) } + } + fun processIAPAction( fragmentManager: FragmentManager, action: IAPAction, diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index b9962599e..e5fdbeb77 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -29,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -74,10 +76,13 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { }, false ) Header( - selectedLearnType = uiState.learnType, + uiState = uiState, onUpdateLearnType = { learnType -> viewModel.updateLearnType(learnType) }, + onNotificationBadgeClick = { + viewModel.onNotificationBadgeClick() + } ) } } @@ -111,8 +116,9 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { @Composable private fun Header( - selectedLearnType: LearnType, - onUpdateLearnType: (LearnType) -> Unit + uiState: LearnUIState, + onUpdateLearnType: (LearnType) -> Unit, + onNotificationBadgeClick: () -> Unit, ) { val viewModel: LearnViewModel = koinViewModel() val windowSize = rememberWindowSize() @@ -135,13 +141,16 @@ private fun Header( ) { Title( label = stringResource(id = R.string.dashboard_learn), + showNotificationIcon = uiState.showNotificationIcon, + hasUnreadNotifications = uiState.hasUnreadNotifications, + onNotificationBadgeClick = onNotificationBadgeClick ) if (viewModel.isProgramTypeWebView) { LearnDropdownMenu( modifier = Modifier .align(Alignment.Start) .padding(horizontal = 16.dp), - selectedLearnType = selectedLearnType, + selectedLearnType = uiState.learnType, onUpdateLearnType = onUpdateLearnType ) } @@ -152,6 +161,9 @@ private fun Header( private fun Title( modifier: Modifier = Modifier, label: String, + showNotificationIcon: Boolean = false, + hasUnreadNotifications: Boolean = false, + onNotificationBadgeClick: () -> Unit, ) { Box( modifier = modifier.fillMaxWidth() @@ -164,6 +176,27 @@ private fun Title( color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.headlineBold ) + if (showNotificationIcon) { + IconButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 12.dp), + onClick = { + onNotificationBadgeClick() + } + ) { + val notificationIcon = if (hasUnreadNotifications) { + R.drawable.dashboard_ic_notification_bubble_badge + } else { + R.drawable.dashboard_ic_notification_badge + } + Icon( + painter = painterResource(id = notificationIcon), + tint = Color.Unspecified, + contentDescription = stringResource(id = R.string.dashboard_notification_badge) + ) + } + } } } @@ -249,7 +282,9 @@ private fun LearnDropdownMenu( @Composable private fun HeaderPreview() { OpenEdXTheme { - Title(label = stringResource(id = R.string.dashboard_learn)) + Title(label = stringResource(id = R.string.dashboard_learn), + showNotificationIcon = true, + onNotificationBadgeClick = {}) } } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt index 934caa374..c1094d82c 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt @@ -2,4 +2,8 @@ package org.openedx.learn.presentation import org.openedx.learn.LearnType -data class LearnUIState(val learnType: LearnType) +data class LearnUIState( + val learnType: LearnType, + val showNotificationIcon: Boolean = false, + val hasUnreadNotifications: Boolean = 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 ff94f2811..50cb75e37 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -1,6 +1,7 @@ package org.openedx.learn.presentation import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -9,6 +10,9 @@ import kotlinx.coroutines.launch import org.openedx.DashboardNavigator import org.openedx.core.BaseViewModel import org.openedx.core.config.Config +import org.openedx.core.system.PushGlobalManager +import org.openedx.core.system.notifier.PushEvent +import org.openedx.core.system.notifier.PushNotifier import org.openedx.dashboard.presentation.DashboardAnalytics import org.openedx.dashboard.presentation.DashboardAnalyticsEvent import org.openedx.dashboard.presentation.DashboardAnalyticsKey @@ -20,6 +24,8 @@ class LearnViewModel( private val config: Config, private val dashboardRouter: DashboardRouter, private val analytics: DashboardAnalytics, + private val pushManager: PushGlobalManager, + private val pushNotifier: PushNotifier ) : BaseViewModel() { private val _uiState = MutableStateFlow( LearnUIState( @@ -27,7 +33,8 @@ class LearnViewModel( LearnType.PROGRAMS } else { LearnType.COURSES - } + }, + showNotificationIcon = config.isPushNotificationsEnabled() ) ) @@ -43,14 +50,24 @@ class LearnViewModel( init { viewModelScope.launch { - _uiState.collect { uiState -> - if (uiState.learnType == LearnType.COURSES) { - logMyCoursesTabClickedEvent() - } else { - logMyProgramsTabClickedEvent() + launch { + _uiState.collect { uiState -> + if (uiState.learnType == LearnType.COURSES) { + logMyCoursesTabClickedEvent() + } else { + logMyProgramsTabClickedEvent() + } + } + } + launch { + pushNotifier.notifier.collect { event -> + if (event is PushEvent.RefreshBadgeCount) { + checkNotificationCount() + } } } } + checkNotificationCount() } fun updateLearnType(learnType: LearnType) { @@ -59,6 +76,19 @@ class LearnViewModel( } } + private fun checkNotificationCount() { + if (config.isPushNotificationsEnabled()) { + viewModelScope.launch(Dispatchers.IO) { + val unreadNotifications = pushManager.getUnreadNotificationsCount() + _uiState.update { it.copy(hasUnreadNotifications = unreadNotifications > 0) } + } + } + } + + fun onNotificationBadgeClick() { + _uiState.update { it.copy(hasUnreadNotifications = false) } + } + private fun logMyCoursesTabClickedEvent() { logScreenEvent(DashboardAnalyticsEvent.MY_COURSES) } diff --git a/dashboard/src/main/res/drawable-night/dashboard_ic_notification_badge.xml b/dashboard/src/main/res/drawable-night/dashboard_ic_notification_badge.xml new file mode 100644 index 000000000..a3d0ad056 --- /dev/null +++ b/dashboard/src/main/res/drawable-night/dashboard_ic_notification_badge.xml @@ -0,0 +1,9 @@ + + + diff --git a/dashboard/src/main/res/drawable-night/dashboard_ic_notification_bubble_badge.xml b/dashboard/src/main/res/drawable-night/dashboard_ic_notification_bubble_badge.xml new file mode 100644 index 000000000..73e0b5fe2 --- /dev/null +++ b/dashboard/src/main/res/drawable-night/dashboard_ic_notification_bubble_badge.xml @@ -0,0 +1,13 @@ + + + + diff --git a/dashboard/src/main/res/drawable/dashboard_ic_notification_badge.xml b/dashboard/src/main/res/drawable/dashboard_ic_notification_badge.xml new file mode 100644 index 000000000..2df5b8ffb --- /dev/null +++ b/dashboard/src/main/res/drawable/dashboard_ic_notification_badge.xml @@ -0,0 +1,9 @@ + + + diff --git a/dashboard/src/main/res/drawable/dashboard_ic_notification_bubble_badge.xml b/dashboard/src/main/res/drawable/dashboard_ic_notification_bubble_badge.xml new file mode 100644 index 000000000..a632f4b5d --- /dev/null +++ b/dashboard/src/main/res/drawable/dashboard_ic_notification_bubble_badge.xml @@ -0,0 +1,13 @@ + + + + diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index b10b8ca0f..0a37071a8 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ You are not currently enrolled in any courses, would you like to explore the course catalog? Find a Course No %1$s Courses + Notification Inbox %1$d Past Due Assignment diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index c0455c763..17140af86 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -36,12 +36,14 @@ import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.IAPConfig import org.openedx.core.domain.model.Pagination import org.openedx.core.presentation.IAPAnalytics +import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.IAPNotifier +import org.openedx.core.system.notifier.PushEvent import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor import java.net.UnknownHostException @@ -61,6 +63,7 @@ class DashboardViewModelTest { private val networkConnection = mockk() private val discoveryNotifier = mockk() private val iapNotifier = mockk() + private val pushNotifier = mockk() private val analytics = mockk() private val appNotifier = mockk() private val iapAnalytics = mockk() @@ -112,11 +115,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() advanceUntilIdle() @@ -143,11 +147,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } throws Exception() @@ -175,11 +180,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList @@ -207,11 +213,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( @@ -248,11 +255,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) advanceUntilIdle() @@ -278,11 +286,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() @@ -312,11 +321,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) coEvery { interactor.getEnrolledCourses(any()) } throws Exception() @@ -340,6 +350,7 @@ class DashboardViewModelTest { coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList coEvery { iapNotifier.notifier } returns flow { emit(CourseDataUpdated()) } coEvery { iapNotifier.send(any()) } returns Unit + coEvery { pushNotifier.send(any()) } returns Unit val viewModel = DashboardListViewModel( context, @@ -349,11 +360,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) viewModel.updateCourses() @@ -383,6 +395,7 @@ class DashboardViewModelTest { ) coEvery { iapNotifier.notifier } returns flow { emit(CourseDataUpdated()) } coEvery { iapNotifier.send(any()) } returns Unit + coEvery { pushNotifier.send(any()) } returns Unit val viewModel = DashboardListViewModel( context, @@ -392,11 +405,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) viewModel.updateCourses() @@ -425,11 +439,12 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, iapNotifier, + pushNotifier, analytics, appNotifier, corePreferences, + iapInteractor, iapAnalytics, - iapInteractor ) val mockLifeCycleOwner: LifecycleOwner = mockk() diff --git a/notifications/src/main/java/org/openedx/notifications/PushManager.kt b/notifications/src/main/java/org/openedx/notifications/PushManager.kt index d4a6059cd..08f8f0b28 100644 --- a/notifications/src/main/java/org/openedx/notifications/PushManager.kt +++ b/notifications/src/main/java/org/openedx/notifications/PushManager.kt @@ -1,5 +1,11 @@ package org.openedx.notifications import org.openedx.core.system.PushGlobalManager +import org.openedx.notifications.domain.interactor.NotificationsInteractor -class PushManager : PushGlobalManager +class PushManager(private val interactor: NotificationsInteractor) : PushGlobalManager { + + override suspend fun getUnreadNotificationsCount(): Int { + return interactor.getUnreadNotificationsCount().discussion + } +} diff --git a/notifications/src/main/java/org/openedx/notifications/data/api/APIConstants.kt b/notifications/src/main/java/org/openedx/notifications/data/api/APIConstants.kt new file mode 100644 index 000000000..8b0fdb6d5 --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/data/api/APIConstants.kt @@ -0,0 +1,5 @@ +package org.openedx.notifications.data.api + +object APIConstants { + const val NOTIFICATION_COUNT = "/api/notifications/count/" +} diff --git a/notifications/src/main/java/org/openedx/notifications/data/api/NotificationsApi.kt b/notifications/src/main/java/org/openedx/notifications/data/api/NotificationsApi.kt index 5abe2c311..9b7dee56b 100644 --- a/notifications/src/main/java/org/openedx/notifications/data/api/NotificationsApi.kt +++ b/notifications/src/main/java/org/openedx/notifications/data/api/NotificationsApi.kt @@ -1 +1,9 @@ -package org.openedx.notifications.data.api \ No newline at end of file +package org.openedx.notifications.data.api + +import org.openedx.notifications.data.model.NotificationsCountResponse +import retrofit2.http.GET + +interface NotificationsApi { + @GET(APIConstants.NOTIFICATION_COUNT) + suspend fun getUnreadNotificationsCount(): NotificationsCountResponse +} diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/CountByAppNameModel.kt b/notifications/src/main/java/org/openedx/notifications/data/model/CountByAppNameModel.kt new file mode 100644 index 000000000..38684337b --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/data/model/CountByAppNameModel.kt @@ -0,0 +1,12 @@ +package org.openedx.notifications.data.model + +import com.google.gson.annotations.SerializedName + +data class CountByAppNameModel( + @SerializedName("discussion") + var discussion: Int, + @SerializedName("updates") + var updates: Int, + @SerializedName("grading") + var grading: Int, +) diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/NotificaitonCountResponse.kt b/notifications/src/main/java/org/openedx/notifications/data/model/NotificaitonCountResponse.kt new file mode 100644 index 000000000..4b2f07ea8 --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/data/model/NotificaitonCountResponse.kt @@ -0,0 +1,23 @@ +package org.openedx.notifications.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.notifications.domain.model.NotificationsCount + +data class NotificationsCountResponse( + @SerializedName("show_notifications_tray") + var showNotificationsTray: Boolean, + @SerializedName("count") + var count: Int, + @SerializedName("count_by_app_name") + var countByAppName: CountByAppNameModel, + @SerializedName("notification_expiry_days") + var notificationExpiryDays: Int, + @SerializedName("is_new_notification_view_enabled") + var isNewNotificationViewEnabled: Boolean, +) { + fun mapToDomain(): NotificationsCount { + return NotificationsCount( + discussion = countByAppName.discussion, + ) + } +} diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/delete_me.kt b/notifications/src/main/java/org/openedx/notifications/data/model/delete_me.kt deleted file mode 100644 index b38cd0b61..000000000 --- a/notifications/src/main/java/org/openedx/notifications/data/model/delete_me.kt +++ /dev/null @@ -1 +0,0 @@ -package org.openedx.notifications.data.model \ No newline at end of file diff --git a/notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt b/notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt new file mode 100644 index 000000000..51a7c744f --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt @@ -0,0 +1,10 @@ +package org.openedx.notifications.data.repository + +import org.openedx.notifications.data.api.NotificationsApi +import org.openedx.notifications.domain.model.NotificationsCount + +class NotificationsRepository(private val api: NotificationsApi) { + suspend fun getUnreadNotificationsCount(): NotificationsCount { + return api.getUnreadNotificationsCount().mapToDomain() + } +} diff --git a/notifications/src/main/java/org/openedx/notifications/data/repository/delete_me.kt b/notifications/src/main/java/org/openedx/notifications/data/repository/delete_me.kt deleted file mode 100644 index 1934872aa..000000000 --- a/notifications/src/main/java/org/openedx/notifications/data/repository/delete_me.kt +++ /dev/null @@ -1 +0,0 @@ -package org.openedx.notifications.data.repository \ No newline at end of file diff --git a/notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt b/notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt new file mode 100644 index 000000000..dccad003f --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt @@ -0,0 +1,11 @@ +package org.openedx.notifications.domain.interactor + +import org.openedx.notifications.data.repository.NotificationsRepository +import org.openedx.notifications.domain.model.NotificationsCount + +class NotificationsInteractor(private val repository: NotificationsRepository) { + + suspend fun getUnreadNotificationsCount(): NotificationsCount { + return repository.getUnreadNotificationsCount() + } +} diff --git a/notifications/src/main/java/org/openedx/notifications/domain/interactor/Notificationsinteractor.kt b/notifications/src/main/java/org/openedx/notifications/domain/interactor/Notificationsinteractor.kt deleted file mode 100644 index 4e2e1ff5a..000000000 --- a/notifications/src/main/java/org/openedx/notifications/domain/interactor/Notificationsinteractor.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.openedx.notifications.domain.interactor - diff --git a/notifications/src/main/java/org/openedx/notifications/domain/model/NotificationsCount.kt b/notifications/src/main/java/org/openedx/notifications/domain/model/NotificationsCount.kt new file mode 100644 index 000000000..1aa9f4315 --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/domain/model/NotificationsCount.kt @@ -0,0 +1,5 @@ +package org.openedx.notifications.domain.model + +data class NotificationsCount( + var discussion: Int, +)