feat: basic implementation of voice channel overlay

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-05-04 04:58:22 +02:00
parent aab658eccc
commit 14522fd6f3
3 changed files with 415 additions and 286 deletions

View File

@ -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) }
} }
} }
} }

View File

@ -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,
) )
} }

View File

@ -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")
}
}
} }
} }