refactor: Implement bottom navigation bar

This commit introduces a bottom navigation bar to the chat interface.

Key changes include:
- Added a `Scaffold` with a `BottomAppBar` in `ChannelNavigator`.
- The `BottomAppBar` contains `NavigationBarItem`s for "Home", "Friends", and "You" (Overview).
- The "Home" destination now displays the `Sidebar`.
- Removed the tablet-aware UI logic and the dismissible navigation drawer implementation.
- Simplified `ChannelNavigator` parameters, removing `useDrawer`, `disableBackHandler`, `onEnterVoiceUI`.
- Updated screen composables (`FriendsScreen`, `NoCurrentChannelScreen`, `ChannelScreen`, `OverviewScreen`) to remove the `useDrawer` parameter and its associated logic, as the drawer is no longer directly managed by these screens.
- Removed `drawerState` parameter from `ChannelSideDrawer` and related components, as drawer state is now managed differently.
- Removed logic related to closing the drawer on item clicks within `ChannelSideDrawer`.
- Added a new `ChatRouterDestination.Home`.
- Request for `POST_NOTIFICATIONS` permission is now correctly using `Manifest.permission.POST_NOTIFICATIONS`.
This commit is contained in:
AbronStudio 2025-07-26 16:50:16 +03:30
parent 64f206fef7
commit 33296cbde1
6 changed files with 146 additions and 219 deletions

View File

@ -37,7 +37,6 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -99,7 +98,6 @@ import chat.revolt.composables.screens.chat.ChannelIcon
import chat.revolt.screens.chat.ChatRouterDestination
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.ChannelContextSheet
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
@ -108,7 +106,6 @@ fun ChannelSideDrawer(
currentDestination: ChatRouterDestination,
onDestinationChanged: (ChatRouterDestination) -> Unit,
onLongPressAvatar: () -> Unit,
drawerState: DrawerState?,
navigateToServer: (String) -> Unit,
onShowServerContextSheet: (String) -> Unit,
showSettingsIcon: Boolean,
@ -126,6 +123,7 @@ fun ChannelSideDrawer(
LaunchedEffect(currentDestination) {
if (currentDestination is ChatRouterDestination.Channel && currentServer != null) {
val channelIndex = categorisedChannels?.indexOfFirst {
when (it) {
is CategorisedChannelList.Channel -> it.channel.id == currentDestination.channelId
else -> false
@ -357,9 +355,6 @@ fun ChannelSideDrawer(
.clip(CircleShape)
.clickable {
serverInList.id?.let { srvId -> navigateToServer(srvId) }
scope.launch {
drawerState?.close()
}
}) {
val icon = serverInList.icon?.id?.let { iconId ->
"$REVOLT_FILES/icons/$iconId"
@ -439,9 +434,6 @@ fun ChannelSideDrawer(
.clip(CircleShape)
.clickable {
onOpenSettings()
scope.launch {
drawerState?.close()
}
}
.size(48.dp),
contentAlignment = Alignment.Center
@ -577,7 +569,6 @@ fun ChannelSideDrawer(
DirectMessagesChannelListRenderer(
currentDestination,
onDestinationChanged,
drawerState,
channelListState,
onOpenChannelContextSheet = { channelContextSheetTarget = it }
)
@ -586,7 +577,6 @@ fun ChannelSideDrawer(
categorisedChannels,
currentDestination,
onDestinationChanged,
drawerState,
channelListState,
onOpenChannelContextSheet = { channelContextSheetTarget = it },
serverId = currentServer
@ -600,11 +590,9 @@ fun ChannelSideDrawer(
fun ColumnScope.DirectMessagesChannelListRenderer(
currentDestination: ChatRouterDestination,
onDestinationChanged: (ChatRouterDestination) -> Unit,
drawerState: DrawerState?,
channelListState: LazyListState,
onOpenChannelContextSheet: (String) -> Unit,
) {
val scope = rememberCoroutineScope()
val dmAbleChannels =
RevoltAPI.channelCache.values
.filter { it.channelType == ChannelType.DirectMessage || it.channelType == ChannelType.Group }
@ -629,9 +617,6 @@ fun ColumnScope.DirectMessagesChannelListRenderer(
isCurrent = currentDestination is ChatRouterDestination.Overview,
onDestinationChanged = {
onDestinationChanged(ChatRouterDestination.Overview)
scope.launch {
drawerState?.close()
}
},
hasUnread = false,
onOpenChannelContextSheet = {}
@ -650,9 +635,6 @@ fun ColumnScope.DirectMessagesChannelListRenderer(
isCurrent = currentDestination is ChatRouterDestination.Friends,
onDestinationChanged = {
onDestinationChanged(ChatRouterDestination.Friends)
scope.launch {
drawerState?.close()
}
},
hasUnread = FriendRequests.getIncoming().isNotEmpty(),
onOpenChannelContextSheet = {},
@ -675,9 +657,6 @@ fun ColumnScope.DirectMessagesChannelListRenderer(
currentDestination.channelId == notesChannel.id,
onDestinationChanged = {
onDestinationChanged(it)
scope.launch {
drawerState?.close()
}
},
hasUnread = false,
onOpenChannelContextSheet = {},
@ -732,9 +711,6 @@ fun ColumnScope.DirectMessagesChannelListRenderer(
isMuted = NotificationSettingsProvider.isChannelMuted(channel.id!!, null),
onDestinationChanged = { dest ->
onDestinationChanged(dest)
scope.launch {
drawerState?.close()
}
},
onOpenChannelContextSheet = onOpenChannelContextSheet
)
@ -756,13 +732,10 @@ fun ColumnScope.ServerChannelListRenderer(
categorisedChannels: List<CategorisedChannelList>?,
currentDestination: ChatRouterDestination,
onDestinationChanged: (ChatRouterDestination) -> Unit,
drawerState: DrawerState?,
channelListState: LazyListState,
onOpenChannelContextSheet: (String) -> Unit,
serverId: String
) {
val scope = rememberCoroutineScope()
LazyColumn(
state = channelListState,
verticalArrangement = Arrangement.spacedBy(4.dp),
@ -806,11 +779,8 @@ fun ColumnScope.ServerChannelListRenderer(
else -> false
},
onDestinationChanged = {
onDestinationChanged(it)
scope.launch {
drawerState?.close()
}
onDestinationChanged = { dest ->
onDestinationChanged(dest)
},
hasUnread = channelOrCat.channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(

View File

@ -1,19 +1,15 @@
package chat.revolt.screens.chat
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.Log
import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
@ -23,15 +19,20 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDrawerState
@ -52,8 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
@ -79,7 +78,6 @@ import chat.revolt.composables.chat.DisconnectedNotice
import chat.revolt.composables.screens.chat.drawer.ChannelSideDrawer
import chat.revolt.dialogs.NotificationRationaleDialog
import chat.revolt.internals.Changelogs
import chat.revolt.internals.extensions.zero
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog
@ -111,6 +109,7 @@ import javax.inject.Inject
sealed class ChatRouterDestination {
data object Overview : ChatRouterDestination()
data object Friends : ChatRouterDestination()
data object Home : ChatRouterDestination()
data class Channel(val channelId: String) : ChatRouterDestination()
data class NoCurrentChannel(val serverId: String?) : ChatRouterDestination()
@ -120,6 +119,7 @@ sealed class ChatRouterDestination {
is Friends -> "friends"
is Channel -> "channel/$channelId"
is NoCurrentChannel -> "no_current_channel/$serverId"
ChatRouterDestination.Home -> "home"
}
}
@ -274,7 +274,6 @@ val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
fun ChatRouterScreen(
topNav: NavController,
windowSizeClass: WindowSizeClass,
disableBackHandler: Boolean,
onNullifiedUser: () -> Unit,
onEnterVoiceUI: (String) -> Unit,
viewModel: ChatRouterViewModel = hiltViewModel()
@ -284,8 +283,6 @@ fun ChatRouterScreen(
val context = LocalContext.current
val view = LocalView.current
var drawerWidth by remember { mutableFloatStateOf(0.0f) }
var showPlatformModDMHint by remember { mutableStateOf(false) }
var showStatusSheet by remember { mutableStateOf(false) }
@ -736,7 +733,7 @@ fun ChatRouterScreen(
onSelected = { accepted ->
if (accepted) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
askNotificationsPermission.launch(android.Manifest.permission.POST_NOTIFICATIONS)
askNotificationsPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
viewModel.setRegisterForNotifications()
}
@ -817,128 +814,26 @@ fun ChatRouterScreen(
CompositionLocalProvider(
LocalIsConnected provides (RealtimeSocket.disconnectionState == DisconnectionState.Connected)
) {
if (useTabletAwareUI) {
Row {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.zero
) {
Sidebar(
viewModel = viewModel,
topNav = topNav,
currentServer = currentServer,
onShowStatusSheet = {
ChannelNavigator(
dest = viewModel.currentDestination,
topNav = topNav,
toggleDrawer = {
toggleDrawerLambda()
},
onShowStatusSheet = {
showStatusSheet = true
},
onShowServerContextSheet = {
},
onShowServerContextSheet = {
serverContextSheetTarget = it
showServerContextSheet = true
},
onShowAddServerSheet = {
},
onShowAddServerSheet = {
showAddServerSheet = true
},
showSettingsButton = isTouchExplorationEnabled,
onOpenSettings = {
topNav.navigate("settings")
},
)
}
ChannelNavigator(
dest = viewModel.currentDestination,
topNav = topNav,
useDrawer = false,
disableBackHandler = disableBackHandler,
toggleDrawer = {
toggleDrawerLambda()
},
onEnterVoiceUI = onEnterVoiceUI,
)
}
} else {
var useSidebarGesture by remember { mutableStateOf(true) }
DismissibleNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = useSidebarGesture,
drawerContent = {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.zero,
modifier = Modifier.onSizeChanged {
drawerWidth = it.width.toFloat()
}
) {
Sidebar(
viewModel = viewModel,
topNav = topNav,
currentServer = currentServer,
onShowStatusSheet = {
showStatusSheet = true
},
onShowServerContextSheet = {
serverContextSheetTarget = it
showServerContextSheet = true
},
onShowAddServerSheet = {
showAddServerSheet = true
},
showSettingsButton = isTouchExplorationEnabled,
onOpenSettings = {
topNav.navigate("settings")
},
drawerState = drawerState
)
}
},
content = {
Box(Modifier.fillMaxSize()) {
ChannelNavigator(
dest = viewModel.currentDestination,
topNav = topNav,
useDrawer = true,
disableBackHandler = disableBackHandler,
toggleDrawer = {
toggleDrawerLambda()
},
drawerState = drawerState,
drawerGestureEnabled = useSidebarGesture,
setDrawerGestureEnabled = {
useSidebarGesture = it
},
onEnterVoiceUI = onEnterVoiceUI,
)
// This is the overlay on the main content when the drawer is open
val interactionSource = remember { MutableInteractionSource() }
Box(
Modifier
.then(
if (drawerState.isOpen) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
enabled = drawerState.isOpen,
onClick = {
scope.launch {
drawerState.close()
}
}
)
} else Modifier
)
.fillMaxSize()
.background(
MaterialTheme
.colorScheme
.surfaceContainerLowest
.copy(
alpha = (1.0f + (drawerState.currentOffset / drawerWidth)) * 0.7f
)
)
)
}
}
)
}
},
isTouchExplorationEnabled = isTouchExplorationEnabled,
viewModel = viewModel,
currentServer = currentServer,
)
}
}
}
@ -948,7 +843,6 @@ fun Sidebar(
viewModel: ChatRouterViewModel,
currentServer: String?,
topNav: NavController,
drawerState: DrawerState? = null,
onShowStatusSheet: () -> Unit,
onShowServerContextSheet: (String) -> Unit,
onShowAddServerSheet: () -> Unit,
@ -959,7 +853,6 @@ fun Sidebar(
onDestinationChanged = viewModel::setSaveDestination,
currentDestination = viewModel.currentDestination,
currentServer = currentServer,
drawerState = drawerState,
navigateToServer = viewModel::navigateToServer,
onLongPressAvatar = onShowStatusSheet,
onShowServerContextSheet = onShowServerContextSheet,
@ -974,59 +867,126 @@ fun Sidebar(
fun ChannelNavigator(
dest: ChatRouterDestination,
topNav: NavController,
useDrawer: Boolean,
viewModel: ChatRouterViewModel,
onShowStatusSheet: () -> Unit = {},
onShowAddServerSheet: () -> Unit = {},
onShowServerContextSheet: (String) -> Unit = {},
currentServer: String?,
toggleDrawer: () -> Unit,
isTouchExplorationEnabled: Boolean,
drawerState: DrawerState? = null,
drawerGestureEnabled: Boolean = true,
disableBackHandler: Boolean = false,
onEnterVoiceUI: (String) -> Unit = {},
setDrawerGestureEnabled: (Boolean) -> Unit = {},
) {
val scope = rememberCoroutineScope()
BackHandler(useDrawer && !disableBackHandler) {
toggleDrawer()
}
Column(Modifier.fillMaxSize()) {
when (dest) {
is ChatRouterDestination.Overview -> {
OverviewScreen(
navController = topNav,
useDrawer = useDrawer,
onDrawerClicked = toggleDrawer,
)
}
is ChatRouterDestination.Friends -> {
FriendsScreen(
topNav = topNav,
useDrawer = useDrawer,
onDrawerClicked = toggleDrawer,
)
}
is ChatRouterDestination.Channel -> {
ChannelScreen(
channelId = dest.channelId,
onToggleDrawer = {
scope.launch {
if (drawerState?.isOpen == true) {
drawerState.close()
} else {
drawerState?.open()
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
BottomAppBar {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Home,
contentDescription = "Home",
)
},
useDrawer = useDrawer,
drawerGestureEnabled = drawerGestureEnabled,
setDrawerGestureEnabled = setDrawerGestureEnabled,
drawerIsOpen = drawerState?.isOpen == true,
label = {
Text(text = "you")
},
selected = dest is ChatRouterDestination.Home,
enabled = true,
onClick = {
viewModel.setSaveDestination(ChatRouterDestination.Home)
}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Friends",
)
},
label = {
Text(text = "Friends")
},
selected = dest is ChatRouterDestination.Friends,
enabled = true,
onClick = {
viewModel.setSaveDestination(ChatRouterDestination.Friends)
}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Face,
contentDescription = "You",
)
},
label = {
Text(text = "you")
},
selected = dest is ChatRouterDestination.Overview,
enabled = true,
onClick = {
viewModel.setSaveDestination(ChatRouterDestination.Overview)
}
)
}
}
) { innerPadding ->
Column {
when (dest) {
is ChatRouterDestination.Overview -> {
OverviewScreen(
navController = topNav,
onDrawerClicked = toggleDrawer,
)
}
is ChatRouterDestination.NoCurrentChannel -> {
NoCurrentChannelScreen(useDrawer = useDrawer, onDrawerClicked = toggleDrawer)
is ChatRouterDestination.Friends -> {
FriendsScreen(
topNav = topNav,
onDrawerClicked = toggleDrawer,
)
}
is ChatRouterDestination.Home -> {
Row {
Sidebar(
viewModel = viewModel,
topNav = topNav,
currentServer = currentServer,
onShowStatusSheet = onShowStatusSheet,
onShowServerContextSheet = onShowServerContextSheet,
onShowAddServerSheet = onShowAddServerSheet,
showSettingsButton = isTouchExplorationEnabled,
onOpenSettings = {
topNav.navigate("settings")
},
)
}
}
is ChatRouterDestination.Channel -> {
ChannelScreen(
channelId = dest.channelId,
onToggleDrawer = {
scope.launch {
if (drawerState?.isOpen == true) {
drawerState.close()
} else {
drawerState?.open()
}
}
},
setDrawerGestureEnabled = setDrawerGestureEnabled,
drawerIsOpen = drawerState?.isOpen == true,
)
}
is ChatRouterDestination.NoCurrentChannel -> {
NoCurrentChannelScreen(onDrawerClicked = toggleDrawer)
}
}
}
}

View File

@ -120,7 +120,7 @@ private fun showInvalidClipboardToast(context: Context) {
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: () -> Unit) {
fun FriendsScreen(topNav: NavController, useDrawer: Boolean = false, onDrawerClicked: () -> Unit) {
val clipboard = LocalClipboard.current
val context = LocalContext.current
var overflowMenuShown by remember { mutableStateOf(false) }

View File

@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -33,7 +31,7 @@ import chat.revolt.screens.chat.LocalIsConnected
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NoCurrentChannelScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
fun NoCurrentChannelScreen(useDrawer: Boolean = false, onDrawerClicked: () -> Unit) {
Scaffold(
topBar = {
Column {

View File

@ -68,7 +68,7 @@ import io.sentry.Sentry
@Composable
fun OverviewScreen(
navController: NavController,
useDrawer: Boolean,
useDrawer: Boolean = false,
onDrawerClicked: () -> Unit,
includePadding: Boolean = true
) {

View File

@ -190,9 +190,8 @@ private const val NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD = 500
fun ChannelScreen(
channelId: String,
onToggleDrawer: () -> Unit,
useDrawer: Boolean,
useDrawer: Boolean = false,
useBackButton: Boolean = false,
drawerGestureEnabled: Boolean = true,
setDrawerGestureEnabled: (Boolean) -> Unit = {},
drawerIsOpen: Boolean = false,
backButtonAction: (() -> Unit)? = null,
@ -347,7 +346,7 @@ fun ChannelScreen(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
} catch (e: Exception) {
} catch (_: Exception) {
Toast.makeText(
context,
context.getString(
@ -363,7 +362,7 @@ fun ChannelScreen(
capturedPhotoUri.value?.let { uri ->
pickCameraLauncher.launch(uri)
}
} catch (e: Exception) {
} catch (_: Exception) {
Toast.makeText(
context,
context.getString(