From 4d2cb5badaa23359832febbb5315f4dc85969aec Mon Sep 17 00:00:00 2001 From: urFate Date: Tue, 16 Jan 2024 22:06:32 +0300 Subject: [PATCH 1/5] feat: implement notifications system --- .../core/entity/NotificationEntity.kt | 13 ++ .../entity/relation/NotificationAndContent.kt | 16 ++ .../shirabox/shirabox/NotificationService.kt | 86 ++++++++++ .../live/shirabox/shirabox/db/AppDatabase.kt | 6 +- .../shirabox/db/dao/NotificationDao.kt | 34 ++++ .../shirabox/ui/activity/MainActivity.kt | 51 ++++++ .../ui/activity/resource/ResourceViewModel.kt | 18 ++- .../ui/component/general/Notifications.kt | 81 ++++++++++ .../ui/component/navigation/ShiraNavHost.kt | 4 +- .../ui/screen/explore/NotificationsScreen.kt | 69 -------- .../notifications/NotificationsScreen.kt | 149 ++++++++++++++++++ .../notifications/NotificationsViewModel.kt | 41 +++++ 12 files changed, 493 insertions(+), 75 deletions(-) create mode 100644 app/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt create mode 100644 app/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt create mode 100644 app/src/main/java/live/shirabox/shirabox/NotificationService.kt create mode 100644 app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt create mode 100644 app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt delete mode 100644 app/src/main/java/live/shirabox/shirabox/ui/screen/explore/NotificationsScreen.kt create mode 100644 app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt create mode 100644 app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt diff --git a/app/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt b/app/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt new file mode 100644 index 0000000..ea750a6 --- /dev/null +++ b/app/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt @@ -0,0 +1,13 @@ +package live.shirabox.core.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "notification") +data class NotificationEntity( + @PrimaryKey(autoGenerate = true) val uid: Int = 0, + @ColumnInfo(name = "content_code") val contentCode: String, + @ColumnInfo(name = "receive_timestamp") val receiveTimestamp: Long, + @ColumnInfo(name = "text") val text: String +) \ No newline at end of file diff --git a/app/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt b/app/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt new file mode 100644 index 0000000..03f9a77 --- /dev/null +++ b/app/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt @@ -0,0 +1,16 @@ +package live.shirabox.core.entity.relation + +import androidx.room.Embedded +import androidx.room.Relation +import live.shirabox.core.entity.ContentEntity +import live.shirabox.core.entity.NotificationEntity + +data class NotificationAndContent ( + @Embedded + val notificationEntity: NotificationEntity, + @Relation( + parentColumn = "content_code", + entityColumn = "code" + ) + val contentEntity: ContentEntity +) \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/NotificationService.kt b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt new file mode 100644 index 0000000..93d1622 --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt @@ -0,0 +1,86 @@ +package live.shirabox.shirabox + +import android.Manifest +import android.app.Notification +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationManagerCompat +import androidx.room.Room.databaseBuilder +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import live.shirabox.core.entity.NotificationEntity +import live.shirabox.shirabox.db.AppDatabase + + +class NotificationService : FirebaseMessagingService() { + private val mainChannelId = "SB_NOTIFICATIONS" + private val databaseName = "shirabox_db" + + private lateinit var appDatabase: AppDatabase + + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) + + override fun onNewToken(token: String) { + super.onNewToken(token) + Log.d("ShiraBoxService", "Refreshed token: $token") + } + + override fun onMessageReceived(message: RemoteMessage) { + appDatabase = databaseBuilder( + applicationContext, + AppDatabase::class.java, databaseName + ).build() + + val data = message.data + + message.notification?.let { remoteNotification -> + val title = remoteNotification.title ?: "Undefined title" + val body = remoteNotification.body?: "Undefined notification body" + + val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(this, mainChannelId).apply { + setContentTitle(title) + setContentText("ПЭЙЛОАД Payload: ${data.size}") + setSmallIcon(R.drawable.ic_stat_shirabox_notification) + setAutoCancel(true) + }.build() + } else { + Notification.Builder(this).apply { + setContentTitle(title) + setContentText("ПЭЙЛОАД Payload: $data") + setSmallIcon(R.drawable.ic_stat_shirabox_notification) + setAutoCancel(true) + }.build() + } + + if (ActivityCompat + .checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED + ) NotificationManagerCompat.from(this).notify(1, notification) + + // Save notification into the database + + Log.d("ShiraBoxService","NOTIFICATION_CODE: ${data["code"]}") + + data["code"]?.let { + scope.launch(Dispatchers.IO) { + val appDatabase = AppDatabase.getAppDataBase(this@NotificationService)!! + Log.d("ShiraBoxService", "WE ADDING IT TO THE DATABASE") + + appDatabase.notificationDao().insertNotification(NotificationEntity( + contentCode = it, + receiveTimestamp = System.currentTimeMillis(), + text = body + )) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt b/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt index 017df9e..6f48e49 100644 --- a/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt +++ b/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt @@ -7,13 +7,16 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import live.shirabox.core.entity.ContentEntity import live.shirabox.core.entity.EpisodeEntity +import live.shirabox.core.entity.NotificationEntity import live.shirabox.core.entity.RelatedContentEntity import live.shirabox.shirabox.db.dao.ContentDao import live.shirabox.shirabox.db.dao.EpisodeDao +import live.shirabox.shirabox.db.dao.NotificationDao import live.shirabox.shirabox.db.dao.RelatedDao @Database( - entities = [ContentEntity::class, EpisodeEntity::class, RelatedContentEntity::class], + entities = [ContentEntity::class, EpisodeEntity::class, + RelatedContentEntity::class, NotificationEntity::class], version = 1 ) @TypeConverters(Converters::class) @@ -21,6 +24,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun contentDao(): ContentDao abstract fun episodeDao(): EpisodeDao abstract fun relatedDao(): RelatedDao + abstract fun notificationDao(): NotificationDao companion object { private var INSTANCE: AppDatabase? = null diff --git a/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt new file mode 100644 index 0000000..cdf851d --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt @@ -0,0 +1,34 @@ +package live.shirabox.shirabox.db.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import live.shirabox.core.entity.NotificationEntity +import live.shirabox.core.entity.relation.NotificationAndContent + +@Dao +interface NotificationDao { + @Query("SELECT * FROM notification") + fun all(): Flow> + + @Transaction + @Query("SELECT * FROM notification WHERE content_code IS :code") + fun notificationWithContent(code: String): NotificationAndContent + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insertNotification(vararg notificationEntity: NotificationEntity) + + @Update + fun updateNotification(vararg notificationEntity: NotificationEntity) + + @Delete + fun deleteNotification(vararg notificationEntity: NotificationEntity) + + @Query("DELETE FROM notification") + fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt index f8cc3e8..10d79d0 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt @@ -1,21 +1,42 @@ package live.shirabox.shirabox.ui.activity +import android.Manifest +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import live.shirabox.shirabox.ui.component.general.NotificationsDismissDialog +import live.shirabox.shirabox.ui.component.general.NotificationsRequestDialog import live.shirabox.shirabox.ui.component.navigation.BottomNavigationView import live.shirabox.shirabox.ui.theme.ShiraBoxTheme class MainActivity : ComponentActivity() { + @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val openDismissDialog = mutableStateOf(false) + + val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + if (!isGranted) { + openDismissDialog.value = true + } + } + setContent { ShiraBoxTheme { // A surface container using the 'background' color from the theme @@ -23,9 +44,39 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { + val openRequestDialog = remember { + mutableStateOf(false) + } + + askNotificationPermission( + openDialogState = openRequestDialog, + launcher = requestPermissionLauncher + ) + + NotificationsRequestDialog(isOpen = openRequestDialog) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + NotificationsDismissDialog(isOpen = openDismissDialog) + BottomNavigationView() } } } } + + private fun askNotificationPermission( + openDialogState: MutableState, + launcher: ActivityResultLauncher + ) { + // This is only necessary for API level >= 33 (TIRAMISU) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { + // Ask user for notifications permission + openDialogState.value = true + } else { + // Directly ask for the permission + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt index 9c709ee..90ff3b7 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.firebase.ktx.Firebase +import com.google.firebase.messaging.ktx.messaging import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -129,9 +131,19 @@ class ResourceViewModel(context: Context, private val contentType: ContentType) viewModelScope.launch(Dispatchers.IO) { val content = db?.contentDao()?.getContent(id) content?.let { - if (pinnedSources.contains(source.name)) pinnedSources.remove(source.name) else pinnedSources.add( - source.name - ) + val contentTopic = "${source.name.lowercase()}_${content.code}" + + when (pinnedSources.contains(source.name)) { + true -> { + pinnedSources.remove(source.name) + + Firebase.messaging.unsubscribeFromTopic(contentTopic) + } + else -> { + pinnedSources.add(source.name) + Firebase.messaging.subscribeToTopic(contentTopic) + } + } db?.contentDao()?.updateContents(it.copy(pinnedSources = pinnedSources)) } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt b/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt new file mode 100644 index 0000000..d927d3f --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt @@ -0,0 +1,81 @@ +package live.shirabox.shirabox.ui.component.general + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.NotificationsActive +import androidx.compose.material.icons.outlined.NotificationsOff +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.res.stringResource +import live.shirabox.shirabox.R + +@Composable +fun NotificationsRequestDialog(isOpen: MutableState, onConfirm: () -> Unit) { + if(isOpen.value) { + AlertDialog( + onDismissRequest = { + isOpen.value = false + }, + icon = { Icon(Icons.Outlined.NotificationsActive, contentDescription = null) }, + title = { + Text(text = stringResource(R.string.notifications_request)) + }, + text = { + Text( + stringResource(R.string.notifications_request_text) + ) + }, + confirmButton = { + TextButton( + onClick = { + isOpen.value = false + onConfirm() + } + ) { + Text(stringResource(R.string.enable_notifications)) + } + }, + dismissButton = { + TextButton( + onClick = { + isOpen.value = false + } + ) { + Text(stringResource(R.string.disable_notifications)) + } + } + ) + } +} + +@Composable +fun NotificationsDismissDialog(isOpen: MutableState) { + if(isOpen.value) { + AlertDialog( + onDismissRequest = { + isOpen.value = false + }, + icon = { Icon(Icons.Outlined.NotificationsOff, contentDescription = null) }, + title = { + Text(text = stringResource(R.string.notifications_disabled)) + }, + text = { + Text( + stringResource(R.string.notifications_disabled_text) + ) + }, + confirmButton = { + TextButton( + onClick = { + isOpen.value = false + } + ) { + Text(stringResource(R.string.notifications_disabled_confirm)) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/component/navigation/ShiraNavHost.kt b/app/src/main/java/live/shirabox/shirabox/ui/component/navigation/ShiraNavHost.kt index 9fdf6cf..2843a56 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/component/navigation/ShiraNavHost.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/component/navigation/ShiraNavHost.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import live.shirabox.shirabox.ui.screen.explore.ExploreScreen -import live.shirabox.shirabox.ui.screen.explore.NotificationsScreen +import live.shirabox.shirabox.ui.screen.explore.notifications.NotificationsScreen import live.shirabox.shirabox.ui.screen.favourites.FavouritesScreen import live.shirabox.shirabox.ui.screen.profile.ProfileScreen import live.shirabox.shirabox.ui.screen.profile.history.History @@ -22,6 +22,6 @@ fun ShiraBoxNavHost(navController: NavHostController){ composable(BottomNavItems.Profile.route) { ProfileScreen(navController) } } composable(NestedNavItems.History.route) { History(navController) } - composable(NestedNavItems.Notifications.route) { NotificationsScreen() } + composable(NestedNavItems.Notifications.route) { NotificationsScreen(navController) } } } \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/NotificationsScreen.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/NotificationsScreen.kt deleted file mode 100644 index 2e9622e..0000000 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/NotificationsScreen.kt +++ /dev/null @@ -1,69 +0,0 @@ -package live.shirabox.shirabox.ui.screen.explore - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.res.imageResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import live.shirabox.shirabox.R -import live.shirabox.shirabox.ui.component.general.ListItem -import live.shirabox.shirabox.ui.component.top.TopBar - -@Composable -fun NotificationsScreen(){ - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - TopBar(null) - - Column( - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - modifier = Modifier.padding(16.dp, 0.dp), - text = stringResource(R.string.notifications), - fontSize = 22.sp, - fontWeight = FontWeight(500) - ) - Text( - modifier = Modifier.padding(16.dp, 0.dp), - text = "05.05.2023", - fontSize = 15.sp, - fontWeight = FontWeight(500) - ) - repeat(4) { - ListItem( - headlineContent = { - Box(Modifier.fillMaxWidth()) { - Text(modifier = Modifier.align(Alignment.TopStart), - text = "Название") - Text(modifier = Modifier.align(Alignment.CenterEnd), - text = "14:32", - fontSize = 12.sp) - } - }, - supportingString = "Вышла новая серия 10 от Unknown Team!", - coverImage = ImageBitmap.imageResource(id = R.drawable.blank) - ) { - /* TODO */ - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt new file mode 100644 index 0000000..fb43b50 --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt @@ -0,0 +1,149 @@ +package live.shirabox.shirabox.ui.screen.explore.notifications + +import android.content.Intent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import live.shirabox.core.util.Util +import live.shirabox.shirabox.R +import live.shirabox.shirabox.ui.activity.resource.ResourceActivity +import live.shirabox.shirabox.ui.component.general.ListItem +import live.shirabox.shirabox.ui.component.general.NoContentsPopup +import live.shirabox.shirabox.ui.component.top.TopBar +import java.text.SimpleDateFormat +import java.util.Date + +@Composable +fun NotificationsScreen( + navController: NavController, + model: NotificationsViewModel = viewModel(factory = Util.viewModelFactory { + NotificationsViewModel(context = navController.context.applicationContext) + }) +){ + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TopBar(null) + val context = LocalContext.current + + val notifications = model.fetchNotifications().collectAsState(initial = emptyList()) + + val filteredNotificationsByDate by remember(model.notificationsWithContent) { + derivedStateOf { + val languageCode = Locale.current.language + + model.notificationsWithContent.groupBy { + SimpleDateFormat( + "dd.MM.yyyy", + java.util.Locale(languageCode) + ).format(Date(it.notificationEntity.receiveTimestamp)) + } + } + } + + LaunchedEffect(notifications) { + model.fetchNotificationsWithContent( + notifications.value.map { it.contentCode } + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp, 0.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.notifications), + fontSize = 22.sp, + fontWeight = FontWeight(500) + ) + + Button( + enabled = notifications.value.isNotEmpty(), + onClick = { + model.clearNotifications() + } + ) { + Text(stringResource(id = R.string.clear_notifications)) + } + } + + if(notifications.value.isEmpty()) { + NoContentsPopup(text = stringResource(id = R.string.no_notifications)) + } + + filteredNotificationsByDate.forEach { entry -> + Text( + modifier = Modifier.padding(16.dp, 0.dp), + text = entry.key, + fontSize = 15.sp, + fontWeight = FontWeight(500) + ) + + entry.value.forEach { + val languageCode = Locale.current.language + + val time = SimpleDateFormat( + "H:mm", + java.util.Locale(languageCode) + ).format(Date(it.notificationEntity.receiveTimestamp)) + + ListItem( + headlineContent = { + Box(Modifier.fillMaxWidth()) { + Text(modifier = Modifier.align(Alignment.TopStart), + text = it.contentEntity.name) + Text(modifier = Modifier.align(Alignment.CenterEnd), + text = time, + fontSize = 12.sp) + } + }, + supportingString = it.notificationEntity.text, + coverImage = it.contentEntity.image + ) { + context.startActivity( + Intent( + context, + ResourceActivity::class.java + ).apply { + putExtra("id", it.contentEntity.shikimoriID) + putExtra("type", it.contentEntity.type) + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt new file mode 100644 index 0000000..a3a9ad6 --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt @@ -0,0 +1,41 @@ +package live.shirabox.shirabox.ui.screen.explore.notifications + +import android.content.Context +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.launch +import live.shirabox.core.entity.NotificationEntity +import live.shirabox.core.entity.relation.NotificationAndContent +import live.shirabox.shirabox.db.AppDatabase + +class NotificationsViewModel(context: Context): ViewModel() { + private val appDatabase = AppDatabase.getAppDataBase(context) + val notificationsWithContent = mutableListOf() + + fun fetchNotifications(): Flow> = + appDatabase?.notificationDao()?.all() ?: emptyFlow() + + fun fetchNotificationsWithContent(codes: List) { + viewModelScope.launch(Dispatchers.IO) { + appDatabase?.let { db -> + notificationsWithContent.clear() + + codes.forEach { + Log.d("NotificationsViewModel", "Code: $it") + notificationsWithContent.add(db.notificationDao().notificationWithContent(it)) + } + } + Log.d("NotificationsViewModel", "NC Size: ${notificationsWithContent.size}") + } + } + + fun clearNotifications() { + viewModelScope.launch(Dispatchers.IO) { + appDatabase?.notificationDao()?.deleteAll() + } + } +} \ No newline at end of file From 41ebc0e1dc1bbf38183af408de625b933ffb5479 Mon Sep 17 00:00:00 2001 From: urFate Date: Sat, 6 Apr 2024 13:25:53 +0300 Subject: [PATCH 2/5] fix: adapt legacy code to the new backend --- .../java/live/shirabox/shirabox/NotificationService.kt | 9 ++------- .../live/shirabox/shirabox/db/dao/NotificationDao.kt | 4 ++-- .../screen/explore/notifications/NotificationsScreen.kt | 6 +++--- .../explore/notifications/NotificationsViewModel.kt | 4 ++-- .../java/live/shirabox/core/entity/NotificationEntity.kt | 2 +- .../core/entity/relation/NotificationAndContent.kt | 4 ++-- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/live/shirabox/shirabox/NotificationService.kt b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt index 93d1622..14733c7 100644 --- a/app/src/main/java/live/shirabox/shirabox/NotificationService.kt +++ b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt @@ -47,14 +47,12 @@ class NotificationService : FirebaseMessagingService() { val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(this, mainChannelId).apply { setContentTitle(title) - setContentText("ПЭЙЛОАД Payload: ${data.size}") setSmallIcon(R.drawable.ic_stat_shirabox_notification) setAutoCancel(true) }.build() } else { Notification.Builder(this).apply { setContentTitle(title) - setContentText("ПЭЙЛОАД Payload: $data") setSmallIcon(R.drawable.ic_stat_shirabox_notification) setAutoCancel(true) }.build() @@ -67,15 +65,12 @@ class NotificationService : FirebaseMessagingService() { // Save notification into the database - Log.d("ShiraBoxService","NOTIFICATION_CODE: ${data["code"]}") - - data["code"]?.let { + data["enName"]?.let { scope.launch(Dispatchers.IO) { val appDatabase = AppDatabase.getAppDataBase(this@NotificationService)!! - Log.d("ShiraBoxService", "WE ADDING IT TO THE DATABASE") appDatabase.notificationDao().insertNotification(NotificationEntity( - contentCode = it, + contentEnName = it, receiveTimestamp = System.currentTimeMillis(), text = body )) diff --git a/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt index cdf851d..a4db041 100644 --- a/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt +++ b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt @@ -17,8 +17,8 @@ interface NotificationDao { fun all(): Flow> @Transaction - @Query("SELECT * FROM notification WHERE content_code IS :code") - fun notificationWithContent(code: String): NotificationAndContent + @Query("SELECT * FROM notification WHERE content_enName IS :enName") + fun notificationWithContent(enName: String): NotificationAndContent @Insert(onConflict = OnConflictStrategy.ABORT) fun insertNotification(vararg notificationEntity: NotificationEntity) diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt index fb43b50..7baeab0 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt @@ -31,8 +31,8 @@ import androidx.navigation.NavController import live.shirabox.core.util.Util import live.shirabox.shirabox.R import live.shirabox.shirabox.ui.activity.resource.ResourceActivity +import live.shirabox.shirabox.ui.component.general.DespondencyEmoticon import live.shirabox.shirabox.ui.component.general.ListItem -import live.shirabox.shirabox.ui.component.general.NoContentsPopup import live.shirabox.shirabox.ui.component.top.TopBar import java.text.SimpleDateFormat import java.util.Date @@ -70,7 +70,7 @@ fun NotificationsScreen( LaunchedEffect(notifications) { model.fetchNotificationsWithContent( - notifications.value.map { it.contentCode } + notifications.value.map { it.contentEnName } ) } @@ -100,7 +100,7 @@ fun NotificationsScreen( } if(notifications.value.isEmpty()) { - NoContentsPopup(text = stringResource(id = R.string.no_notifications)) + DespondencyEmoticon(text = stringResource(id = R.string.no_notifications)) } filteredNotificationsByDate.forEach { entry -> diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt index a3a9ad6..b6957eb 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt @@ -19,12 +19,12 @@ class NotificationsViewModel(context: Context): ViewModel() { fun fetchNotifications(): Flow> = appDatabase?.notificationDao()?.all() ?: emptyFlow() - fun fetchNotificationsWithContent(codes: List) { + fun fetchNotificationsWithContent(names: List) { viewModelScope.launch(Dispatchers.IO) { appDatabase?.let { db -> notificationsWithContent.clear() - codes.forEach { + names.forEach { Log.d("NotificationsViewModel", "Code: $it") notificationsWithContent.add(db.notificationDao().notificationWithContent(it)) } diff --git a/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt b/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt index ea750a6..98794c9 100644 --- a/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt +++ b/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt @@ -7,7 +7,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "notification") data class NotificationEntity( @PrimaryKey(autoGenerate = true) val uid: Int = 0, - @ColumnInfo(name = "content_code") val contentCode: String, + @ColumnInfo(name = "content_enName") val contentEnName: String, @ColumnInfo(name = "receive_timestamp") val receiveTimestamp: Long, @ColumnInfo(name = "text") val text: String ) \ No newline at end of file diff --git a/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt b/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt index 03f9a77..f886f74 100644 --- a/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt +++ b/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt @@ -9,8 +9,8 @@ data class NotificationAndContent ( @Embedded val notificationEntity: NotificationEntity, @Relation( - parentColumn = "content_code", - entityColumn = "code" + parentColumn = "content_enName", + entityColumn = "en_name" ) val contentEntity: ContentEntity ) \ No newline at end of file From 17fd875f7fea1e46439c5fa29c746ebe1a305450 Mon Sep 17 00:00:00 2001 From: urFate Date: Wed, 10 Apr 2024 12:29:16 +0300 Subject: [PATCH 3/5] feat: handle firebase data messages --- app/src/main/AndroidManifest.xml | 37 ++-- .../shirabox/shirabox/NotificationService.kt | 162 ++++++++++++------ .../live/shirabox/shirabox/db/AppDatabase.kt | 3 +- .../shirabox/db/dao/NotificationDao.kt | 8 +- .../ui/activity/resource/ResourceActivity.kt | 1 + .../ui/activity/resource/ResourceViewModel.kt | 35 +++- .../shirabox/ui/component/top/TopBar.kt | 45 ++++- .../ui/screen/explore/BaseMediaScreen.kt | 2 +- .../notifications/NotificationsScreen.kt | 81 ++++++--- .../notifications/NotificationsViewModel.kt | 25 +-- app/src/main/res/values/strings.xml | 8 + build.gradle.kts | 5 + .../core/entity/NotificationEntity.kt | 8 +- .../entity/relation/NotificationAndContent.kt | 8 +- .../live/shirabox/core/serializable/Topic.kt | 11 ++ .../main/java/live/shirabox/core/util/Util.kt | 43 +++++ .../java/live/shirabox/core/util/Values.kt | 1 + 17 files changed, 350 insertions(+), 133 deletions(-) create mode 100644 core/src/main/java/live/shirabox/core/serializable/Topic.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19a2410..cdfa2c0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" > @@ -15,7 +15,8 @@ android:label="@string/app_name" android:supportsRtl="false" android:theme="@style/Theme.ShiraBox" - tools:targetApi="34"> + tools:targetApi="34" > + - - - - - - - - - + android:theme="@style/Theme.ShiraBox" > + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/NotificationService.kt b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt index 14733c7..7fef4d6 100644 --- a/app/src/main/java/live/shirabox/shirabox/NotificationService.kt +++ b/app/src/main/java/live/shirabox/shirabox/NotificationService.kt @@ -1,81 +1,143 @@ package live.shirabox.shirabox -import android.Manifest import android.app.Notification -import android.content.pm.PackageManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.TaskStackBuilder +import android.content.Context +import android.content.Intent import android.os.Build import android.util.Log -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationManagerCompat -import androidx.room.Room.databaseBuilder +import androidx.annotation.RequiresApi import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import live.shirabox.core.entity.NotificationEntity +import live.shirabox.core.model.ContentType +import live.shirabox.core.util.Util import live.shirabox.shirabox.db.AppDatabase - +import live.shirabox.shirabox.ui.activity.resource.ResourceActivity +import java.net.URL class NotificationService : FirebaseMessagingService() { - private val mainChannelId = "SB_NOTIFICATIONS" - private val databaseName = "shirabox_db" - - private lateinit var appDatabase: AppDatabase + companion object { + private const val TAG = "ShiraBoxService" + private const val MAIN_CHANNEL_ID = "SB_NOTIFICATIONS" + lateinit var db: AppDatabase + } private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) + private data class MessageData( + val title: String, + val body: String, + val thumbnailUrl: String?, + val shikimoriId: Int + ) + override fun onNewToken(token: String) { super.onNewToken(token) - Log.d("ShiraBoxService", "Refreshed token: $token") + Log.d(TAG, "New token observed: $token") } - override fun onMessageReceived(message: RemoteMessage) { - appDatabase = databaseBuilder( - applicationContext, - AppDatabase::class.java, databaseName - ).build() - - val data = message.data - - message.notification?.let { remoteNotification -> - val title = remoteNotification.title ?: "Undefined title" - val body = remoteNotification.body?: "Undefined notification body" - - val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Notification.Builder(this, mainChannelId).apply { - setContentTitle(title) - setSmallIcon(R.drawable.ic_stat_shirabox_notification) - setAutoCancel(true) - }.build() - } else { - Notification.Builder(this).apply { - setContentTitle(title) - setSmallIcon(R.drawable.ic_stat_shirabox_notification) - setAutoCancel(true) - }.build() - } + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + remoteMessage.data.ifEmpty { return } + + Log.d(TAG, "Message data payload: ${remoteMessage.data}") - if (ActivityCompat - .checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED - ) NotificationManagerCompat.from(this).notify(1, notification) + try { + db = AppDatabase.getAppDataBase(this)!! - // Save notification into the database + val data = remoteMessage.data - data["enName"]?.let { - scope.launch(Dispatchers.IO) { - val appDatabase = AppDatabase.getAppDataBase(this@NotificationService)!! + val messageData = MessageData( + title = data["title"] ?: "Undefined title", + body = data["body"] ?: "Undefined notification body", + shikimoriId = data["shikimori_id"]!!.toInt(), + thumbnailUrl = data["thumbnail"] + ) - appDatabase.notificationDao().insertNotification(NotificationEntity( - contentEnName = it, - receiveTimestamp = System.currentTimeMillis(), - text = body - )) - } + scope.launch { + launch { sendNotification(messageData) } + launch { saveNotification(messageData) } } + + } catch (ex: Exception) { + ex.printStackTrace() } } + + private suspend fun sendNotification(messageData: MessageData) { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + manager.createNotificationChannel(notificationChannel(this)) + } + + val thumbnailBitmap = messageData.thumbnailUrl?.let { + withContext(Dispatchers.IO) { + async { Util.getBitmapFromURL(URL(it)) } + }.await() + } + + val activityIntent = Intent(this, ResourceActivity::class.java).apply { + putExtra("id", messageData.shikimoriId) + putExtra("type", ContentType.ANIME.toString()) + } + val activityPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run { + addNextIntentWithParentStack(activityIntent) + getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(this@NotificationService, MAIN_CHANNEL_ID).apply { + setContentTitle(messageData.title) + setContentText(messageData.body) + setLargeIcon(thumbnailBitmap) + setSmallIcon(R.drawable.ic_stat_shirabox_notification) + setContentIntent(activityPendingIntent) + setAutoCancel(true) + }.build() + } else { + Notification.Builder(this@NotificationService).apply { + setContentTitle(messageData.title) + setContentText(messageData.body) + setLargeIcon(thumbnailBitmap) + setSmallIcon(R.drawable.ic_stat_shirabox_notification) + setContentIntent(activityPendingIntent) + setAutoCancel(true) + }.build() + } + + manager.notify(System.nanoTime().toInt(), notification) + } + + private suspend fun saveNotification(messageData: MessageData) { + withContext(Dispatchers.IO) { + db.notificationDao().insertNotification( + NotificationEntity( + contentShikimoriId = messageData.shikimoriId, + receiveTimestamp = System.currentTimeMillis(), + title = messageData.title, + body = messageData.body, + thumbnailUrl = messageData.thumbnailUrl ?: "" + ) + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun notificationChannel(context: Context) = NotificationChannel( + MAIN_CHANNEL_ID, + context.getString(R.string.notificaion_channel), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { description = context.getString(R.string.notificaion_channel_description) } } \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt b/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt index cef2662..2345c15 100644 --- a/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt +++ b/app/src/main/java/live/shirabox/shirabox/db/AppDatabase.kt @@ -8,6 +8,7 @@ import androidx.room.TypeConverters import live.shirabox.core.entity.ContentEntity import live.shirabox.core.entity.EpisodeEntity import live.shirabox.core.entity.NotificationEntity +import live.shirabox.core.util.Values import live.shirabox.shirabox.db.dao.ContentDao import live.shirabox.shirabox.db.dao.EpisodeDao import live.shirabox.shirabox.db.dao.NotificationDao @@ -31,7 +32,7 @@ abstract class AppDatabase : RoomDatabase() { INSTANCE = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, - "shirabox_db" + Values.DATABASE_NAME ).build() } } diff --git a/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt index a4db041..52dc8a0 100644 --- a/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt +++ b/app/src/main/java/live/shirabox/shirabox/db/dao/NotificationDao.kt @@ -17,8 +17,12 @@ interface NotificationDao { fun all(): Flow> @Transaction - @Query("SELECT * FROM notification WHERE content_enName IS :enName") - fun notificationWithContent(enName: String): NotificationAndContent + @Query("SELECT * FROM notification") + fun allNotificationsWithContent(): Flow> + + @Transaction + @Query("SELECT * FROM notification WHERE content_shikimori_id IS :shikimoriId") + fun notificationsFromParent(shikimoriId: Int): Flow> @Insert(onConflict = OnConflictStrategy.ABORT) fun insertNotification(vararg notificationEntity: NotificationEntity) diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceActivity.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceActivity.kt index 7fbdc14..ea625b3 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceActivity.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceActivity.kt @@ -161,6 +161,7 @@ fun Resource( LaunchedEffect(Unit) { model.fetchContent(id) model.fetchRelated(id) + model.clearNotifications(id) } AnimatedVisibility( diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt index 5877eae..092e695 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt @@ -14,10 +14,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import live.shirabox.core.entity.EpisodeEntity import live.shirabox.core.model.Content import live.shirabox.core.model.ContentType +import live.shirabox.core.util.Util import live.shirabox.core.util.Util.Companion.mapContentToEntity import live.shirabox.core.util.Util.Companion.mapEntityToContent import live.shirabox.data.DataSources @@ -133,25 +135,42 @@ class ResourceViewModel(context: Context, private val contentType: ContentType) } } - fun switchSourcePinStatus(id: Int, source: AbstractContentRepository) { + fun switchSourcePinStatus(id: Int, repository: AbstractContentRepository) { viewModelScope.launch(Dispatchers.IO) { val content = db?.contentDao()?.getContent(id) - content?.let { - val contentTopic = "${source.name.lowercase()}_${content.enName}" - - when (pinnedSources.contains(source.name)) { + content?.let { entity -> + val contentTopic = Util.encodeTopic( + repository = repository.name, + actingTeam = repository.name, + contentEnName = entity.enName + ) + + when (pinnedSources.contains(repository.name)) { true -> { - pinnedSources.remove(source.name) + pinnedSources.remove(repository.name) Firebase.messaging.unsubscribeFromTopic(contentTopic) } else -> { - pinnedSources.add(source.name) + pinnedSources.add(repository.name) Firebase.messaging.subscribeToTopic(contentTopic) } } - db?.contentDao()?.updateContents(it.copy(pinnedSources = pinnedSources)) + db?.contentDao()?.updateContents(entity.copy(pinnedSources = pinnedSources)) + } + } + } + + fun clearNotifications(shikimoriId: Int) { + viewModelScope.launch(Dispatchers.IO) { + db?.notificationDao()?.notificationsFromParent(shikimoriId)?.catch { + it.printStackTrace() + emitAll(emptyFlow()) + }?.map { + it.toTypedArray() + }?.collect { + db.notificationDao().deleteNotification(*it) } } } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/component/top/TopBar.kt b/app/src/main/java/live/shirabox/shirabox/ui/component/top/TopBar.kt index b8f9332..11cbfe3 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/component/top/TopBar.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/component/top/TopBar.kt @@ -9,10 +9,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.NotificationsNone +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -27,16 +30,33 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import live.shirabox.core.util.Util import live.shirabox.shirabox.R import live.shirabox.shirabox.ui.activity.search.SearchActivity import live.shirabox.shirabox.ui.component.navigation.NestedNavItems +import live.shirabox.shirabox.ui.screen.explore.notifications.NotificationsViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopBar(navController: NavController?) { +fun TopBar( + navController: NavController, + model: NotificationsViewModel = viewModel(factory = Util.viewModelFactory { + NotificationsViewModel(context = navController.context.applicationContext) + }) +) { val context = LocalContext.current + val notifications = model.allNotificationsFlow().catch { + it.printStackTrace() + emitAll(emptyFlow()) + }.collectAsStateWithLifecycle(initialValue = emptyList()) + Row( modifier = Modifier .padding(16.dp, 16.dp, 16.dp, 0.dp), @@ -71,14 +91,23 @@ fun TopBar(navController: NavController?) { } IconButton( - onClick = { navController?.navigate(NestedNavItems.Notifications.route) } + modifier = Modifier.requiredSize(48.dp), + onClick = { navController.navigate(NestedNavItems.Notifications.route) } ) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.Rounded.NotificationsNone, - contentDescription = "Notifications", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + BadgedBox( + badge = { + if (notifications.value.isNotEmpty()) Badge { + Text(text = Util.formatBadgeNumber(notifications.value.size)) + } + }) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Rounded.NotificationsNone, + contentDescription = "Notifications", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/BaseMediaScreen.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/BaseMediaScreen.kt index 9395b27..bec3d8c 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/BaseMediaScreen.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/BaseMediaScreen.kt @@ -257,7 +257,7 @@ private fun PopularsGrid(isReady: Boolean, contents: List) { ResourceActivity::class.java ).apply { putExtra("id", it.shikimoriID) - putExtra("type", it.type) + putExtra("type", it.type.toString()) } ) } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt index 7baeab0..0653673 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsScreen.kt @@ -1,6 +1,7 @@ package live.shirabox.shirabox.ui.screen.explore.notifications import android.content.Intent +import android.text.format.DateUtils import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -8,13 +9,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -24,16 +28,21 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow import live.shirabox.core.util.Util import live.shirabox.shirabox.R import live.shirabox.shirabox.ui.activity.resource.ResourceActivity -import live.shirabox.shirabox.ui.component.general.DespondencyEmoticon import live.shirabox.shirabox.ui.component.general.ListItem import live.shirabox.shirabox.ui.component.top.TopBar +import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -50,36 +59,38 @@ fun NotificationsScreen( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - TopBar(null) + TopBar(navController) val context = LocalContext.current - val notifications = model.fetchNotifications().collectAsState(initial = emptyList()) + val notifications = model.notificationsWithContentFlow().catch { + it.printStackTrace() + emitAll(emptyFlow()) + }.collectAsStateWithLifecycle(initialValue = emptyList()) - val filteredNotificationsByDate by remember(model.notificationsWithContent) { + val filteredNotificationsByDate by remember(notifications.value) { derivedStateOf { - val languageCode = Locale.current.language - - model.notificationsWithContent.groupBy { - SimpleDateFormat( - "dd.MM.yyyy", - java.util.Locale(languageCode) - ).format(Date(it.notificationEntity.receiveTimestamp)) + notifications.value.groupBy { + when (DateUtils.isToday(it.notificationEntity.receiveTimestamp)) { + true -> context.resources.getString(R.string.today) + false -> DateUtils.formatSameDayTime( + it.notificationEntity.receiveTimestamp, + System.currentTimeMillis(), + DateFormat.SHORT, + DateFormat.SHORT + ).toString() + } } } } - - LaunchedEffect(notifications) { - model.fetchNotificationsWithContent( - notifications.value.map { it.contentEnName } - ) - } Column( verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Row( - modifier = Modifier.fillMaxWidth().padding(16.dp, 0.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp, 0.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -100,13 +111,34 @@ fun NotificationsScreen( } if(notifications.value.isEmpty()) { - DespondencyEmoticon(text = stringResource(id = R.string.no_notifications)) + Column( + modifier = Modifier + .padding(64.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = Icons.Default.AutoAwesome, + contentDescription = "awesome", + tint = MaterialTheme.colorScheme.surfaceTint.copy(0.4f) + ) + Text( + text = stringResource(id = R.string.no_notifications), + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + } } filteredNotificationsByDate.forEach { entry -> Text( - modifier = Modifier.padding(16.dp, 0.dp), + modifier = Modifier + .padding(16.dp, 0.dp) + .fillMaxWidth(), text = entry.key, + textAlign = TextAlign.Left, fontSize = 15.sp, fontWeight = FontWeight(500) ) @@ -129,7 +161,7 @@ fun NotificationsScreen( fontSize = 12.sp) } }, - supportingString = it.notificationEntity.text, + supportingString = it.notificationEntity.body, coverImage = it.contentEntity.image ) { context.startActivity( @@ -138,9 +170,10 @@ fun NotificationsScreen( ResourceActivity::class.java ).apply { putExtra("id", it.contentEntity.shikimoriID) - putExtra("type", it.contentEntity.type) + putExtra("type", it.contentEntity.type.toString()) } ) + model.removeNotification(it.notificationEntity) } } } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt index b6957eb..b8ed567 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsViewModel.kt @@ -1,7 +1,6 @@ package live.shirabox.shirabox.ui.screen.explore.notifications import android.content.Context -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -13,29 +12,23 @@ import live.shirabox.core.entity.relation.NotificationAndContent import live.shirabox.shirabox.db.AppDatabase class NotificationsViewModel(context: Context): ViewModel() { - private val appDatabase = AppDatabase.getAppDataBase(context) - val notificationsWithContent = mutableListOf() + private val db = AppDatabase.getAppDataBase(context) - fun fetchNotifications(): Flow> = - appDatabase?.notificationDao()?.all() ?: emptyFlow() + fun allNotificationsFlow(): Flow> = + db?.notificationDao()?.all() ?: emptyFlow() - fun fetchNotificationsWithContent(names: List) { - viewModelScope.launch(Dispatchers.IO) { - appDatabase?.let { db -> - notificationsWithContent.clear() + fun notificationsWithContentFlow(): Flow> = + db?.notificationDao()?.allNotificationsWithContent() ?: emptyFlow() - names.forEach { - Log.d("NotificationsViewModel", "Code: $it") - notificationsWithContent.add(db.notificationDao().notificationWithContent(it)) - } - } - Log.d("NotificationsViewModel", "NC Size: ${notificationsWithContent.size}") + fun removeNotification(entity: NotificationEntity) { + viewModelScope.launch(Dispatchers.IO) { + db?.notificationDao()?.deleteNotification(entity) } } fun clearNotifications() { viewModelScope.launch(Dispatchers.IO) { - appDatabase?.notificationDao()?.deleteAll() + db?.notificationDao()?.deleteAll() } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8705b80..18ad73c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,6 +28,8 @@ Среди новинок Популярно Уведомления + Сегодня + + Новые серии + Уведомления о новых выпусках эпизодов + diff --git a/build.gradle.kts b/build.gradle.kts index 200ecd2..332a87d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,8 @@ +buildscript { + dependencies { + classpath("com.google.gms:google-services:4.4.1") + } +} // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id ("com.android.application") version "8.3.1" apply false diff --git a/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt b/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt index 98794c9..4c78f91 100644 --- a/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt +++ b/core/src/main/java/live/shirabox/core/entity/NotificationEntity.kt @@ -6,8 +6,10 @@ import androidx.room.PrimaryKey @Entity(tableName = "notification") data class NotificationEntity( - @PrimaryKey(autoGenerate = true) val uid: Int = 0, - @ColumnInfo(name = "content_enName") val contentEnName: String, + @PrimaryKey(autoGenerate = true) val uid: Long = 0, + @ColumnInfo(name = "content_shikimori_id") val contentShikimoriId: Int, @ColumnInfo(name = "receive_timestamp") val receiveTimestamp: Long, - @ColumnInfo(name = "text") val text: String + @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "body") val body: String, + @ColumnInfo(name = "thumbnail") val thumbnailUrl: String ) \ No newline at end of file diff --git a/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt b/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt index f886f74..103d7f9 100644 --- a/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt +++ b/core/src/main/java/live/shirabox/core/entity/relation/NotificationAndContent.kt @@ -6,11 +6,11 @@ import live.shirabox.core.entity.ContentEntity import live.shirabox.core.entity.NotificationEntity data class NotificationAndContent ( - @Embedded - val notificationEntity: NotificationEntity, + @Embedded val notificationEntity: NotificationEntity, + @Relation( - parentColumn = "content_enName", - entityColumn = "en_name" + parentColumn = "content_shikimori_id", + entityColumn = "shikimori_id" ) val contentEntity: ContentEntity ) \ No newline at end of file diff --git a/core/src/main/java/live/shirabox/core/serializable/Topic.kt b/core/src/main/java/live/shirabox/core/serializable/Topic.kt new file mode 100644 index 0000000..83a00ea --- /dev/null +++ b/core/src/main/java/live/shirabox/core/serializable/Topic.kt @@ -0,0 +1,11 @@ +package live.shirabox.core.serializable + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Topic( + val repository: String, + @SerialName("acting_team") val actingTeam: String, + val md5: String +) \ No newline at end of file diff --git a/core/src/main/java/live/shirabox/core/util/Util.kt b/core/src/main/java/live/shirabox/core/util/Util.kt index e444af9..56222bc 100644 --- a/core/src/main/java/live/shirabox/core/util/Util.kt +++ b/core/src/main/java/live/shirabox/core/util/Util.kt @@ -3,6 +3,8 @@ package live.shirabox.core.util import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Build import android.text.Html import androidx.core.view.WindowInsetsControllerCompat @@ -10,10 +12,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.media3.common.C import com.google.accompanist.systemuicontroller.SystemUiController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import live.shirabox.core.entity.ContentEntity import live.shirabox.core.model.Content +import live.shirabox.core.serializable.Topic +import java.net.URL +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.time.Duration.Companion.milliseconds + class Util { companion object { fun hideSystemUi(controller: SystemUiController) { @@ -100,6 +112,37 @@ class Util { ) } + @OptIn(ExperimentalEncodingApi::class) + fun encodeTopic(repository: String, actingTeam: String, contentEnName: String): String { + val json = Json.encodeToString( + Topic( + repository = repository, + actingTeam = actingTeam, + md5 = contentEnName.md5() + ) + ) + + return Base64.encode(json.toByteArray()) + } + suspend fun getBitmapFromURL(url: URL): Bitmap? { + return try { + val inputStream = withContext(Dispatchers.IO) { + async { + val connection = url.openConnection() + connection.setDoInput(true) + connection.connect() + connection.inputStream + } + } + + BitmapFactory.decodeStream(inputStream.await()) + } catch (_: Exception) { null } + } + + fun formatBadgeNumber(number: Int): String { + return if (number < 9) number.toString() else "9+" + } + inline fun viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T = f() as T diff --git a/core/src/main/java/live/shirabox/core/util/Values.kt b/core/src/main/java/live/shirabox/core/util/Values.kt index f3f2569..f727ac4 100644 --- a/core/src/main/java/live/shirabox/core/util/Values.kt +++ b/core/src/main/java/live/shirabox/core/util/Values.kt @@ -4,6 +4,7 @@ class Values { companion object { const val INSTANT_SEEK_TIME = 10000 const val CONTROLS_HIDE_DELAY = 3000L + const val DATABASE_NAME = "shirabox_db" const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A127F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/21.0 Chrome/110.0.5481.154 Mobile Safari/537.36" } } \ No newline at end of file From 945f7a10a1480574158891989a7ef27259c32df4 Mon Sep 17 00:00:00 2001 From: urFate Date: Wed, 10 Apr 2024 16:27:37 +0300 Subject: [PATCH 4/5] feat: notifications dialog --- .../shirabox/ui/activity/MainActivity.kt | 7 ++- .../ui/component/general/Notifications.kt | 55 ++++++++++++++++-- .../notifications/NotificationsDialog.kt | 56 +++++++++++++++++++ app/src/main/res/values/strings.xml | 6 +- 4 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsDialog.kt diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt index 0efede4..c2a273e 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/MainActivity.kt @@ -9,19 +9,24 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import live.shirabox.shirabox.ui.component.navigation.BottomNavigationView +import live.shirabox.shirabox.ui.screen.explore.notifications.NotificationsDialog import live.shirabox.shirabox.ui.theme.ShiraBoxTheme class MainActivity : ComponentActivity() { @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { ShiraBoxTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background - ) { BottomNavigationView() } + ) { + NotificationsDialog() + BottomNavigationView() + } } } } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt b/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt index d927d3f..4fd64f4 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/component/general/Notifications.kt @@ -1,15 +1,25 @@ package live.shirabox.shirabox.ui.component.general +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.NotificationsActive import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import live.shirabox.shirabox.R @Composable @@ -19,9 +29,20 @@ fun NotificationsRequestDialog(isOpen: MutableState, onConfirm: () -> U onDismissRequest = { isOpen.value = false }, - icon = { Icon(Icons.Outlined.NotificationsActive, contentDescription = null) }, + icon = { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Outlined.NotificationsActive, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null + ) + }, title = { - Text(text = stringResource(R.string.notifications_request)) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.notifications_request), + textAlign = TextAlign.Center + ) }, text = { Text( @@ -52,15 +73,26 @@ fun NotificationsRequestDialog(isOpen: MutableState, onConfirm: () -> U } @Composable -fun NotificationsDismissDialog(isOpen: MutableState) { +fun NotificationsDismissDialog(context: Context, isOpen: MutableState) { if(isOpen.value) { AlertDialog( onDismissRequest = { isOpen.value = false }, - icon = { Icon(Icons.Outlined.NotificationsOff, contentDescription = null) }, + icon = { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Outlined.NotificationsOff, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null + ) + }, title = { - Text(text = stringResource(R.string.notifications_disabled)) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.notifications_disabled), + textAlign = TextAlign.Center + ) }, text = { Text( @@ -75,6 +107,19 @@ fun NotificationsDismissDialog(isOpen: MutableState) { ) { Text(stringResource(R.string.notifications_disabled_confirm)) } + }, + dismissButton = { + TextButton( + onClick = { + isOpen.value = false + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:${context.applicationContext.packageName}") + } + context.startActivity(intent) + } + ) { + Text(stringResource(R.string.notifications_request)) + } } ) } diff --git a/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsDialog.kt b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsDialog.kt new file mode 100644 index 0000000..8c6aee2 --- /dev/null +++ b/app/src/main/java/live/shirabox/shirabox/ui/screen/explore/notifications/NotificationsDialog.kt @@ -0,0 +1,56 @@ +package live.shirabox.shirabox.ui.screen.explore.notifications + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import live.shirabox.shirabox.ui.component.general.NotificationsDismissDialog +import live.shirabox.shirabox.ui.component.general.NotificationsRequestDialog + +@Composable +fun NotificationsDialog() { + val context = LocalContext.current + val activity = context as Activity + + val openRequestDialog = remember { mutableStateOf(false) } + val openDismissDialog = remember { mutableStateOf(false) } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted: Boolean -> + openDismissDialog.value = !isGranted + } + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val showRationale = !ActivityCompat.shouldShowRequestPermissionRationale( + activity, Manifest.permission.POST_NOTIFICATIONS) + + if(!isNotificationsPermissionGranted(context) && showRationale){ + openRequestDialog.value = true + } + + NotificationsRequestDialog(isOpen = openRequestDialog) { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + NotificationsDismissDialog(context = context, isOpen = openDismissDialog) +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +private fun isNotificationsPermissionGranted(context: Context): Boolean = + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18ad73c..9e45d2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,14 +106,14 @@ Ничего не найдено Включить уведомления - Вы сможете получать уведомления о выходе новых серий и обновлений ShiraBox. Обещаем, мы не будем надоедать вам ❤️️ + Вы сможете получать уведомления о выходе новых серий и обновлений ShiraBox. \nОбещаем, мы не будем надоедать вам! Включить уведомления Нет, спасибо Уведомления отключены - Вы больше не сможете получать уведомления о выходе новых серий и обновлений ShiraBox. Обещаем, мы не будем надоедать вам ❤️ + Вы больше не сможете получать уведомления о выходе новых серий и обновлений ShiraBox. \nУведомления можно включить перейдя в раздел приложений в настройках вашего устройства. Хорошо Очистить Уведомлений нет From 401f574ffb02aea7f9905d4306009a88c3000c53 Mon Sep 17 00:00:00 2001 From: urFate Date: Wed, 10 Apr 2024 16:42:07 +0300 Subject: [PATCH 5/5] feat: respect notifications subscription preference --- .../ui/activity/resource/ResourceSheetScreens.kt | 2 +- .../ui/activity/resource/ResourceViewModel.kt | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceSheetScreens.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceSheetScreens.kt index eec8a74..9bedb75 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceSheetScreens.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceSheetScreens.kt @@ -209,7 +209,7 @@ fun SourcesSheetScreen( coverImage = source.icon, trailingIcon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin, onTrailingIconClick = { - model.switchSourcePinStatus(content.shikimoriID, source) + model.switchSourcePinStatus(context, content.shikimoriID, source) } ) { currentSheetScreenState.value = diff --git a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt index 092e695..20a1158 100644 --- a/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt +++ b/app/src/main/java/live/shirabox/shirabox/ui/activity/resource/ResourceViewModel.kt @@ -14,8 +14,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import live.shirabox.core.datastore.AppDataStore +import live.shirabox.core.datastore.DataStoreScheme import live.shirabox.core.entity.EpisodeEntity import live.shirabox.core.model.Content import live.shirabox.core.model.ContentType @@ -135,9 +138,13 @@ class ResourceViewModel(context: Context, private val contentType: ContentType) } } - fun switchSourcePinStatus(id: Int, repository: AbstractContentRepository) { + fun switchSourcePinStatus(context: Context, id: Int, repository: AbstractContentRepository) { viewModelScope.launch(Dispatchers.IO) { val content = db?.contentDao()?.getContent(id) + val subscriptionAllowed = + AppDataStore.read(context, DataStoreScheme.FIELD_SUBSCRIPTION.key).firstOrNull() + ?: DataStoreScheme.FIELD_SUBSCRIPTION.defaultValue + content?.let { entity -> val contentTopic = Util.encodeTopic( repository = repository.name, @@ -148,12 +155,11 @@ class ResourceViewModel(context: Context, private val contentType: ContentType) when (pinnedSources.contains(repository.name)) { true -> { pinnedSources.remove(repository.name) - - Firebase.messaging.unsubscribeFromTopic(contentTopic) + if(subscriptionAllowed) Firebase.messaging.unsubscribeFromTopic(contentTopic) } else -> { pinnedSources.add(repository.name) - Firebase.messaging.subscribeToTopic(contentTopic) + if(subscriptionAllowed) Firebase.messaging.subscribeToTopic(contentTopic) } }