feat: bottom navigation when drawer is open, placeholder settings screen
This commit is contained in:
parent
89d372d65c
commit
2e6e20939f
|
|
@ -12,6 +12,7 @@ import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
import chat.revolt.screens.SplashScreen
|
import chat.revolt.screens.SplashScreen
|
||||||
import chat.revolt.screens.about.AboutScreen
|
import chat.revolt.screens.about.AboutScreen
|
||||||
import chat.revolt.screens.about.AttributionScreen
|
import chat.revolt.screens.about.AttributionScreen
|
||||||
|
|
@ -20,6 +21,7 @@ import chat.revolt.screens.chat.ChatRouterScreen
|
||||||
import chat.revolt.screens.login.GreeterScreen
|
import chat.revolt.screens.login.GreeterScreen
|
||||||
import chat.revolt.screens.login.LoginScreen
|
import chat.revolt.screens.login.LoginScreen
|
||||||
import chat.revolt.screens.login.MfaScreen
|
import chat.revolt.screens.login.MfaScreen
|
||||||
|
import chat.revolt.screens.settings.SettingsScreen
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import com.google.accompanist.navigation.animation.composable
|
||||||
|
|
@ -44,6 +46,7 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val RevoltTweenInt: FiniteAnimationSpec<IntOffset> = tween(400, easing = EaseInOutExpo)
|
val RevoltTweenInt: FiniteAnimationSpec<IntOffset> = tween(400, easing = EaseInOutExpo)
|
||||||
|
val RevoltTweenIntSize: FiniteAnimationSpec<IntSize> = tween(400, easing = EaseInOutExpo)
|
||||||
val RevoltTweenFloat: FiniteAnimationSpec<Float> = tween(400, easing = EaseInOutExpo)
|
val RevoltTweenFloat: FiniteAnimationSpec<Float> = tween(400, easing = EaseInOutExpo)
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
|
@ -93,6 +96,8 @@ fun AppEntrypoint() {
|
||||||
|
|
||||||
composable("chat") { ChatRouterScreen(navController) }
|
composable("chat") { ChatRouterScreen(navController) }
|
||||||
|
|
||||||
|
composable("settings") { SettingsScreen(navController) }
|
||||||
|
|
||||||
composable("about") { AboutScreen(navController) }
|
composable("about") { AboutScreen(navController) }
|
||||||
composable("about/oss") { AttributionScreen(navController) }
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
composable("about/placeholder") { PlaceholderScreen(navController) }
|
composable("about/placeholder") { PlaceholderScreen(navController) }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package chat.revolt.api.realtime
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import chat.revolt.api.REVOLT_WEBSOCKET
|
import chat.revolt.api.REVOLT_WEBSOCKET
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
|
|
@ -118,9 +119,7 @@ object RealtimeSocket {
|
||||||
|
|
||||||
RevoltAPI.messageCache[messageFrame.id!!] = messageFrame
|
RevoltAPI.messageCache[messageFrame.id!!] = messageFrame
|
||||||
|
|
||||||
channelCallbacks[messageFrame.channel]?.forEach { callback ->
|
channelCallbacks[messageFrame.channel]?.onMessage(messageFrame)
|
||||||
callback.onMessage(messageFrame)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"ChannelStartTyping" -> {
|
"ChannelStartTyping" -> {
|
||||||
val typingFrame =
|
val typingFrame =
|
||||||
|
|
@ -130,9 +129,7 @@ object RealtimeSocket {
|
||||||
"Received channel start typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
"Received channel start typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
||||||
)
|
)
|
||||||
|
|
||||||
channelCallbacks[typingFrame.id]?.forEach { callback ->
|
channelCallbacks[typingFrame.id]?.onStartTyping(typingFrame)
|
||||||
callback.onStartTyping(typingFrame)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"ChannelStopTyping" -> {
|
"ChannelStopTyping" -> {
|
||||||
val typingFrame =
|
val typingFrame =
|
||||||
|
|
@ -142,9 +139,7 @@ object RealtimeSocket {
|
||||||
"Received channel stop typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
"Received channel stop typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
||||||
)
|
)
|
||||||
|
|
||||||
channelCallbacks[typingFrame.id]?.forEach { callback ->
|
channelCallbacks[typingFrame.id]?.onStopTyping(typingFrame)
|
||||||
callback.onStopTyping(typingFrame)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"UserUpdate" -> {
|
"UserUpdate" -> {
|
||||||
val userUpdateFrame =
|
val userUpdateFrame =
|
||||||
|
|
@ -178,18 +173,16 @@ object RealtimeSocket {
|
||||||
fun onMessage(message: MessageFrame)
|
fun onMessage(message: MessageFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val channelCallbacks: MutableMap<String, List<ChannelCallback>> = mutableStateMapOf()
|
private val channelCallbacks: SnapshotStateMap<String, ChannelCallback> = mutableStateMapOf()
|
||||||
|
|
||||||
fun registerChannelCallback(channelId: String, callback: ChannelCallback) {
|
fun registerChannelCallback(channelId: String, callback: ChannelCallback) {
|
||||||
val callbacks = channelCallbacks[channelId] ?: emptyList()
|
channelCallbacks[channelId] = callback
|
||||||
channelCallbacks[channelId] = callbacks + callback
|
|
||||||
|
|
||||||
Log.d("RealtimeSocket", "Registered channel callback for $channelId.")
|
Log.d("RealtimeSocket", "Registered channel callback for $channelId.")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterChannelCallback(channelId: String, callback: ChannelCallback) {
|
fun unregisterChannelCallback(channelId: String, callback: ChannelCallback) {
|
||||||
val callbacks = channelCallbacks[channelId] ?: emptyList()
|
channelCallbacks.remove(channelId, callback)
|
||||||
channelCallbacks[channelId] = callbacks - callback
|
|
||||||
|
|
||||||
Log.d("RealtimeSocket", "Unregistered channel callback for $channelId")
|
Log.d("RealtimeSocket", "Unregistered channel callback for $channelId")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package chat.revolt.components.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.BottomAppBar
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.RevoltTweenIntSize
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomNavigation(
|
||||||
|
navController: NavController,
|
||||||
|
show: Boolean,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = show,
|
||||||
|
enter = expandVertically(
|
||||||
|
animationSpec = RevoltTweenIntSize
|
||||||
|
),
|
||||||
|
exit = shrinkVertically(
|
||||||
|
animationSpec = RevoltTweenIntSize
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
BottomAppBar {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (navController.currentDestination?.route != "chat") {
|
||||||
|
navController.navigate("chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Home,
|
||||||
|
contentDescription = stringResource(id = R.string.home),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (navController.currentDestination?.route != "settings") {
|
||||||
|
navController.navigate("settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = stringResource(id = R.string.settings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package chat.revolt.screens.chat
|
package chat.revolt.screens.chat
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
|
@ -37,6 +36,7 @@ import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.schemas.ChannelType
|
import chat.revolt.api.schemas.ChannelType
|
||||||
import chat.revolt.components.chat.DisconnectedNotice
|
import chat.revolt.components.chat.DisconnectedNotice
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
import chat.revolt.components.screens.chat.BottomNavigation
|
||||||
import chat.revolt.components.screens.chat.DrawerChannel
|
import chat.revolt.components.screens.chat.DrawerChannel
|
||||||
import chat.revolt.screens.chat.views.ChannelScreen
|
import chat.revolt.screens.chat.views.ChannelScreen
|
||||||
import chat.revolt.screens.chat.views.HomeScreen
|
import chat.revolt.screens.chat.views.HomeScreen
|
||||||
|
|
@ -80,7 +80,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
Column() {
|
Column {
|
||||||
AnimatedVisibility(visible = RealtimeSocket.disconnectionState != DisconnectionState.Connected) {
|
AnimatedVisibility(visible = RealtimeSocket.disconnectionState != DisconnectionState.Connected) {
|
||||||
DisconnectedNotice(
|
DisconnectedNotice(
|
||||||
state = RealtimeSocket.disconnectionState,
|
state = RealtimeSocket.disconnectionState,
|
||||||
|
|
@ -174,7 +174,8 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
|
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
|
||||||
.forEach { channel ->
|
.forEach { channel ->
|
||||||
DrawerChannel(
|
DrawerChannel(
|
||||||
name = channel.name ?: "GDM #${channel.id}",
|
name = channel.name
|
||||||
|
?: "GDM #${channel.id}",
|
||||||
channelType = ChannelType.Group,
|
channelType = ChannelType.Group,
|
||||||
selected = channel.id == (navBackStackEntry?.arguments?.getString(
|
selected = channel.id == (navBackStackEntry?.arguments?.getString(
|
||||||
"channelId"
|
"channelId"
|
||||||
|
|
@ -192,7 +193,8 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
val server = RevoltAPI.serverCache[it]
|
val server = RevoltAPI.serverCache[it]
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = server?.name ?: stringResource(R.string.unknown),
|
text = server?.name
|
||||||
|
?: stringResource(R.string.unknown),
|
||||||
fontWeight = FontWeight.Black,
|
fontWeight = FontWeight.Black,
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
|
|
@ -245,5 +247,10 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BottomNavigation(
|
||||||
|
navController = topNav,
|
||||||
|
show = channelDrawerState.currentValue == DrawerValue.Open,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,10 +78,10 @@ fun GreeterScreen(navController: NavController) {
|
||||||
.padding(horizontal = 20.dp, vertical = 30.dp)
|
.padding(horizontal = 20.dp, vertical = 30.dp)
|
||||||
) {
|
) {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onClick = { navController.navigate("about") },
|
onClick = { navController.navigate("about/placeholder") },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.about))
|
Text(text = stringResource(R.string.signup))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.components.generic.PageHeader
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
PageHeader(stringResource(id = R.string.settings))
|
||||||
|
ElevatedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = R.string.back))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("about")
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = R.string.about))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -106,4 +106,5 @@
|
||||||
<string name="tap_to_retry">Tap to retry</string>
|
<string name="tap_to_retry">Tap to retry</string>
|
||||||
|
|
||||||
<string name="scroll_to_bottom">Scroll to bottom</string>
|
<string name="scroll_to_bottom">Scroll to bottom</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in New Issue