diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/DefaultTransition.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/DefaultTransition.kt new file mode 100644 index 000000000..300bedd6b --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/DefaultTransition.kt @@ -0,0 +1,57 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith + + +object DefaultTransition { + const val duration = 220 + + val fadeIn = fadeIn(tween(duration)) + val fadeOut = fadeOut(tween(duration)) + + val navigateForward = enter togetherWith exit + val navigateBack = popEnter togetherWith popExit + val navigateNeutral = fadeIn togetherWith fadeOut + + val enter + get() = slideIn(AnimatedContentTransitionScope.SlideDirection.Left) + + val exit + get() = slideOut(AnimatedContentTransitionScope.SlideDirection.Left) + + val popEnter + get() = slideIn(AnimatedContentTransitionScope.SlideDirection.Right) + + val popExit + get() = slideOut(AnimatedContentTransitionScope.SlideDirection.Right) + + fun slideIn( + direction: AnimatedContentTransitionScope.SlideDirection + ): EnterTransition = slideInHorizontally( + animationSpec = tween(duration) + ) { width -> + return@slideInHorizontally when(direction) { + AnimatedContentTransitionScope.SlideDirection.Left -> width + else -> - width + } + } + fadeIn + + fun slideOut( + direction: AnimatedContentTransitionScope.SlideDirection + ): ExitTransition = slideOutHorizontally( + animationSpec = tween(duration) + ) { width -> + return@slideOutHorizontally when(direction) { + AnimatedContentTransitionScope.SlideDirection.Left -> - width + else -> width + } + } + fadeOut +} diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/navigation_animation.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/navigation_animation.kt new file mode 100644 index 000000000..d31341a43 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/navigation_animation.kt @@ -0,0 +1,46 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.SizeTransform +import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import kotlin.reflect.KType + + +inline fun NavGraphBuilder.animatedComposable( + typeMap: Map> = emptyMap(), + deepLinks: List = emptyList(), + noinline enterTransition: + (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards + EnterTransition?)? = { DefaultTransition.enter }, + noinline exitTransition: + (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards + ExitTransition?)? = { DefaultTransition.exit }, + noinline popEnterTransition: + (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards + EnterTransition?)? = { DefaultTransition.popEnter }, + noinline popExitTransition: + (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards + ExitTransition?)? = { DefaultTransition.popExit }, + noinline sizeTransform: + (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards + SizeTransform?)? = + null, + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit +) = composable( + enterTransition = enterTransition, + exitTransition = exitTransition, + popEnterTransition = popEnterTransition, + popExitTransition = popExitTransition, + sizeTransform = sizeTransform, + typeMap = typeMap, + deepLinks = deepLinks, + content = content +) diff --git a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt index 032e0691a..94a1be956 100644 --- a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt +++ b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.model.Course import de.tum.informatics.www1.artemis.native_app.core.ui.AwaitDeferredCompletion @@ -63,6 +62,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.computeC import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.computeCourseItemModifier import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.MarkdownText +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import kotlinx.coroutines.Deferred import kotlinx.serialization.Serializable import org.koin.androidx.compose.koinViewModel @@ -82,7 +82,7 @@ fun NavGraphBuilder.courseRegistration( onNavigateUp: () -> Unit, onRegisteredInCourse: (courseId: Long) -> Unit ) { - composable { + animatedComposable { RegisterForCourseScreen( modifier = Modifier.fillMaxSize(), viewModel = koinViewModel(), diff --git a/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt b/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt index 29d957e69..6ba43dce9 100644 --- a/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt +++ b/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt @@ -2,9 +2,6 @@ package de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets @@ -20,7 +17,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -33,9 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState @@ -46,17 +39,19 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateU import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.BoundExerciseActions import de.tum.informatics.www1.artemis.native_app.core.ui.generateLinks +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.DefaultTransition +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.courseview.GroupedByWeek import de.tum.informatics.www1.artemis.native_app.feature.courseview.R import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.CourseViewModel import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.LectureListUi import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.exercise_list.ExerciseListUi +import de.tum.informatics.www1.artemis.native_app.feature.metis.NavigateToUserConversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.NothingOpened +import de.tum.informatics.www1.artemis.native_app.feature.metis.OpenedConversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.OpenedThread import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.ConversationFacadeUi -import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NavigateToUserConversation -import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NothingOpened -import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedConversation -import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedThread import kotlinx.serialization.Serializable import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -98,7 +93,7 @@ fun NavGraphBuilder.course( generateLinks("courses/{courseId}/exercises") + generateLinks("courses/{courseId}/messages?conversationId={conversationId}") + generateLinks("courses/{courseId}/messages?username={username}") - composable( + animatedComposable( deepLinks = deepLinks ) { backStackEntry -> val route: CourseUiScreen = backStackEntry.toRoute() @@ -346,11 +341,9 @@ internal fun CourseUiScreen( targetState = selectedTabIndex, transitionSpec = { if (targetState > initialState) { - slideInHorizontally { width -> width } togetherWith - slideOutHorizontally { width -> -width } + DefaultTransition.navigateForward } else { - slideInHorizontally { width -> -width } togetherWith - slideOutHorizontally { width -> width } + DefaultTransition.navigateBack }.using( SizeTransform(clip = false) ) diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 77f94ba77..d824d68e6 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -43,9 +43,9 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable import de.tum.informatics.www1.artemis.native_app.core.model.Dashboard import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.dashboard.BuildConfig import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService @@ -71,7 +71,7 @@ fun NavGraphBuilder.dashboard( onClickRegisterForCourse: () -> Unit, onViewCourse: (courseId: Long) -> Unit ) { - composable { + animatedComposable { CoursesOverview( modifier = Modifier.fillMaxSize(), viewModel = koinViewModel(), diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt index bd5798234..4cc6c0c36 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt @@ -14,7 +14,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import androidx.navigation.toRoute @@ -25,6 +24,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateU import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.generateLinks import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.KSerializableNavType +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home.ExerciseScreen import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.participate.textexercise.TextExerciseParticipationScreen import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.viewresult.ViewResultScreen @@ -82,7 +82,7 @@ fun NavGraphBuilder.exercise( onParticipateInQuiz: (courseId: Long, exerciseId: Long, isPractice: Boolean) -> Unit, onClickViewQuizResults: (courseId: Long, exerciseId: Long) -> Unit ) { - composable( + animatedComposable( typeMap = mapOf( typeOf() to KSerializableNavType( isNullableAllowed = false, @@ -131,7 +131,7 @@ fun NavGraphBuilder.exercise( } NavHost(navController = nestedNavController, startDestination = startDestination) { - composable { + animatedComposable { ExerciseScreen( modifier = Modifier.fillMaxSize(), viewModel = exerciseViewModel, @@ -156,7 +156,7 @@ fun NavGraphBuilder.exercise( ) } - composable { + animatedComposable { ViewResultScreen( modifier = Modifier.fillMaxSize(), viewModel = exerciseViewModel, @@ -164,7 +164,7 @@ fun NavGraphBuilder.exercise( ) } - composable { backStackEntry -> + animatedComposable { backStackEntry -> val nestedRoute: ExerciseViewUiNestedNavigation.ParticipateTextExercise = backStackEntry.toRoute() diff --git a/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt b/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt index e4884dab0..cf461b59d 100644 --- a/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt +++ b/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt @@ -31,18 +31,16 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.navigation.toRoute -import io.github.fornewid.placeholder.material3.placeholder import de.tum.informatics.www1.artemis.native_app.core.model.lecture.Attachment import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog import de.tum.informatics.www1.artemis.native_app.core.ui.generateLinks +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.canDisplayMetisOnDisplaySide +import io.github.fornewid.placeholder.material3.placeholder import io.ktor.http.HttpHeaders import io.ktor.http.URLBuilder import io.ktor.http.appendPathSegments @@ -71,7 +69,7 @@ fun NavGraphBuilder.lecture( onParticipateInQuiz: (courseId: Long, exerciseId: Long, isPractice: Boolean) -> Unit, onClickViewQuizResults: (courseId: Long, exerciseId: Long) -> Unit, ) { - composable( + animatedComposable( deepLinks = listOf( navDeepLink { uriPattern = "artemis://lectures/{lectureId}" 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 b092a427e..278bdca04 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 @@ -3,9 +3,6 @@ package de.tum.informatics.www1.artemis.native_app.feature.login import androidx.compose.animation.AnimatedContent import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -26,13 +23,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -55,7 +54,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute @@ -66,6 +64,8 @@ import de.tum.informatics.www1.artemis.native_app.core.model.server_config.Profi import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.material.colors.linkTextColor +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.DefaultTransition +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.login.custom_instance_selection.CustomInstanceSelectionScreen import de.tum.informatics.www1.artemis.native_app.feature.login.instance_selection.InstanceSelectionScreen import de.tum.informatics.www1.artemis.native_app.feature.login.login.LoginScreen @@ -74,6 +74,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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -83,7 +84,6 @@ import org.koin.core.parameter.parametersOf import java.io.IOException private const val ARG_REMEMBER_ME = "rememberMe" -private const val NESTED_SAML2_LOGIN_ROUTE = "saml2_login" @Serializable private sealed interface NestedDestination { @@ -127,7 +127,9 @@ fun NavGraphBuilder.loginScreen( onFinishedLoginFlow: (deepLink: String?) -> Unit, onRequestOpenSettings: () -> Unit ) { - composable { + animatedComposable( + enterTransition = { DefaultTransition.fadeIn }, + ) { val screen = it.toRoute() val nextDestinationValue = screen.nextDestination @@ -148,9 +150,7 @@ fun NavGraphBuilder.loginScreen( AnimatedContent( targetState = currentContent, transitionSpec = { - // Animation is always the same - slideInHorizontally { width -> width } togetherWith - slideOutHorizontally { width -> -width } + DefaultTransition.navigateForward }, label = "Login <-> Notification configuration" ) { content -> @@ -260,6 +260,10 @@ private fun LoginUiScreen( ) } ) { paddingValues -> + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + var showBottomSheet by remember { mutableStateOf(false) } + NavHost( modifier = Modifier .fillMaxSize() @@ -269,7 +273,7 @@ private fun LoginUiScreen( navController = nestedNavController, startDestination = if (hasSelectedInstance) NestedDestination.Home else NestedDestination.InstanceSelection ) { - composable() { + animatedComposable { AccountScreen( modifier = Modifier.fillMaxSize(), canSwitchInstance = !BuildConfig.hasInstanceRestriction, @@ -281,19 +285,14 @@ private fun LoginUiScreen( }, onNavigateToInstanceSelection = { onNavigatedToInstanceSelection() - - nestedNavController.navigate(NestedDestination.InstanceSelection) { - popUpTo { - inclusive = true - } - } + showBottomSheet = true }, onLoggedIn = onLoggedIn, onClickSaml2Login = onClickSaml2Login ) } - composable { + animatedComposable { CustomInstanceSelectionScreen( modifier = Modifier .fillMaxSize() @@ -307,7 +306,7 @@ private fun LoginUiScreen( } } - composable { + animatedComposable { LoginScreen( modifier = Modifier.fillMaxSize(), viewModel = koinViewModel(), @@ -316,7 +315,7 @@ private fun LoginUiScreen( ) } - composable { backStack -> + animatedComposable { backStack -> val rememberMe = backStack.arguments?.getBoolean(ARG_REMEMBER_ME) checkNotNull(rememberMe) @@ -330,7 +329,7 @@ private fun LoginUiScreen( ) } - composable { + animatedComposable { RegisterUi( modifier = Modifier .fillMaxSize() @@ -343,29 +342,44 @@ private fun LoginUiScreen( } ) } + } - composable { - val scope = rememberCoroutineScope() + val hideBottomSheet: (action: (suspend CoroutineScope.() -> Unit)?) -> Unit = { action -> + scope.launch { + if (action != null) { + action() + } + sheetState.hide() + }.invokeOnCompletion { + if (!sheetState.isVisible) { + showBottomSheet = false + } + } + } + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState + ) { InstanceSelectionScreen( modifier = Modifier .fillMaxSize() .padding(horizontal = Spacings.ScreenHorizontalSpacing), availableInstances = ArtemisInstances.instances, onSelectArtemisInstance = { serverUrl -> - scope.launch { + hideBottomSheet { serverConfigurationService.updateServerUrl(serverUrl) - nestedNavController.navigate(NestedDestination.Home) { - popUpTo { - inclusive = true - } - } } }, onRequestOpenCustomInstanceSelection = { - nestedNavController.navigate( - NestedDestination.CustomInstanceSelection - ) + hideBottomSheet { + nestedNavController.navigate( + NestedDestination.CustomInstanceSelection + ) + } } ) } @@ -451,6 +465,7 @@ private fun AccountUi( bottom = WindowInsets.systemBars .asPaddingValues() .calculateBottomPadding() + + 8.dp ), text = AnnotatedString(stringResource(id = R.string.account_change_artemis_instance_label)), style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.linkTextColor), @@ -574,7 +589,7 @@ private fun LoginOrRegister( onClickLogin ) - Divider( + HorizontalDivider( modifier = Modifier .fillMaxWidth() .padding(start = 8.dp, end = 8.dp, top = 24.dp, bottom = 8.dp) diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt index 21560bdbf..f7dc4e843 100644 --- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt +++ b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt @@ -1,31 +1,26 @@ package de.tum.informatics.www1.artemis.native_app.feature.login.instance_selection import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -35,8 +30,6 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.request.ImageRequest import de.tum.informatics.www1.artemis.native_app.core.datastore.defaults.ArtemisInstances -import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass -import de.tum.informatics.www1.artemis.native_app.feature.login.ArtemisHeader import de.tum.informatics.www1.artemis.native_app.feature.login.R @Composable @@ -50,20 +43,6 @@ internal fun InstanceSelectionScreen( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.05f) - ) - - ArtemisHeader(modifier = Modifier.fillMaxWidth()) - - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.05f) - ) - Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.account_select_artemis_instance_select_text), @@ -71,22 +50,17 @@ internal fun InstanceSelectionScreen( fontWeight = FontWeight.Bold ) - val windowSizeClass = getWindowSizeClass() - val columnCount = when { - windowSizeClass.widthSizeClass > WindowWidthSizeClass.Expanded -> 6 - windowSizeClass.widthSizeClass > WindowWidthSizeClass.Medium -> 4 - else -> 2 - } - - LazyVerticalGrid( - columns = GridCells.Fixed(columnCount), - horizontalArrangement = Arrangement.spacedBy(16.dp), + LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues( bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() ) ) { - items(availableInstances) { instance -> + items( + count = availableInstances.size, + key = { availableInstances[it].serverUrl } + ) { index -> + val instance = availableInstances[index] val item = GridCellItem.ArtemisInstanceGridCellItem( instance = instance, imageUrl = "${instance.serverUrl}public/images/logo.png" @@ -118,14 +92,15 @@ private fun ArtemisInstanceGridCell( val context = LocalContext.current Card(modifier = modifier, onClick = onClick) { - Column( + Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically ) { val imageModifier = Modifier - .fillMaxWidth() + .height(64.dp) .aspectRatio(1f) when (item) { @@ -153,25 +128,11 @@ private fun ArtemisInstanceGridCell( } } - val name = item.getName() - - var threeLineName: String by remember(name) { - mutableStateOf(name) - } - Text( modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = threeLineName, + textAlign = TextAlign.Left, + text = item.getName(), style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - maxLines = 3, - onTextLayout = { - if (it.lineCount < 3) { - val additionalLines = 3 - it.lineCount - threeLineName += "\n".repeat(additionalLines) - } - } ) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationScreen.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationScreen.kt index 43de8d786..19dd85cb7 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationScreen.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationScreen.kt @@ -1,25 +1,29 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background +import androidx.compose.animation.SizeTransform import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.DefaultTransition import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -70,26 +74,50 @@ fun ConversationScreen( } val widthSizeClass = getWindowSizeClass().widthSizeClass + var showThread by remember { mutableStateOf(threadPostId != null) } + + val onNavigateUp = { + onCloseThread() + showThread = false + } + val onClickViewPost = { clientPostId: StandalonePostId -> + onOpenThread(clientPostId) + showThread = true + } when { widthSizeClass <= WindowWidthSizeClass.Compact -> { Box(modifier = modifier) { - ConversationChatListScreen( - modifier = Modifier.fillMaxSize(), - viewModel = viewModel, - courseId = courseId, - conversationId = conversationId, - onNavigateBack = onCloseConversation, - onNavigateToSettings = onNavigateToSettings, - onClickViewPost = { clientPostId -> onOpenThread(clientPostId) } - ) - - if (threadPostId != null) { - ConversationThreadScreen( - modifier = Modifier.fillMaxSize(), - viewModel = viewModel, - onNavigateUp = onCloseThread - ) + AnimatedContent( + targetState = showThread, + transitionSpec = { + if (targetState) { + DefaultTransition.navigateForward + } else { + DefaultTransition.navigateBack + }.using( + SizeTransform(clip = false) + ) + }, + label = "ConversationScreen chatList thread navigation animation" + ) { _showThread -> + if (_showThread) { + ConversationThreadScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + onNavigateUp = onNavigateUp + ) + } else { + ConversationChatListScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + courseId = courseId, + conversationId = conversationId, + onNavigateBack = onCloseConversation, + onNavigateToSettings = onNavigateToSettings, + onClickViewPost = onClickViewPost + ) + } } } } @@ -101,8 +129,7 @@ fun ConversationScreen( modifier = modifier, horizontalArrangement = arrangement ) { - val isOverviewVisible = - threadPostId == null || widthSizeClass >= WindowWidthSizeClass.Expanded + val isOverviewVisible = !showThread || widthSizeClass >= WindowWidthSizeClass.Expanded AnimatedVisibility( modifier = Modifier .weight(ConversationOverviewMaxWeight) @@ -125,8 +152,8 @@ fun ConversationScreen( } val otherWeight = when { - isOverviewVisible && threadPostId != null -> 0.35f - isOverviewVisible && threadPostId == null -> 0.7f + isOverviewVisible && showThread -> 0.35f + isOverviewVisible && !showThread -> 0.7f else -> 0.5f } @@ -143,29 +170,19 @@ fun ConversationScreen( conversationId = conversationId, onNavigateBack = onCloseConversation, onNavigateToSettings = onNavigateToSettings, - onClickViewPost = { clientPostId -> onOpenThread(clientPostId) } + onClickViewPost = onClickViewPost ) - if (threadPostId != null) { + if (showThread) { VerticalDivider() ConversationThreadScreen( modifier = otherModifier, viewModel = viewModel, - onNavigateUp = onCloseThread + onNavigateUp = onNavigateUp ) } } } } } - -@Composable -private fun VerticalDivider() { - Box( - modifier = Modifier - .fillMaxHeight() - .width(1.dp) - .background(DividerDefaults.color) - ) -} diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ConversationConfiguration.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ConversationConfiguration.kt new file mode 100644 index 000000000..1e402d2cd --- /dev/null +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ConversationConfiguration.kt @@ -0,0 +1,59 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis + +import android.os.Parcelable +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class ConversationConfiguration( + /** + * Used to determine the animation when switching between configurations. + */ + val navigationLevel: Int +) : Parcelable + +@Parcelize +data object NothingOpened : ConversationConfiguration(0) + +@Parcelize +data class OpenedConversation(val conversationId: Long, val openedThread: OpenedThread?) : + ConversationConfiguration(1) + +@Parcelize +data class OpenedThread(val conversationId: Long, val postId: StandalonePostId) : Parcelable + +/** + * Special configuration in which we want to navigate to the 1-to-1 conversation with the user with the specified username. + * In this configuration, we simply show a loading bar while we load the necessary data to show the chat. + */ +@Parcelize +data class NavigateToUserConversation(val username: String) : ConversationConfiguration(0) + +@Parcelize +internal data class AddChannelConfiguration( + val prevConfiguration: ConversationConfiguration +) : + ConversationConfiguration(1) + +@Parcelize +internal data class BrowseChannelConfiguration( + val prevConfiguration: ConversationConfiguration +) : + ConversationConfiguration(1) + +@Parcelize +internal data class CreatePersonalConversation(val prevConfiguration: ConversationConfiguration) : + ConversationConfiguration(1) + +@Parcelize +internal data class ConversationSettings( + val conversationId: Long, + val prevConfiguration: ConversationConfiguration, + val isAddingMembers: Boolean = false, + val isViewingAllMembers: Boolean = false +) : ConversationConfiguration( + navigationLevel = when(prevConfiguration) { + is ConversationSettings -> 3 + else -> 2 + } +) \ No newline at end of file diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt index 7b256bbb9..d7c621bba 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt @@ -3,6 +3,8 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import de.tum.informatics.www1.artemis.native_app.feature.metis.ConversationConfiguration +import de.tum.informatics.www1.artemis.native_app.feature.metis.NothingOpened import de.tum.informatics.www1.artemis.native_app.feature.metis.codeofconduct.ui.CodeOfConductFacadeUi import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt index a629e7c79..3ea79f654 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt @@ -1,7 +1,8 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.ui -import android.os.Parcelable import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -11,6 +12,16 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.DefaultTransition +import de.tum.informatics.www1.artemis.native_app.feature.metis.AddChannelConfiguration +import de.tum.informatics.www1.artemis.native_app.feature.metis.BrowseChannelConfiguration +import de.tum.informatics.www1.artemis.native_app.feature.metis.ConversationConfiguration +import de.tum.informatics.www1.artemis.native_app.feature.metis.ConversationSettings +import de.tum.informatics.www1.artemis.native_app.feature.metis.CreatePersonalConversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.NavigateToUserConversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.NothingOpened +import de.tum.informatics.www1.artemis.native_app.feature.metis.OpenedConversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.OpenedThread import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ConversationScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.browse_channels.BrowseChannelsScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.create_channel.CreateChannelScreen @@ -19,8 +30,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversati import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.add_members.ConversationAddMembersScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.members.ConversationMembersScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.overview.ConversationSettingsScreen -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId -import kotlinx.parcelize.Parcelize @Composable internal fun SinglePageConversationBody( @@ -76,170 +85,143 @@ internal fun SinglePageConversationBody( ) } - when (val config = configuration) { - NothingOpened -> { - conversationOverview(modifier) - } - - is OpenedConversation -> { - ConversationScreen( - modifier = modifier, - conversationId = config.conversationId, - threadPostId = config.openedThread?.postId, - courseId = courseId, - onOpenThread = { postId -> - configuration = OpenedConversation( - config.conversationId, - OpenedThread(config.conversationId, postId) - ) - }, - onCloseThread = { - configuration = config.copy(openedThread = null) - }, - onCloseConversation = { - configuration = NothingOpened - }, - onNavigateToSettings = { - configuration = ConversationSettings( - conversationId = config.conversationId, - prevConfiguration = config - ) - }, - conversationsOverview = { mod -> conversationOverview(mod) } + AnimatedContent( + targetState = configuration, + transitionSpec = { + val navigationLevelDiff = targetState.navigationLevel - initialState.navigationLevel + when (navigationLevelDiff) { + 1 -> DefaultTransition.navigateForward + -1 -> DefaultTransition.navigateBack + else -> DefaultTransition.navigateNeutral + } + .using( + SizeTransform(clip = false) ) - } + }, + contentKey = { + it.javaClass // Eg no recomposition of the ChatList when navigating to a thread. + }, + label = "SinglePageConversationBody screen transition animation" + ) { config -> + when (config) { + NothingOpened -> { + conversationOverview(modifier) + } - is BrowseChannelConfiguration -> { - BrowseChannelsScreen( - modifier = modifier, - courseId = courseId, - onNavigateToConversation = openConversation, - //onNavigateToCreateChannel = {}, - onNavigateBack = { configuration = config.prevConfiguration } - ) - } + is OpenedConversation -> { + ConversationScreen( + modifier = modifier, + conversationId = config.conversationId, + threadPostId = config.openedThread?.postId, + courseId = courseId, + onOpenThread = { postId -> + configuration = OpenedConversation( + config.conversationId, + OpenedThread(config.conversationId, postId) + ) + }, + onCloseThread = { + configuration = config.copy(openedThread = null) + }, + onCloseConversation = { + configuration = NothingOpened + }, + onNavigateToSettings = { + configuration = ConversationSettings( + conversationId = config.conversationId, + prevConfiguration = config + ) + }, + conversationsOverview = { mod -> conversationOverview(mod) } + ) + } - is AddChannelConfiguration -> { - if (canCreateChannel) { - CreateChannelScreen( + is BrowseChannelConfiguration -> { + BrowseChannelsScreen( modifier = modifier, courseId = courseId, - onConversationCreated = openConversation, + onNavigateToConversation = openConversation, onNavigateBack = { configuration = config.prevConfiguration } ) } - } - - is CreatePersonalConversation -> { - CreatePersonalConversationScreen( - modifier = modifier, - courseId = courseId, - onConversationCreated = openConversation, - onNavigateBack = { configuration = config.prevConfiguration } - ) - } - is ConversationSettings -> { - when { - config.isViewingAllMembers -> { - ConversationMembersScreen( + is AddChannelConfiguration -> { + if (canCreateChannel) { + CreateChannelScreen( modifier = modifier, courseId = courseId, - conversationId = config.conversationId, + onConversationCreated = openConversation, onNavigateBack = { configuration = config.prevConfiguration } ) } + } - config.isAddingMembers -> { - ConversationAddMembersScreen( - modifier = modifier, - courseId = courseId, - conversationId = config.conversationId, - onNavigateBack = { configuration = config.prevConfiguration } - ) - } + is CreatePersonalConversation -> { + CreatePersonalConversationScreen( + modifier = modifier, + courseId = courseId, + onConversationCreated = openConversation, + onNavigateBack = { configuration = config.prevConfiguration } + ) + } - else -> { - ConversationSettingsScreen( - modifier = modifier, - courseId = courseId, - conversationId = config.conversationId, - onNavigateBack = { configuration = config.prevConfiguration }, - onRequestAddMembers = { - configuration = config.copy( - isAddingMembers = true, - prevConfiguration = configuration - ) - }, - onRequestViewAllMembers = { - configuration = config.copy( - isViewingAllMembers = true, - prevConfiguration = configuration - ) - }, - onConversationLeft = { - configuration = NothingOpened - } - ) + is ConversationSettings -> { + when { + config.isViewingAllMembers -> { + ConversationMembersScreen( + modifier = modifier, + courseId = courseId, + conversationId = config.conversationId, + onNavigateBack = { configuration = config.prevConfiguration } + ) + } + + config.isAddingMembers -> { + ConversationAddMembersScreen( + modifier = modifier, + courseId = courseId, + conversationId = config.conversationId, + onNavigateBack = { configuration = config.prevConfiguration } + ) + } + + else -> { + ConversationSettingsScreen( + modifier = modifier, + courseId = courseId, + conversationId = config.conversationId, + onNavigateBack = { configuration = config.prevConfiguration }, + onRequestAddMembers = { + configuration = config.copy( + isAddingMembers = true, + prevConfiguration = configuration + ) + }, + onRequestViewAllMembers = { + configuration = config.copy( + isViewingAllMembers = true, + prevConfiguration = configuration + ) + }, + onConversationLeft = { + configuration = NothingOpened + } + ) + } } } - } - is NavigateToUserConversation -> { - NavigateToUserConversationUi( - modifier = modifier, - courseId = courseId, - username = config.username, - onNavigateToConversation = { conversationId -> - configuration = OpenedConversation(conversationId, null) - }, - onNavigateBack = { configuration = NothingOpened } - ) + is NavigateToUserConversation -> { + NavigateToUserConversationUi( + modifier = modifier, + courseId = courseId, + username = config.username, + onNavigateToConversation = { conversationId -> + configuration = OpenedConversation(conversationId, null) + }, + onNavigateBack = { configuration = NothingOpened } + ) + } } } } - - -@Parcelize -sealed interface ConversationConfiguration : Parcelable - -@Parcelize -data object NothingOpened : ConversationConfiguration - -@Parcelize -data class OpenedConversation(val conversationId: Long, val openedThread: OpenedThread?) : - ConversationConfiguration - -@Parcelize -data class OpenedThread(val conversationId: Long, val postId: StandalonePostId) : Parcelable - -/** - * Special configuration in which we want to navigate to the 1-to-1 conversation with the user with the specified username. - * In this configuration, we simply show a loading bar while we load the necessary data to show the chat. - */ -@Parcelize -data class NavigateToUserConversation(val username: String) : ConversationConfiguration - -@Parcelize -private data class AddChannelConfiguration( - val prevConfiguration: ConversationConfiguration -) : - ConversationConfiguration - -@Parcelize -private data class BrowseChannelConfiguration( - val prevConfiguration: ConversationConfiguration -) : - ConversationConfiguration - -@Parcelize -private data class CreatePersonalConversation(val prevConfiguration: ConversationConfiguration) : - ConversationConfiguration - -@Parcelize -private data class ConversationSettings( - val conversationId: Long, - val prevConfiguration: ConversationConfiguration, - val isAddingMembers: Boolean = false, - val isViewingAllMembers: Boolean = false -) : ConversationConfiguration diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt index 79dfe92d1..cfae93ece 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import androidx.navigation.navDeepLink import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.service.impl.JsonProvider @@ -35,6 +34,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.alert.DestructiveMarkd import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog import de.tum.informatics.www1.artemis.native_app.core.ui.common.ButtonWithLoadingAnimation import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.KSerializableNavType +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.quiz.QuizType import de.tum.informatics.www1.artemis.native_app.feature.quiz.R import de.tum.informatics.www1.artemis.native_app.feature.quiz.view_result.ViewQuizResultScreen @@ -68,7 +68,7 @@ fun NavController.navigateToQuizParticipation( } fun NavGraphBuilder.quizParticipation(onLeaveQuiz: () -> Unit) { - composable( + animatedComposable( typeMap = mapOf( typeOf() to KSerializableNavType( isNullableAllowed = false, diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt index 6362488a7..ddb173bcd 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.join @@ -25,6 +24,7 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExerci import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.QuizSubmission import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.Result import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable import de.tum.informatics.www1.artemis.native_app.feature.quiz.QuizType import de.tum.informatics.www1.artemis.native_app.feature.quiz.R import de.tum.informatics.www1.artemis.native_app.feature.quiz.participation.QuizQuestionData @@ -43,7 +43,7 @@ fun NavController.navigateToQuizResult( } fun NavGraphBuilder.quizResults(onRequestLeaveQuizResults: () -> Unit) { - composable { backStackEntry -> + animatedComposable { backStackEntry -> val route: ViewQuizResultScreen = backStackEntry.toRoute() ViewQuizResultScreen( diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 4e76737dd..6e7520d0d 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(project(":core:device")) implementation(project(":core:datastore")) implementation(project(":feature:push")) + implementation(project(":feature:login")) implementation(libs.placeholder.material) implementation(libs.androidx.browser) 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 e7887c7aa..fb97d1cc6 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 @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet @@ -55,6 +54,9 @@ import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvi import de.tum.informatics.www1.artemis.native_app.core.model.account.Account import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.DefaultTransition +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.animatedComposable +import de.tum.informatics.www1.artemis.native_app.feature.login.LoginScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePicture 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 @@ -91,7 +93,15 @@ fun NavGraphBuilder.settingsScreen( onLoggedOut: () -> Unit, onDisplayThirdPartyLicenses: () -> Unit ) { - composable { + animatedComposable( + exitTransition = { + val toLoginScreen = targetState.destination.route?.startsWith(LoginScreen::class.qualifiedName!!) ?: false + if (toLoginScreen) { + return@animatedComposable DefaultTransition.fadeOut + } + DefaultTransition.exit + } + ) { SettingsScreen( modifier = Modifier.fillMaxSize(), versionCode = versionCode, @@ -104,7 +114,7 @@ fun NavGraphBuilder.settingsScreen( } } - composable { + animatedComposable { PushNotificationSettingsScreen( modifier = Modifier.fillMaxSize(), onNavigateBack = onNavigateUp diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1be1b6727..292ae5b3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ kotlin = "2.1.0" kotlinxCoroutines = "1.10.1" kotlinxDatetime = "0.6.1" kotlinxSerializationJson = "1.7.3" -koin = "4.0.0" +koin = "4.0.1" koinAndroidxCompose = "4.0.0" kover = "0.9.0" krossbow = "8.1.0" @@ -32,7 +32,7 @@ mockk = "1.13.14" ossLicensesPlugin = "0.10.6" placeholderMaterial = "2.0.0" room = "2.6.1" -sentry-android = "7.19.0" +sentry-android = "7.19.1" work = "2.10.0" [libraries]