diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 5e5254d2..63790cd4 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -13,18 +13,33 @@ import android.view.Menu import android.view.View import android.view.ViewTreeObserver import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseInOutExpo import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut 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.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.Button +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -34,13 +49,21 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 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.setValue +import androidx.compose.ui.Alignment 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.TransformOrigin import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -394,6 +417,8 @@ val RevoltTweenColour: FiniteAnimationSpec = tween(400, easing = EaseInOu val NavTweenInt: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) val NavTweenFloat: FiniteAnimationSpec = 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 fun AppEntrypoint( windowSizeClass: WindowSizeClass, @@ -408,251 +433,332 @@ fun AppEntrypoint( onRetryConnection: () -> 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() RevoltTheme( requestedTheme = LoadedSettings.theme, colourOverrides = SyncedSettings.android.colourOverrides ) { - Surface( - modifier = Modifier - .fillMaxSize(), - color = MaterialTheme.colorScheme.background + Box( + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLowest) ) { - if (isHealthAlertActive) { - healthNotice?.let { - HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert) - } - } - - 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) - } + Surface( + modifier = Modifier + .fillMaxSize() + .scale(chatUIScale) + .alpha(chatUIOpacity), + color = MaterialTheme.colorScheme.background ) { - composable("default") { - DefaultDestinationScreen( - navController, - nextDestination, - isConnected, - onRetryConnection - ) + if (isHealthAlertActive) { + healthNotice?.let { + HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert) + } } - 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") + 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)) + } } ) } - 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, - 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}", + NavHost( + navController = navController, + startDestination = "default", enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = tween( - 600, - easing = EasingTokens.EmphasizedDecelerate - ), - initialOffset = { it } - ) + fadeIn(animationSpec = RevoltTweenFloat) + 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 = tween( - 600, - easing = EasingTokens.EmphasizedDecelerate - ), - targetOffset = { it } - ) + fadeOut(animationSpec = RevoltTweenFloat) + animationSpec = NavTweenInt, + initialOffset = { it / 3 } + ) + fadeIn(animationSpec = NavTweenFloat) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = NavTweenInt, + targetOffset = { it / 2 } + ) + fadeOut(animationSpec = NavTweenFloat) } - ) { backStackEntry -> - val channelId = backStackEntry.arguments?.getString("channelId") ?: "" - ChannelScreen( - channelId = channelId, - onToggleDrawer = {}, - useDrawer = false, - useBackButton = true, - backButtonAction = { - navController.popBackStack() + ) { + composable("default") { + DefaultDestinationScreen( + navController, + nextDestination, + isConnected, + onRetryConnection + ) + } + + 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) } } } } diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 8678e753..d9467596 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -266,7 +266,9 @@ class ChatRouterViewModel @Inject constructor( fun ChatRouterScreen( topNav: NavController, windowSizeClass: WindowSizeClass, + disableBackHandler: Boolean, onNullifiedUser: () -> Unit, + onEnterVoiceUI: () -> Unit, viewModel: ChatRouterViewModel = hiltViewModel() ) { val drawerState = rememberDrawerState(DrawerValue.Closed) @@ -848,9 +850,11 @@ fun ChatRouterScreen( dest = viewModel.currentDestination, topNav = topNav, useDrawer = false, + disableBackHandler = disableBackHandler, toggleDrawer = { toggleDrawerLambda() - } + }, + onEnterVoiceUI = onEnterVoiceUI, ) } } else { @@ -891,6 +895,7 @@ fun ChatRouterScreen( dest = viewModel.currentDestination, topNav = topNav, useDrawer = true, + disableBackHandler = disableBackHandler, toggleDrawer = { toggleDrawerLambda() }, @@ -898,7 +903,8 @@ fun ChatRouterScreen( drawerGestureEnabled = useSidebarGesture, setDrawerGestureEnabled = { useSidebarGesture = it - } + }, + onEnterVoiceUI = onEnterVoiceUI, ) } } @@ -942,11 +948,13 @@ fun ChannelNavigator( toggleDrawer: () -> Unit, drawerState: DrawerState? = null, drawerGestureEnabled: Boolean = true, + disableBackHandler: Boolean = false, + onEnterVoiceUI: () -> Unit = {}, setDrawerGestureEnabled: (Boolean) -> Unit = {}, ) { val scope = rememberCoroutineScope() - BackHandler(enabled = useDrawer) { + BackHandler(useDrawer && !disableBackHandler) { toggleDrawer() } @@ -984,6 +992,7 @@ fun ChannelNavigator( drawerGestureEnabled = drawerGestureEnabled, setDrawerGestureEnabled = setDrawerGestureEnabled, drawerIsOpen = drawerState?.isOpen == true, + onEnterVoiceUI = onEnterVoiceUI, ) } diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 1825658e..b29f6cb8 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -61,7 +61,6 @@ import androidx.compose.material3.AssistChip import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DrawerState import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem 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.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.Placeholder @@ -200,6 +198,7 @@ fun ChannelScreen( drawerIsOpen: Boolean = false, backButtonAction: (() -> Unit)? = null, useChatUI: Boolean = false, + onEnterVoiceUI: () -> Unit = {}, viewModel: ChannelScreenViewModel = hiltViewModel() ) { // @@ -848,74 +847,89 @@ fun ChannelScreen( } } - if (viewModel.showPhysicalKeyboardSpark) { - Card( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(8.dp) - ) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(16.dp) - ) { - Text( - 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), + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(8.dp) + ) { + if (viewModel.showPhysicalKeyboardSpark) { + Card { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp) ) { - Button( - onClick = { - viewModel.dismissPhysicalKeyboardSpark() + Text( + 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) }, - 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)) - } - TextButton( - onClick = { - (context as Activity).requestShowKeyboardShortcuts() - }, - modifier = Modifier.weight(1f) - ) { - Text(stringResource(R.string.spark_keyboard_shortcuts_cta)) + Button( + onClick = { + viewModel.dismissPhysicalKeyboardSpark() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss)) + } + 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") + } + } } }