feat: basic implementation of voice channel overlay
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
aab658eccc
commit
14522fd6f3
|
|
@ -13,18 +13,33 @@ import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.EaseInOutExpo
|
import androidx.compose.animation.core.EaseInOutExpo
|
||||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -34,13 +49,21 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -394,6 +417,8 @@ val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOu
|
||||||
val NavTweenInt: FiniteAnimationSpec<IntOffset> = tween(350, easing = EaseInOutExpo)
|
val NavTweenInt: FiniteAnimationSpec<IntOffset> = tween(350, easing = EaseInOutExpo)
|
||||||
val NavTweenFloat: FiniteAnimationSpec<Float> = tween(350, easing = EaseInOutExpo)
|
val NavTweenFloat: FiniteAnimationSpec<Float> = tween(350, easing = EaseInOutExpo)
|
||||||
|
|
||||||
|
// This composable handles the main compose entrypoint of the app, provides the main navigation
|
||||||
|
// graph, and handles the animation and layout for the voice chat UI.
|
||||||
@Composable
|
@Composable
|
||||||
fun AppEntrypoint(
|
fun AppEntrypoint(
|
||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
|
|
@ -408,251 +433,332 @@ fun AppEntrypoint(
|
||||||
onRetryConnection: () -> Unit,
|
onRetryConnection: () -> Unit,
|
||||||
onUpdateNextDestination: (String) -> Unit = {}
|
onUpdateNextDestination: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
var showVoiceUI by remember { mutableStateOf(false) }
|
||||||
|
val chatUIScale by animateFloatAsState(
|
||||||
|
if (showVoiceUI) 0.8f else 1.0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chatUIOpacity by animateFloatAsState(
|
||||||
|
if (showVoiceUI) 0.8f else 1.0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BackHandler(showVoiceUI) {
|
||||||
|
showVoiceUI = false
|
||||||
|
}
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
RevoltTheme(
|
RevoltTheme(
|
||||||
requestedTheme = LoadedSettings.theme,
|
requestedTheme = LoadedSettings.theme,
|
||||||
colourOverrides = SyncedSettings.android.colourOverrides
|
colourOverrides = SyncedSettings.android.colourOverrides
|
||||||
) {
|
) {
|
||||||
Surface(
|
Box(
|
||||||
modifier = Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize()
|
||||||
color = MaterialTheme.colorScheme.background
|
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||||
) {
|
) {
|
||||||
if (isHealthAlertActive) {
|
Surface(
|
||||||
healthNotice?.let {
|
modifier = Modifier
|
||||||
HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert)
|
.fillMaxSize()
|
||||||
}
|
.scale(chatUIScale)
|
||||||
}
|
.alpha(chatUIOpacity),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
if (couldNotLogIn) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = {
|
|
||||||
// no-op
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(stringResource(R.string.could_not_log_in_heading))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.could_not_log_in_body))
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissLoginError()
|
|
||||||
onRetryConnection()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.could_not_log_in_cta_try_again))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissLoginError()
|
|
||||||
onLogout()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.could_not_log_in_cta_logout))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = "default",
|
|
||||||
enterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
|
||||||
animationSpec = NavTweenInt,
|
|
||||||
initialOffset = { it / 3 }
|
|
||||||
) + fadeIn(animationSpec = NavTweenFloat)
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
|
||||||
animationSpec = NavTweenInt,
|
|
||||||
targetOffset = { it / 3 }
|
|
||||||
) + fadeOut(animationSpec = NavTweenFloat)
|
|
||||||
},
|
|
||||||
popEnterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
|
||||||
animationSpec = NavTweenInt,
|
|
||||||
initialOffset = { it / 3 }
|
|
||||||
) + fadeIn(animationSpec = NavTweenFloat)
|
|
||||||
},
|
|
||||||
popExitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
|
||||||
animationSpec = NavTweenInt,
|
|
||||||
targetOffset = { it / 2 }
|
|
||||||
) + fadeOut(animationSpec = NavTweenFloat)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
composable("default") {
|
if (isHealthAlertActive) {
|
||||||
DefaultDestinationScreen(
|
healthNotice?.let {
|
||||||
navController,
|
HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert)
|
||||||
nextDestination,
|
}
|
||||||
isConnected,
|
|
||||||
onRetryConnection
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("login/greeting") { LoginGreetingScreen(navController) }
|
if (couldNotLogIn) {
|
||||||
composable("login/login") { LoginScreen(navController) }
|
AlertDialog(
|
||||||
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
|
onDismissRequest = {
|
||||||
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
|
// no-op
|
||||||
val allowedAuthTypes =
|
},
|
||||||
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
|
title = {
|
||||||
|
Text(stringResource(R.string.could_not_log_in_heading))
|
||||||
MfaScreen(navController, allowedAuthTypes, mfaTicket)
|
},
|
||||||
}
|
text = {
|
||||||
|
Text(stringResource(R.string.could_not_log_in_body))
|
||||||
composable("register/greeting") { RegisterGreetingScreen(navController) }
|
},
|
||||||
composable("register/details") { RegisterDetailsScreen(navController) }
|
confirmButton = {
|
||||||
composable("register/verify/{email}") { backStackEntry ->
|
TextButton(
|
||||||
val email = backStackEntry.arguments?.getString("email") ?: ""
|
onClick = {
|
||||||
|
onDismissLoginError()
|
||||||
RegisterVerifyScreen(navController, email)
|
onRetryConnection()
|
||||||
}
|
}
|
||||||
composable("register/onboarding") {
|
) {
|
||||||
OnboardingScreen(
|
Text(stringResource(R.string.could_not_log_in_cta_try_again))
|
||||||
navController,
|
}
|
||||||
onOnboardingComplete = {
|
},
|
||||||
onUpdateNextDestination("chat")
|
dismissButton = {
|
||||||
navController.popBackStack(
|
TextButton(
|
||||||
navController.graph.startDestinationRoute!!,
|
onClick = {
|
||||||
inclusive = true
|
onDismissLoginError()
|
||||||
)
|
onLogout()
|
||||||
navController.navigate("default")
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.could_not_log_in_cta_logout))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("login2/init") { InitScreen(navController, windowSizeClass) }
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
// This is only used outside of Polar mode
|
startDestination = "default",
|
||||||
// Otherwise you may be looking for "main" right below
|
|
||||||
composable(
|
|
||||||
"chat",
|
|
||||||
enterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
|
||||||
animationSpec = tween(
|
|
||||||
400,
|
|
||||||
easing = EasingTokens.EmphasizedDecelerate
|
|
||||||
),
|
|
||||||
initialOffset = { it / 3 }
|
|
||||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
ChatRouterScreen(
|
|
||||||
navController,
|
|
||||||
windowSizeClass,
|
|
||||||
onNullifiedUser = {
|
|
||||||
onRetryConnection()
|
|
||||||
navController.popBackStack(
|
|
||||||
navController.graph.startDestinationRoute!!,
|
|
||||||
inclusive = true
|
|
||||||
)
|
|
||||||
navController.navigate("default")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only the main screen in Polar mode
|
|
||||||
// Otherwise you may be looking for "chat" right above
|
|
||||||
composable(
|
|
||||||
"main",
|
|
||||||
enterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
|
||||||
animationSpec = tween(
|
|
||||||
400,
|
|
||||||
easing = EasingTokens.EmphasizedDecelerate
|
|
||||||
),
|
|
||||||
initialOffset = { it / 3 }
|
|
||||||
) + fadeIn(animationSpec = RevoltTweenFloat) + scaleIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
400,
|
|
||||||
easing = EasingTokens.EmphasizedDecelerate
|
|
||||||
),
|
|
||||||
initialScale = 0.8f,
|
|
||||||
transformOrigin = TransformOrigin.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
MainScreen(navController)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
"main/conversation/{channelId}",
|
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
slideIntoContainer(
|
slideIntoContainer(
|
||||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||||
animationSpec = tween(
|
animationSpec = NavTweenInt,
|
||||||
600,
|
initialOffset = { it / 3 }
|
||||||
easing = EasingTokens.EmphasizedDecelerate
|
) + fadeIn(animationSpec = NavTweenFloat)
|
||||||
),
|
|
||||||
initialOffset = { it }
|
|
||||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
|
||||||
},
|
},
|
||||||
exitTransition = {
|
exitTransition = {
|
||||||
slideOutOfContainer(
|
slideOutOfContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||||
|
animationSpec = NavTweenInt,
|
||||||
|
targetOffset = { it / 3 }
|
||||||
|
) + fadeOut(animationSpec = NavTweenFloat)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
slideIntoContainer(
|
||||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||||
animationSpec = tween(
|
animationSpec = NavTweenInt,
|
||||||
600,
|
initialOffset = { it / 3 }
|
||||||
easing = EasingTokens.EmphasizedDecelerate
|
) + fadeIn(animationSpec = NavTweenFloat)
|
||||||
),
|
},
|
||||||
targetOffset = { it }
|
popExitTransition = {
|
||||||
) + fadeOut(animationSpec = RevoltTweenFloat)
|
slideOutOfContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||||
|
animationSpec = NavTweenInt,
|
||||||
|
targetOffset = { it / 2 }
|
||||||
|
) + fadeOut(animationSpec = NavTweenFloat)
|
||||||
}
|
}
|
||||||
) { backStackEntry ->
|
) {
|
||||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
composable("default") {
|
||||||
ChannelScreen(
|
DefaultDestinationScreen(
|
||||||
channelId = channelId,
|
navController,
|
||||||
onToggleDrawer = {},
|
nextDestination,
|
||||||
useDrawer = false,
|
isConnected,
|
||||||
useBackButton = true,
|
onRetryConnection
|
||||||
backButtonAction = {
|
)
|
||||||
navController.popBackStack()
|
}
|
||||||
|
|
||||||
|
composable("login/greeting") { LoginGreetingScreen(navController) }
|
||||||
|
composable("login/login") { LoginScreen(navController) }
|
||||||
|
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
|
||||||
|
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
|
||||||
|
val allowedAuthTypes =
|
||||||
|
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
|
||||||
|
|
||||||
|
MfaScreen(navController, allowedAuthTypes, mfaTicket)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("register/greeting") { RegisterGreetingScreen(navController) }
|
||||||
|
composable("register/details") { RegisterDetailsScreen(navController) }
|
||||||
|
composable("register/verify/{email}") { backStackEntry ->
|
||||||
|
val email = backStackEntry.arguments?.getString("email") ?: ""
|
||||||
|
|
||||||
|
RegisterVerifyScreen(navController, email)
|
||||||
|
}
|
||||||
|
composable("register/onboarding") {
|
||||||
|
OnboardingScreen(
|
||||||
|
navController,
|
||||||
|
onOnboardingComplete = {
|
||||||
|
onUpdateNextDestination("chat")
|
||||||
|
navController.popBackStack(
|
||||||
|
navController.graph.startDestinationRoute!!,
|
||||||
|
inclusive = true
|
||||||
|
)
|
||||||
|
navController.navigate("default")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("login2/init") { InitScreen(navController, windowSizeClass) }
|
||||||
|
|
||||||
|
// This is only used outside of Polar mode
|
||||||
|
// Otherwise you may be looking for "main" right below
|
||||||
|
composable(
|
||||||
|
"chat",
|
||||||
|
enterTransition = {
|
||||||
|
slideIntoContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||||
|
animationSpec = tween(
|
||||||
|
400,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
),
|
||||||
|
initialOffset = { it / 3 }
|
||||||
|
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ChatRouterScreen(
|
||||||
|
navController,
|
||||||
|
windowSizeClass,
|
||||||
|
disableBackHandler = showVoiceUI,
|
||||||
|
onNullifiedUser = {
|
||||||
|
onRetryConnection()
|
||||||
|
navController.popBackStack(
|
||||||
|
navController.graph.startDestinationRoute!!,
|
||||||
|
inclusive = true
|
||||||
|
)
|
||||||
|
navController.navigate("default")
|
||||||
|
},
|
||||||
|
onEnterVoiceUI = {
|
||||||
|
showVoiceUI = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only the main screen in Polar mode
|
||||||
|
// Otherwise you may be looking for "chat" right above
|
||||||
|
composable(
|
||||||
|
"main",
|
||||||
|
enterTransition = {
|
||||||
|
slideIntoContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||||
|
animationSpec = tween(
|
||||||
|
400,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
),
|
||||||
|
initialOffset = { it / 3 }
|
||||||
|
) + fadeIn(animationSpec = RevoltTweenFloat) + scaleIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
400,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
),
|
||||||
|
initialScale = 0.8f,
|
||||||
|
transformOrigin = TransformOrigin.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MainScreen(navController)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
"main/conversation/{channelId}",
|
||||||
|
enterTransition = {
|
||||||
|
slideIntoContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||||
|
animationSpec = tween(
|
||||||
|
600,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
),
|
||||||
|
initialOffset = { it }
|
||||||
|
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||||
},
|
},
|
||||||
useChatUI = true
|
exitTransition = {
|
||||||
|
slideOutOfContainer(
|
||||||
|
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||||
|
animationSpec = tween(
|
||||||
|
600,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
),
|
||||||
|
targetOffset = { it }
|
||||||
|
) + fadeOut(animationSpec = RevoltTweenFloat)
|
||||||
|
}
|
||||||
|
) { backStackEntry ->
|
||||||
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
ChannelScreen(
|
||||||
|
channelId = channelId,
|
||||||
|
onToggleDrawer = {},
|
||||||
|
useDrawer = false,
|
||||||
|
useBackButton = true,
|
||||||
|
backButtonAction = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
useChatUI = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("create/group") { CreateGroupScreen(navController) }
|
||||||
|
|
||||||
|
composable("discover") { DiscoverScreen(navController) }
|
||||||
|
|
||||||
|
composable("settings") { SettingsScreen(navController) }
|
||||||
|
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
||||||
|
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||||
|
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||||
|
composable("settings/chat") { ChatSettingsScreen(navController) }
|
||||||
|
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||||
|
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
||||||
|
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
||||||
|
composable("settings/language") { LanguagePickerSettingsScreen(navController) }
|
||||||
|
|
||||||
|
composable("settings/channel/{channelId}") { backStackEntry ->
|
||||||
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
ChannelSettingsHome(navController, channelId)
|
||||||
|
}
|
||||||
|
composable("settings/channel/{channelId}/overview") { backStackEntry ->
|
||||||
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
ChannelSettingsOverview(navController, channelId)
|
||||||
|
}
|
||||||
|
composable("settings/channel/{channelId}/permissions") { backStackEntry ->
|
||||||
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
ChannelSettingsPermissions(navController, channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("about") { AboutScreen(navController) }
|
||||||
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
|
|
||||||
|
composable("labs") { LabsRootScreen(navController) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showVoiceUI) { // if tapped outside the voice UI, close it
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
showVoiceUI = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showVoiceUI,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { it -> it },
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { it -> it },
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// We need a box as applying the padding elsewhere leads to either
|
||||||
|
// janky animation or layout
|
||||||
|
Box(Modifier.safeDrawingPadding()) {
|
||||||
|
Card(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.widthIn(max = 600.dp)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
showVoiceUI = false
|
||||||
|
}) {
|
||||||
|
Text("Close voice UI")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("create/group") { CreateGroupScreen(navController) }
|
|
||||||
|
|
||||||
composable("discover") { DiscoverScreen(navController) }
|
|
||||||
|
|
||||||
composable("settings") { SettingsScreen(navController) }
|
|
||||||
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
|
||||||
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
|
||||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
|
||||||
composable("settings/chat") { ChatSettingsScreen(navController) }
|
|
||||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
|
||||||
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
|
||||||
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
|
||||||
composable("settings/language") { LanguagePickerSettingsScreen(navController) }
|
|
||||||
|
|
||||||
composable("settings/channel/{channelId}") { backStackEntry ->
|
|
||||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
|
||||||
ChannelSettingsHome(navController, channelId)
|
|
||||||
}
|
|
||||||
composable("settings/channel/{channelId}/overview") { backStackEntry ->
|
|
||||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
|
||||||
ChannelSettingsOverview(navController, channelId)
|
|
||||||
}
|
|
||||||
composable("settings/channel/{channelId}/permissions") { backStackEntry ->
|
|
||||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
|
||||||
ChannelSettingsPermissions(navController, channelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable("about") { AboutScreen(navController) }
|
|
||||||
composable("about/oss") { AttributionScreen(navController) }
|
|
||||||
|
|
||||||
composable("labs") { LabsRootScreen(navController) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,9 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
fun ChatRouterScreen(
|
fun ChatRouterScreen(
|
||||||
topNav: NavController,
|
topNav: NavController,
|
||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
|
disableBackHandler: Boolean,
|
||||||
onNullifiedUser: () -> Unit,
|
onNullifiedUser: () -> Unit,
|
||||||
|
onEnterVoiceUI: () -> Unit,
|
||||||
viewModel: ChatRouterViewModel = hiltViewModel()
|
viewModel: ChatRouterViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
|
@ -848,9 +850,11 @@ fun ChatRouterScreen(
|
||||||
dest = viewModel.currentDestination,
|
dest = viewModel.currentDestination,
|
||||||
topNav = topNav,
|
topNav = topNav,
|
||||||
useDrawer = false,
|
useDrawer = false,
|
||||||
|
disableBackHandler = disableBackHandler,
|
||||||
toggleDrawer = {
|
toggleDrawer = {
|
||||||
toggleDrawerLambda()
|
toggleDrawerLambda()
|
||||||
}
|
},
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -891,6 +895,7 @@ fun ChatRouterScreen(
|
||||||
dest = viewModel.currentDestination,
|
dest = viewModel.currentDestination,
|
||||||
topNav = topNav,
|
topNav = topNav,
|
||||||
useDrawer = true,
|
useDrawer = true,
|
||||||
|
disableBackHandler = disableBackHandler,
|
||||||
toggleDrawer = {
|
toggleDrawer = {
|
||||||
toggleDrawerLambda()
|
toggleDrawerLambda()
|
||||||
},
|
},
|
||||||
|
|
@ -898,7 +903,8 @@ fun ChatRouterScreen(
|
||||||
drawerGestureEnabled = useSidebarGesture,
|
drawerGestureEnabled = useSidebarGesture,
|
||||||
setDrawerGestureEnabled = {
|
setDrawerGestureEnabled = {
|
||||||
useSidebarGesture = it
|
useSidebarGesture = it
|
||||||
}
|
},
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -942,11 +948,13 @@ fun ChannelNavigator(
|
||||||
toggleDrawer: () -> Unit,
|
toggleDrawer: () -> Unit,
|
||||||
drawerState: DrawerState? = null,
|
drawerState: DrawerState? = null,
|
||||||
drawerGestureEnabled: Boolean = true,
|
drawerGestureEnabled: Boolean = true,
|
||||||
|
disableBackHandler: Boolean = false,
|
||||||
|
onEnterVoiceUI: () -> Unit = {},
|
||||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
BackHandler(enabled = useDrawer) {
|
BackHandler(useDrawer && !disableBackHandler) {
|
||||||
toggleDrawer()
|
toggleDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,6 +992,7 @@ fun ChannelNavigator(
|
||||||
drawerGestureEnabled = drawerGestureEnabled,
|
drawerGestureEnabled = drawerGestureEnabled,
|
||||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||||
drawerIsOpen = drawerState?.isOpen == true,
|
drawerIsOpen = drawerState?.isOpen == true,
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DrawerState
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -95,7 +94,6 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
|
@ -200,6 +198,7 @@ fun ChannelScreen(
|
||||||
drawerIsOpen: Boolean = false,
|
drawerIsOpen: Boolean = false,
|
||||||
backButtonAction: (() -> Unit)? = null,
|
backButtonAction: (() -> Unit)? = null,
|
||||||
useChatUI: Boolean = false,
|
useChatUI: Boolean = false,
|
||||||
|
onEnterVoiceUI: () -> Unit = {},
|
||||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// <editor-fold desc="State and effects">
|
// <editor-fold desc="State and effects">
|
||||||
|
|
@ -848,74 +847,89 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.showPhysicalKeyboardSpark) {
|
Column(
|
||||||
Card(
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
if (viewModel.showPhysicalKeyboardSpark) {
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
Card {
|
||||||
modifier = Modifier.padding(16.dp)
|
Column(
|
||||||
) {
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
Text(
|
modifier = Modifier.padding(16.dp)
|
||||||
stringResource(R.string.spark_keyboard_shortcuts),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
buildAnnotatedString {
|
|
||||||
val raw =
|
|
||||||
stringResource(R.string.spark_keyboard_shortcuts_description)
|
|
||||||
val before = raw.substringBefore("%1\$s")
|
|
||||||
val after = raw.substringAfter("%1\$s")
|
|
||||||
|
|
||||||
append(before)
|
|
||||||
appendInlineContent("metaKey", "Meta")
|
|
||||||
append(" + /")
|
|
||||||
append(after)
|
|
||||||
},
|
|
||||||
inlineContent = mapOf(
|
|
||||||
"metaKey" to InlineTextContent(
|
|
||||||
placeholder = Placeholder(
|
|
||||||
width = 1.em,
|
|
||||||
height = 1.em,
|
|
||||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
with(LocalDensity.current) {
|
|
||||||
Image(
|
|
||||||
painterResource(R.drawable.ic_meta_key_24dp),
|
|
||||||
contentDescription = null,
|
|
||||||
/*modifier = Modifier.size(1.em.toDp())*/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
) {
|
||||||
Button(
|
Text(
|
||||||
onClick = {
|
stringResource(R.string.spark_keyboard_shortcuts),
|
||||||
viewModel.dismissPhysicalKeyboardSpark()
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
val raw =
|
||||||
|
stringResource(R.string.spark_keyboard_shortcuts_description)
|
||||||
|
val before = raw.substringBefore("%1\$s")
|
||||||
|
val after = raw.substringAfter("%1\$s")
|
||||||
|
|
||||||
|
append(before)
|
||||||
|
appendInlineContent("metaKey", "Meta")
|
||||||
|
append(" + /")
|
||||||
|
append(after)
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
inlineContent = mapOf(
|
||||||
|
"metaKey" to InlineTextContent(
|
||||||
|
placeholder = Placeholder(
|
||||||
|
width = 1.em,
|
||||||
|
height = 1.em,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
Image(
|
||||||
|
painterResource(R.drawable.ic_meta_key_24dp),
|
||||||
|
contentDescription = null,
|
||||||
|
/*modifier = Modifier.size(1.em.toDp())*/
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss))
|
Button(
|
||||||
}
|
onClick = {
|
||||||
TextButton(
|
viewModel.dismissPhysicalKeyboardSpark()
|
||||||
onClick = {
|
},
|
||||||
(context as Activity).requestShowKeyboardShortcuts()
|
modifier = Modifier.weight(1f)
|
||||||
},
|
) {
|
||||||
modifier = Modifier.weight(1f)
|
Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss))
|
||||||
) {
|
}
|
||||||
Text(stringResource(R.string.spark_keyboard_shortcuts_cta))
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
(context as Activity).requestShowKeyboardShortcuts()
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.spark_keyboard_shortcuts_cta))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.channel?.channelType == ChannelType.VoiceChannel) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onEnterVoiceUI()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Join Voice Channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue