diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index c88b96edc..44de2d2f0 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -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.NotificationsRouter import org.openedx.notifications.presentation.inbox.NotificationsInboxFragment import org.openedx.notifications.presentation.settings.NotificationsSettingsFragment import org.openedx.profile.domain.model.Account @@ -58,7 +59,7 @@ import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, - ProfileRouter, AppUpgradeRouter, WhatsNewRouter { + ProfileRouter, AppUpgradeRouter, WhatsNewRouter, NotificationsRouter { //region AuthRouter override fun navigateToMain( 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 6ed25670a..9619be3fb 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -70,6 +70,7 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.notifications.PushManager import org.openedx.notifications.data.storage.NotificationsPreferences import org.openedx.notifications.presentation.NotificationsAnalytics +import org.openedx.notifications.presentation.NotificationsRouter import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter @@ -132,6 +133,7 @@ val appModule = module { single { get() } single { get() } single { DeepLinkRouter(get(), get(), get(), get(), get(), get()) } + single { 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 a336f705e..dfee6aee3 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -487,7 +487,7 @@ val screenModule = module { single { NotificationsRepository(get()) } factory { NotificationsInteractor(get()) } - viewModel { NotificationsInboxViewModel(get(), get()) } + viewModel { NotificationsInboxViewModel(get(), get(), get()) } viewModel { NotificationsSettingsViewModel(get(), get(), get()) } single { IAPRepository(get()) } diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/MarkNotificationReadBody.kt b/notifications/src/main/java/org/openedx/notifications/data/model/MarkNotificationReadBody.kt index 5036f9fc7..6a0513476 100644 --- a/notifications/src/main/java/org/openedx/notifications/data/model/MarkNotificationReadBody.kt +++ b/notifications/src/main/java/org/openedx/notifications/data/model/MarkNotificationReadBody.kt @@ -3,6 +3,8 @@ package org.openedx.notifications.data.model import com.google.gson.annotations.SerializedName data class MarkNotificationReadBody( + @SerializedName("app_name") + val appName: String, @SerializedName("notification_id") - val notificationId: Int, + val notificationId: Int?, ) diff --git a/notifications/src/main/java/org/openedx/notifications/data/model/NotificationsMenuType.kt b/notifications/src/main/java/org/openedx/notifications/data/model/NotificationsMenuType.kt new file mode 100644 index 000000000..6bb0c1163 --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/data/model/NotificationsMenuType.kt @@ -0,0 +1,9 @@ +package org.openedx.notifications.data.model + +import androidx.annotation.StringRes +import org.openedx.notifications.R + +enum class NotificationsMenuType(@StringRes val title: Int) { + MARK_ALL_READ(R.string.notifications_menu_mark_all_read), + NOTIFICATION_SETTINGS(R.string.notifications_menu_settings), +} 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 index b4b3b3ff5..faa269dc0 100644 --- a/notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt +++ b/notifications/src/main/java/org/openedx/notifications/data/repository/NotificationsRepository.kt @@ -30,9 +30,10 @@ class NotificationsRepository( ).message.isNotNull() } - suspend fun markNotificationAsRead(notificationId: Int): Boolean { + suspend fun markNotificationAsRead(notificationId: Int?): Boolean { return api.markNotificationAsRead( MarkNotificationReadBody( + appName = APIConstants.APP_NAME_DISCUSSION, notificationId = notificationId, ) ).message.isNotNull() 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 index 34fd6799d..2661dcfb5 100644 --- a/notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt +++ b/notifications/src/main/java/org/openedx/notifications/domain/interactor/NotificationsInteractor.kt @@ -36,4 +36,8 @@ class NotificationsInteractor( isDiscussionPushEnabled = isDiscussionPushEnabled, ) } + + suspend fun markAllNotificationsAsRead(): Boolean { + return repository.markNotificationAsRead(notificationId = null) + } } diff --git a/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt b/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt new file mode 100644 index 000000000..c9baf57eb --- /dev/null +++ b/notifications/src/main/java/org/openedx/notifications/presentation/NotificationsRouter.kt @@ -0,0 +1,7 @@ +package org.openedx.notifications.presentation + +import androidx.fragment.app.FragmentManager + +interface NotificationsRouter { + fun navigateToPushNotificationsSettings(fm: FragmentManager) +} 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 a51b1d1f1..75cc9f646 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 @@ -21,9 +21,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator +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.Scaffold import androidx.compose.material.Surface @@ -39,6 +41,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -63,15 +66,19 @@ import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OpenEdXPrimaryButton import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType +import org.openedx.core.ui.crop import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.shouldLoadMore import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.AppShapes import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.notifications.R +import org.openedx.notifications.data.model.NotificationsMenuType import org.openedx.notifications.domain.model.InboxSection import org.openedx.notifications.domain.model.NotificationContent import org.openedx.notifications.domain.model.NotificationItem @@ -104,8 +111,16 @@ class NotificationsInboxFragment : Fragment() { onBackClick = { requireActivity().supportFragmentManager.popBackStack() }, - onSettingsClick = { + onSettingsClick = { menuType -> + when (menuType) { + NotificationsMenuType.MARK_ALL_READ -> { + viewModel.markAllNotificationsAsRead() + } + NotificationsMenuType.NOTIFICATION_SETTINGS -> { + viewModel.navigateToPushNotificationsSettings(requireActivity().supportFragmentManager) + } + } }, onReloadNotifications = { viewModel.onReloadNotifications() @@ -132,7 +147,7 @@ private fun InboxView( uiMessage: UIMessage?, canLoadMore: Boolean, onBackClick: () -> Unit, - onSettingsClick: () -> Unit, + onSettingsClick: (NotificationsMenuType) -> Unit, onReloadNotifications: () -> Unit, paginationCallBack: () -> Unit, markNotificationAsRead: (notificationItem: NotificationItem, inboxSection: InboxSection) -> Unit, @@ -267,7 +282,7 @@ private fun InboxView( private fun Header( modifier: Modifier = Modifier, onBackClick: () -> Unit, - onSettingsClick: () -> Unit, + onSettingsClick: (NotificationsMenuType) -> Unit, ) { Box( modifier = modifier @@ -291,17 +306,14 @@ private fun Header( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, ) + NotificationsDropdownMenu( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 16.dp), + onItemClick = onSettingsClick + ) + - IconButton( - modifier = Modifier.align(Alignment.CenterEnd), - onClick = { onSettingsClick() } - ) { - Icon( - imageVector = Icons.Default.MoreVert, - tint = MaterialTheme.appColors.primary, - contentDescription = stringResource(id = R.string.notifications_accessibility_settings) - ) - } } } @@ -324,6 +336,63 @@ private fun SectionHeader( } } +@Composable +private fun NotificationsDropdownMenu( + modifier: Modifier = Modifier, + onItemClick: (NotificationsMenuType) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + Column( + modifier = modifier.padding(vertical = 8.dp) + ) { + Row( + modifier = Modifier + .clickable { + expanded = true + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = MaterialTheme.appColors.primary, + contentDescription = stringResource(id = R.string.notifications_accessibility_settings) + ) + } + + MaterialTheme( + colors = MaterialTheme.colors.copy(surface = MaterialTheme.appColors.background), + shapes = MaterialTheme.shapes.copy(MaterialTheme.appShapes.material.medium) + ) { + DropdownMenu( + modifier = Modifier + .crop(vertical = 8.dp) + .widthIn(min = 182.dp) + .background(MaterialTheme.appColors.surface), + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + for (menuItem in NotificationsMenuType.entries) { + DropdownMenuItem( + modifier = Modifier + .background(MaterialTheme.appColors.surface), + onClick = { + onItemClick(menuItem) + expanded = false + } + ) { + Text( + text = stringResource(id = menuItem.title), + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textPrimary + ) + } + } + } + } + } +} + @Composable private fun NotificationItemView( modifier: Modifier = Modifier, 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 a4845ec33..a1b50c5f1 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 @@ -1,5 +1,6 @@ package org.openedx.notifications.presentation.inbox +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -13,11 +14,13 @@ import org.openedx.core.system.ResourceManager import org.openedx.notifications.domain.interactor.NotificationsInteractor import org.openedx.notifications.domain.model.InboxSection import org.openedx.notifications.domain.model.NotificationItem +import org.openedx.notifications.presentation.NotificationsRouter import java.util.Date import org.openedx.core.R as coreR class NotificationsInboxViewModel( private val interactor: NotificationsInteractor, + private val notificationsRouter: NotificationsRouter, private val resourceManager: ResourceManager, ) : BaseViewModel() { @@ -114,7 +117,7 @@ class NotificationsInboxViewModel( ) { viewModelScope.launch { try { - if (notification.isUnread() && interactor.markNotificationAsRead(notification.id)) { + if (interactor.markNotificationAsRead(notification.id)) { val currentSection = notifications[inboxSection] ?: return@launch val index = currentSection.indexOfFirst { it.id == notification.id } @@ -150,4 +153,26 @@ class NotificationsInboxViewModel( ) } } + + fun navigateToPushNotificationsSettings(fm: FragmentManager) { + notificationsRouter.navigateToPushNotificationsSettings(fm) + } + + fun markAllNotificationsAsRead() { + viewModelScope.launch { + try { + interactor.markAllNotificationsAsRead() + + notifications.forEach { (section, notificationItems) -> + notifications[section] = + notificationItems.map { it.copy(lastRead = Date()) }.toMutableList() + } + + _uiState.value = InboxUIState.Data(notifications = notifications.toMap()) + } catch (e: Exception) { + e.printStackTrace() + emitErrorMessage(e) + } + } + } } diff --git a/notifications/src/main/res/values/strings.xml b/notifications/src/main/res/values/strings.xml index 979fe6fbd..3364c2937 100644 --- a/notifications/src/main/res/values/strings.xml +++ b/notifications/src/main/res/values/strings.xml @@ -8,6 +8,9 @@ Now No notifications yet When you receive notifications they’ll show up here + + Notification Settings + Mark all as read %1$s second %1$s seconds