From faf48d4003024f321c094120cf072256c2e48d41 Mon Sep 17 00:00:00 2001
From: Julian Waluschyk <37155504+julian-wls@users.noreply.github.com>
Date: Fri, 17 Jan 2025 08:49:46 +0100
Subject: [PATCH] `Chore`: Unify PushNoficationSettingsScreen and
NotificationSettingsUi & unsaved changes dialog (#287)
---
.../native_app/feature/login/AccountUi.kt | 4 +-
.../feature/login/NotificationSettingsUi.kt | 116 -----------
.../main/res/values/account_ui_strings.xml | 5 -
.../push/ui/PushNotificationSettingsScreen.kt | 182 ++++++++++++++++++
.../push/ui/PushNotificationSettingsUi.kt | 15 ++
.../push_notification_settings_strings.xml | 11 ++
.../PushNotificationSettingsScreen.kt | 86 ---------
.../feature/settings/SettingsScreen.kt | 3 +-
.../src/main/res/values/settings_strings.xml | 2 -
9 files changed, 213 insertions(+), 211 deletions(-)
delete mode 100644 feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/NotificationSettingsUi.kt
create mode 100644 feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsScreen.kt
delete mode 100644 feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/PushNotificationSettingsScreen.kt
diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt
index 8623b7208..e927ff34a 100644
--- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt
+++ b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt
@@ -75,6 +75,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.login.register.Registe
import de.tum.informatics.www1.artemis.native_app.feature.login.saml2_login.Saml2LoginScreen
import de.tum.informatics.www1.artemis.native_app.feature.login.saml2_login.Saml2LoginViewModel
import de.tum.informatics.www1.artemis.native_app.feature.login.service.ServerNotificationStorageService
+import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsScreen
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@@ -175,8 +176,9 @@ fun NavGraphBuilder.loginScreen(
}
LoginScreenContent.NOTIFICATION_SETTINGS -> {
- NotificationSettingsUi(
+ PushNotificationSettingsScreen(
modifier = Modifier.fillMaxSize(),
+ isInitialNotificationSettingsScreen = true,
onDone = {
scope.launch {
serverNotificationStorageService.setHasDisplayed(
diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/NotificationSettingsUi.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/NotificationSettingsUi.kt
deleted file mode 100644
index de1d6eb2c..000000000
--- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/NotificationSettingsUi.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package de.tum.informatics.www1.artemis.native_app.feature.login
-
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.filled.Save
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberTopAppBarState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.res.stringResource
-import de.tum.informatics.www1.artemis.native_app.core.ui.pagePadding
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsUi
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsViewModel
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSyncChangesDialog
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSyncFailedDialog
-import kotlinx.coroutines.Job
-import org.koin.androidx.compose.koinViewModel
-
-/**
- * Display UI so the user can select if they want to receive notifications and what notifications to receive.
- */
-@Composable
-internal fun NotificationSettingsUi(modifier: Modifier, onDone: () -> Unit) {
- val viewModel: PushNotificationSettingsViewModel = koinViewModel()
- val isDirty by viewModel.isDirty.collectAsState(initial = false)
-
- var saveJob: Job? by remember { mutableStateOf(null) }
- var displaySyncFailedDialog: Boolean by rememberSaveable { mutableStateOf(false) }
-
- val topAppBarState = rememberTopAppBarState()
-
- val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
- topAppBarState
- )
-
- Scaffold(
- modifier = modifier.then(Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)),
- topBar = {
- TopAppBar(
- scrollBehavior = scrollBehavior,
- title = {
- Text(text = stringResource(id = R.string.push_notification_settings_title))
- }
- )
- },
- floatingActionButton = {
- ExtendedFloatingActionButton(
- onClick = {
- // If changes have been made, these need to be synced first.
- if (isDirty) {
- saveJob = viewModel.saveSettings()
- } else onDone()
- },
- text = {
- Text(
- text = stringResource(
- id =
- if (isDirty) R.string.push_notification_settings_fab_text_with_save
- else R.string.push_notification_settings_fab_text_without_save
- )
- )
- },
- icon = {
- Icon(
- imageVector = if (isDirty) Icons.Default.Save else Icons.AutoMirrored.Filled.ArrowForward,
- contentDescription = null
- )
- }
- )
- }
- ) { padding ->
- PushNotificationSettingsUi(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(padding)
- .consumeWindowInsets(WindowInsets.systemBars)
- .pagePadding(),
- viewModel = viewModel
- )
-
- if (saveJob != null) {
- PushNotificationSyncChangesDialog(
- onDismissRequest = {
- saveJob?.cancel()
- saveJob = null
- }
- )
- }
-
- if (displaySyncFailedDialog) {
- PushNotificationSyncFailedDialog {
- displaySyncFailedDialog = false
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/login/src/main/res/values/account_ui_strings.xml b/feature/login/src/main/res/values/account_ui_strings.xml
index 5494f7686..3eef8394d 100644
--- a/feature/login/src/main/res/values/account_ui_strings.xml
+++ b/feature/login/src/main/res/values/account_ui_strings.xml
@@ -106,9 +106,4 @@
An unexpected error occurred. Please try again or use a different login method.
Loading authentication website…
Reload now
-
-
- Notification configuration
- Save & Continue
- Continue
\ No newline at end of file
diff --git a/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsScreen.kt b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsScreen.kt
new file mode 100644
index 000000000..a7bd5f2d1
--- /dev/null
+++ b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsScreen.kt
@@ -0,0 +1,182 @@
+package de.tum.informatics.www1.artemis.native_app.feature.push.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import de.tum.informatics.www1.artemis.native_app.core.ui.compose.JobAnimatedFloatingActionButton
+import de.tum.informatics.www1.artemis.native_app.core.ui.compose.NavigationBackButton
+import de.tum.informatics.www1.artemis.native_app.feature.push.R
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+
+/**
+ * Displays the notification settings screen.
+ * Contains PushNotificationSettingsUi and is used in the settings screen and after login.
+ */
+@Composable
+fun PushNotificationSettingsScreen(
+ modifier: Modifier = Modifier,
+ isInitialNotificationSettingsScreen: Boolean = false,
+ onDone: () -> Unit
+) {
+ val viewModel: PushNotificationSettingsViewModel = koinViewModel()
+ val isDirty by viewModel.isDirty.collectAsState(initial = false)
+
+ var saveJob: Job? by remember { mutableStateOf(null) }
+ var displaySyncFailedDialog: Boolean by rememberSaveable { mutableStateOf(false) }
+ var displayUnsavedChangesDialog: Boolean by rememberSaveable { mutableStateOf(false) }
+
+ val onNavigateBack: () -> Unit = {
+ if (isDirty) {
+ displayUnsavedChangesDialog = true
+ } else {
+ onDone()
+ }
+ }
+ val saveChanges: () -> Unit = {
+ saveJob = viewModel.saveSettings()
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ val isSuccessful = (saveJob as Deferred).await()
+ if (isSuccessful) {
+ onDone()
+ } else {
+ displayUnsavedChangesDialog = false
+ displaySyncFailedDialog = true
+ }
+ } catch (e: Exception) {
+ displayUnsavedChangesDialog = false
+ displaySyncFailedDialog = true
+ }
+ }
+ }
+
+ BackHandler {
+ onNavigateBack()
+ }
+
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ if (isInitialNotificationSettingsScreen) return@TopAppBar
+
+ NavigationBackButton(onNavigateBack = {
+ onNavigateBack()
+ })
+ },
+ title = {
+ if (isInitialNotificationSettingsScreen) {
+ Text(text = stringResource(id = R.string.initial_push_notification_settings_title))
+ } else {
+ Text(text = stringResource(id = R.string.settings_push_notification_settings_screen_title))
+ }
+ }
+ )
+ },
+ floatingActionButton = {
+ if (isInitialNotificationSettingsScreen) {
+ ExtendedFloatingActionButton(
+ onClick = {
+ // If changes have been made, these need to be synced first.
+ if (isDirty) saveChanges() else onDone()
+ },
+ text = {
+ Text(
+ text = stringResource(
+ id =
+ if (isDirty) R.string.initial_push_notification_settings_fab_text_with_save
+ else R.string.initial_push_notification_settings_fab_text_without_save
+ )
+ )
+ },
+ icon = {
+ Icon(
+ imageVector = if (isDirty) Icons.Default.Save else Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = null
+ )
+ }
+ )
+ } else {
+ JobAnimatedFloatingActionButton(
+ enabled = isDirty,
+ startJob = { viewModel.saveSettings() },
+ onJobCompleted = { isSuccessful ->
+ if (!isSuccessful) {
+ displaySyncFailedDialog = true
+ }
+ }
+ ) {
+ Icon(imageVector = Icons.Default.Save, contentDescription = null)
+ }
+ }
+ }
+ ) { padding ->
+ PushNotificationSettingsUi(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = padding.calculateTopPadding())
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ .padding(
+ bottom = WindowInsets.systemBars
+ .asPaddingValues()
+ .calculateBottomPadding()
+ ),
+ viewModel = viewModel
+ )
+
+ if (displaySyncFailedDialog) {
+ PushNotificationSyncFailedDialog {
+ displaySyncFailedDialog = false
+ }
+ }
+
+ if (displayUnsavedChangesDialog) {
+ PushNotificationUnsavedChangesDialog(
+ onDismissRequest = {
+ displayUnsavedChangesDialog = false
+ onDone()
+ },
+ onSaveChanges = saveChanges
+ )
+ }
+
+ if (saveJob != null && isInitialNotificationSettingsScreen) {
+ PushNotificationSyncChangesDialog(
+ onDismissRequest = {
+ saveJob?.cancel()
+ saveJob = null
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt
index 7cc0e885c..be7a7292a 100644
--- a/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt
+++ b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt
@@ -218,4 +218,19 @@ fun PushNotificationSyncFailedDialog(onDismissRequest: () -> Unit) {
onPressPositiveButton = onDismissRequest,
onDismissRequest = onDismissRequest
)
+}
+
+@Composable
+fun PushNotificationUnsavedChangesDialog(
+ onDismissRequest: () -> Unit,
+ onSaveChanges: () -> Unit
+) {
+ TextAlertDialog(
+ title = stringResource(id = R.string.push_notification_settings_unsaved_changes_dialog_title),
+ text = stringResource(id = R.string.push_notification_settings_unsaved_changes_dialog_message),
+ confirmButtonText = stringResource(id = R.string.push_notification_settings_unsaved_changes_dialog_positive),
+ dismissButtonText = stringResource(id = R.string.push_notification_settings_unsaved_changes_dialog_negative),
+ onPressPositiveButton = onSaveChanges,
+ onDismissRequest = onDismissRequest
+ )
}
\ No newline at end of file
diff --git a/feature/push/src/main/res/values/push_notification_settings_strings.xml b/feature/push/src/main/res/values/push_notification_settings_strings.xml
index b67502856..f8138699b 100644
--- a/feature/push/src/main/res/values/push_notification_settings_strings.xml
+++ b/feature/push/src/main/res/values/push_notification_settings_strings.xml
@@ -87,4 +87,15 @@
Syncing failed!
Something went wrong while syncing your settings.
@android:string/ok
+
+ You have unsaved changes!
+ Would you like to save your changes before continuing?
+ Save
+ Discard
+
+ Notification settings
+
+ Notification configuration
+ Save & Continue
+ Continue
\ No newline at end of file
diff --git a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/PushNotificationSettingsScreen.kt b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/PushNotificationSettingsScreen.kt
deleted file mode 100644
index 868ba189c..000000000
--- a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/PushNotificationSettingsScreen.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package de.tum.informatics.www1.artemis.native_app.feature.settings
-
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Save
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import de.tum.informatics.www1.artemis.native_app.core.ui.compose.JobAnimatedFloatingActionButton
-import de.tum.informatics.www1.artemis.native_app.core.ui.pagePadding
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsUi
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsViewModel
-import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSyncFailedDialog
-import org.koin.androidx.compose.koinViewModel
-
-@Composable
-internal fun PushNotificationSettingsScreen(modifier: Modifier, onNavigateBack: () -> Unit) {
- val viewModel: PushNotificationSettingsViewModel = koinViewModel()
- val isDirty: Boolean by viewModel.isDirty.collectAsState(initial = false)
-
- var displaySyncFailedDialog: Boolean by rememberSaveable { mutableStateOf(false) }
-
- Scaffold(
- modifier = modifier,
- topBar = {
- TopAppBar(
- navigationIcon = {
- IconButton(onClick = onNavigateBack) {
- Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
- }
- },
- title = {
- Text(
- text = stringResource(id = R.string.notification_settings_screen_title)
- )
- }
- )
- },
- floatingActionButton = {
- JobAnimatedFloatingActionButton(
- enabled = isDirty,
- startJob = { viewModel.saveSettings() },
- onJobCompleted = { isSuccessful ->
- if (!isSuccessful) {
- displaySyncFailedDialog = true
- }
- }
- ) {
- Icon(imageVector = Icons.Default.Save, contentDescription = null)
- }
- }
- ) { padding ->
- PushNotificationSettingsUi(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(padding)
- .consumeWindowInsets(WindowInsets.systemBars)
- .pagePadding(),
- viewModel = viewModel
- )
-
- if (displaySyncFailedDialog) {
- PushNotificationSyncFailedDialog {
- displaySyncFailedDialog = false
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt
index 5631f083e..ba898c0ea 100644
--- a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt
+++ b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt
@@ -61,6 +61,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profil
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureData
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationConfigurationService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationJobService
+import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsScreen
import de.tum.informatics.www1.artemis.native_app.feature.push.unsubscribeFromNotifications
import io.ktor.http.URLBuilder
import io.ktor.http.appendPathSegments
@@ -117,7 +118,7 @@ fun NavGraphBuilder.settingsScreen(
animatedComposable {
PushNotificationSettingsScreen(
modifier = Modifier.fillMaxSize(),
- onNavigateBack = onNavigateUp
+ onDone = onNavigateUp
)
}
}
diff --git a/feature/settings/src/main/res/values/settings_strings.xml b/feature/settings/src/main/res/values/settings_strings.xml
index a412ed104..12595c4f0 100644
--- a/feature/settings/src/main/res/values/settings_strings.xml
+++ b/feature/settings/src/main/res/values/settings_strings.xml
@@ -22,6 +22,4 @@
Build information
Version code:
Version name:
-
- Notification settings
\ No newline at end of file