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