feat: full edge to edge in chat router

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-05-29 21:32:21 +02:00
parent ade9e03a9d
commit 2b752fefbc
8 changed files with 508 additions and 362 deletions

View File

@ -53,18 +53,20 @@ import kotlinx.serialization.json.Json
import java.net.SocketException
import chat.revolt.api.schemas.Channel as ChannelSchema
private const val USE_ALPHA_API = false
val REVOLT_BASE =
if (BuildConfig.USE_ALPHA_API) "https://alpha.revolt.chat/api" else "https://api.revolt.chat/0.8"
if (USE_ALPHA_API) "https://alpha.revolt.chat/api" else "https://api.revolt.chat/0.8"
const val REVOLT_SUPPORT = "https://support.revolt.chat"
const val REVOLT_MARKETING = "https://revolt.chat"
val REVOLT_FILES =
if (BuildConfig.USE_ALPHA_API) "https://alpha.revolt.chat/autumn" else "https://cdn.revoltusercontent.com"
if (USE_ALPHA_API) "https://alpha.revolt.chat/autumn" else "https://cdn.revoltusercontent.com"
val REVOLT_JANUARY =
if (BuildConfig.USE_ALPHA_API) "https://alpha.revolt.chat/january" else "https://jan.revolt.chat"
if (USE_ALPHA_API) "https://alpha.revolt.chat/january" else "https://jan.revolt.chat"
const val REVOLT_APP = "https://app.revolt.chat"
const val REVOLT_INVITES = "https://rvlt.gg"
val REVOLT_WEBSOCKET =
if (BuildConfig.USE_ALPHA_API) "wss://alpha.revolt.chat/ws" else "wss://ws.revolt.chat"
if (USE_ALPHA_API) "wss://alpha.revolt.chat/ws" else "wss://ws.revolt.chat"
const val REVOLT_KJBOOK = "https://revoltchat.github.io/android"
fun String.api(): String {
@ -72,7 +74,9 @@ fun String.api(): String {
}
fun buildUserAgent(accessMethod: String = "Ktor"): String {
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} ${BuildConfig.APPLICATION_ID} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE}; (Kotlin ${KotlinVersion.CURRENT})"
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} " +
"${BuildConfig.APPLICATION_ID} Android/${android.os.Build.VERSION.SDK_INT} " +
"(${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE}) Kotlin/${KotlinVersion.CURRENT}"
}
private const val BACKEND_IS_STABLE = false

View File

@ -44,7 +44,7 @@ data class ReadyFrame(
val servers: List<Server>,
val channels: List<Channel>,
val emojis: List<Emoji>,
@SerialName("voice_states") val voiceStates: List<ChannelVoiceState>,
@SerialName("voice_states") val voiceStates: List<ChannelVoiceState> = listOf(),
)
typealias MessageFrame = Message

View File

@ -1,7 +1,9 @@
package chat.revolt.composables.screens.chat.drawer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@ -28,6 +30,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@ -71,6 +74,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@ -96,6 +100,7 @@ import chat.revolt.composables.generic.UserAvatar
import chat.revolt.composables.generic.presenceFromStatus
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
@ -154,6 +159,17 @@ fun ChannelSideDrawer(
), label = "Server banner height"
)
val serverInfoOffset by animateDpAsState(
if (LocalIsConnected.current)
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
else
0.dp,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Dp.VisibilityThreshold
)
)
// - Take the list of servers and filter them by the ones that are in the ordering.
// - Sort the servers that are in the ordering using the ordering.
// - Add the servers that aren't in the ordering to the end of the list.
@ -202,30 +218,40 @@ fun ChannelSideDrawer(
)
) {
stickyHeader(key = "self") {
UserAvatar(
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
User.resolveDefaultName(
it
Column(Modifier.background(MaterialTheme.colorScheme.background)) {
AnimatedVisibility(LocalIsConnected.current) {
Spacer(
Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
)
}
?: "",
presence = presenceFromStatus(
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
),
userId = RevoltAPI.selfId ?: "",
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
onDestinationChanged(ChatRouterDestination.defaultForDMList)
},
onLongClick = onLongPressAvatar,
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(8.dp)
.size(48.dp)
)
UserAvatar(
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
User.resolveDefaultName(
it
)
}
?: "",
presence = presenceFromStatus(
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
),
userId = RevoltAPI.selfId ?: "",
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
onDestinationChanged(ChatRouterDestination.defaultForDMList)
},
onLongClick = onLongPressAvatar,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
}
}
items(
@ -314,12 +340,12 @@ fun ChannelSideDrawer(
)
val leftIndicatorColour = animateColorAsState(
targetValue =
if (serverInList.id == currentServer)
MaterialTheme.colorScheme.primary
else if (serverHasUnread)
MaterialTheme.colorScheme.onSurfaceVariant
else
Color.Transparent,
if (serverInList.id == currentServer)
MaterialTheme.colorScheme.primary
else if (serverHasUnread)
MaterialTheme.colorScheme.onSurfaceVariant
else
Color.Transparent,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
@ -433,27 +459,30 @@ fun ChannelSideDrawer(
}
Column(
Modifier
.clip(
MaterialTheme.shapes.extraLarge.copy(
bottomEnd = CornerSize(0)
)
)
.background(MaterialTheme.colorScheme.surfaceContainer)
.weight(1f)
.fillMaxHeight()
) {
Box(Modifier.height(serverBannerHeight)) {
Box(
Modifier
.clip(
MaterialTheme.shapes.medium.copy(
topStart = CornerSize(0.dp),
topEnd = CornerSize(0.dp)
)
)
.height(
serverBannerHeight + WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
//.offset(y = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
if (server?.banner != null) {
RemoteImage(
url = "$REVOLT_FILES/banners/${server.banner.id}",
description = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(
MaterialTheme.shapes.medium.copy(
topStart = CornerSize(0), topEnd = CornerSize(0)
)
)
.fillMaxSize()
)
@ -465,7 +494,7 @@ fun ChannelSideDrawer(
drawRect(
Brush.linearGradient(
listOf(
surfaceContainer.copy(alpha = 0.8f),
Color.Black.copy(alpha = 0.6f),
Color.Transparent
),
Offset.Zero,
@ -477,63 +506,72 @@ fun ChannelSideDrawer(
}
Row(
Modifier.padding(16.dp),
Modifier
.padding(16.dp)
.offset(y = serverInfoOffset),
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
CompositionLocalProvider(
LocalContentColor provides
if (server?.banner != null) Color.White
else LocalContentColor.current
) {
if (server?.flags has ServerFlags.Official) {
Icon(
painter = painterResource(
id = R.drawable.ic_revolt_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_official
),
tint = LocalContentColor.current,
modifier = Modifier
.size(24.dp)
)
}
if (server?.flags has ServerFlags.Verified) {
Icon(
painter = painterResource(
id = R.drawable.ic_check_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_verified
),
tint = LocalContentColor.current,
modifier = Modifier
.size(24.dp)
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (server?.flags has ServerFlags.Official) {
Icon(
painter = painterResource(
id = R.drawable.ic_revolt_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_official
),
tint = LocalContentColor.current,
modifier = Modifier
.size(24.dp)
)
}
if (server?.flags has ServerFlags.Verified) {
Icon(
painter = painterResource(
id = R.drawable.ic_check_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_verified
),
tint = LocalContentColor.current,
modifier = Modifier
.size(24.dp)
)
}
Text(
text = when (currentServer) {
null -> stringResource(R.string.direct_messages)
else -> server?.name ?: stringResource(R.string.unknown)
},
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = when (currentServer) {
null -> stringResource(R.string.direct_messages)
else -> server?.name ?: stringResource(R.string.unknown)
},
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (currentServer != null) {
IconButton(onClick = {
server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.menu)
)
if (currentServer != null) {
IconButton(onClick = {
server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.menu),
tint = LocalContentColor.current
)
}
} else {
Spacer(Modifier.height(64.dp))
}
} else {
Spacer(Modifier.height(64.dp))
}
}
}
@ -762,7 +800,8 @@ fun ColumnScope.ServerChannelListRenderer(
items(categorisedChannels?.size ?: 0) {
when (val channelOrCat = categorisedChannels?.get(it)) {
is CategorisedChannelList.Channel -> {
ChannelItem(channel = channelOrCat.channel,
ChannelItem(
channel = channelOrCat.channel,
isCurrent = when (currentDestination) {
is ChatRouterDestination.Channel -> {
currentDestination.channelId == channelOrCat.channel.id
@ -840,7 +879,8 @@ fun ChannelItem(
}
}
) {
Row(verticalAlignment = Alignment.CenterVertically,
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
modifier = Modifier
.padding(start = 8.dp, end = 8.dp)

View File

@ -10,15 +10,17 @@ 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.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog
@ -38,15 +40,20 @@ import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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
@ -261,6 +268,8 @@ class ChatRouterViewModel @Inject constructor(
}
}
val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatRouterScreen(
@ -276,6 +285,8 @@ 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) }
@ -814,55 +825,11 @@ fun ChatRouterScreen(
)
}
AnimatedVisibility(
visible = RealtimeSocket.disconnectionState == DisconnectionState.Connected
CompositionLocalProvider(
LocalIsConnected provides (RealtimeSocket.disconnectionState == DisconnectionState.Connected)
) {
Spacer(Modifier.windowInsetsPadding(WindowInsets.statusBars))
}
if (useTabletAwareUI) {
Row {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.zero
) {
Sidebar(
viewModel = viewModel,
topNav = topNav,
currentServer = currentServer,
onShowStatusSheet = {
showStatusSheet = true
},
onShowServerContextSheet = {
serverContextSheetTarget = it
showServerContextSheet = true
},
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 = {
if (useTabletAwareUI) {
Row {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.zero
@ -885,30 +852,104 @@ fun ChatRouterScreen(
onOpenSettings = {
topNav.navigate("settings")
},
drawerState = drawerState
)
}
},
content = {
Row(Modifier.fillMaxSize()) {
ChannelNavigator(
dest = viewModel.currentDestination,
topNav = topNav,
useDrawer = true,
disableBackHandler = disableBackHandler,
toggleDrawer = {
toggleDrawerLambda()
},
drawerState = drawerState,
drawerGestureEnabled = useSidebarGesture,
setDrawerGestureEnabled = {
useSidebarGesture = it
},
onEnterVoiceUI = onEnterVoiceUI,
)
}
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
)
)
)
}
}
)
}
}
}
}

View File

@ -1,11 +1,16 @@
package chat.revolt.screens.chat.views
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
@ -37,6 +42,7 @@ import chat.revolt.callbacks.ActionChannel
import chat.revolt.composables.chat.MemberListItem
import chat.revolt.composables.generic.CountableListHeader
import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -48,69 +54,80 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.friends),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
Column {
AnimatedVisibility(LocalIsConnected.current) {
Spacer(
Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
)
},
navigationIcon = {
if (useDrawer) {
}
TopAppBar(
title = {
Text(
text = stringResource(R.string.friends),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
if (useDrawer) {
IconButton(onClick = {
onDrawerClicked()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
)
}
}
},
actions = {
IconButton(onClick = {
onDrawerClicked()
topNav.navigate("create/group")
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
painter = painterResource(R.drawable.ic_account_multiple_plus_24dp),
contentDescription = stringResource(R.string.frends_new_group)
)
}
}
},
actions = {
IconButton(onClick = {
topNav.navigate("create/group")
}) {
Icon(
painter = painterResource(R.drawable.ic_account_multiple_plus_24dp),
contentDescription = stringResource(R.string.frends_new_group)
)
}
IconButton(onClick = {
overflowMenuShown = true
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.menu)
)
}
DropdownMenu(
expanded = overflowMenuShown,
onDismissRequest = {
overflowMenuShown = false
IconButton(onClick = {
overflowMenuShown = true
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.menu)
)
}
) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.friends_deny_all_incoming))
},
onClick = {
scope.launch {
overflowMenuShown = false
}
with(Dispatchers.IO) {
DropdownMenu(
expanded = overflowMenuShown,
onDismissRequest = {
overflowMenuShown = false
}
) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.friends_deny_all_incoming))
},
onClick = {
scope.launch {
FriendRequests.getIncoming()
.forEach { it.id?.let { id -> unfriendUser(id) } }
overflowMenuShown = false
}
with(Dispatchers.IO) {
scope.launch {
FriendRequests.getIncoming()
.forEach { it.id?.let { id -> unfriendUser(id) } }
}
}
}
}
)
}
},
windowInsets = WindowInsets.zero
)
)
}
},
windowInsets = WindowInsets.zero
)
}
},
) { pv ->
Column(
@ -234,7 +251,7 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
items(FriendRequests.getBlocked().size) {
val item = FriendRequests.getBlocked().getOrNull(it)
if (item == null) return@items
MemberListItem(
member = null,
user = item,

View File

@ -1,10 +1,15 @@
package chat.revolt.screens.chat.views
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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
@ -23,28 +28,40 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.revolt.R
import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NoCurrentChannelScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
Scaffold(
topBar = {
TopAppBar(
title = {},
navigationIcon = {
if (useDrawer) {
IconButton(onClick = {
onDrawerClicked()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
Column {
AnimatedVisibility(LocalIsConnected.current) {
Spacer(
Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
)
}
TopAppBar(
title = {},
navigationIcon = {
if (useDrawer) {
IconButton(onClick = {
onDrawerClicked()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
)
}
}
}
},
windowInsets = WindowInsets.zero
)
},
windowInsets = WindowInsets.zero
)
}
},
) { pv ->
Column(

View File

@ -13,11 +13,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.material.icons.Icons
@ -62,6 +64,7 @@ import chat.revolt.composables.generic.NonIdealState
import chat.revolt.composables.screens.settings.UserOverview
import chat.revolt.composables.skeletons.UserOverviewSkeleton
import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.UserCardSheet
import io.sentry.Sentry
@ -110,22 +113,33 @@ fun OverviewScreen(
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text(stringResource(R.string.overview_screen_title)) },
navigationIcon = {
if (useDrawer) {
IconButton(onClick = {
onDrawerClicked()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
Column {
AnimatedVisibility(LocalIsConnected.current) {
Spacer(
Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
)
}
CenterAlignedTopAppBar(
title = { Text(stringResource(R.string.overview_screen_title)) },
navigationIcon = {
if (useDrawer) {
IconButton(onClick = {
onDrawerClicked()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
)
}
}
}
},
windowInsets = WindowInsets.zero
)
},
windowInsets = WindowInsets.zero
)
}
},
contentWindowInsets = if (includePadding) ScaffoldDefaults.contentWindowInsets else ScaffoldDefaults.contentWindowInsets.exclude(
NavigationBarDefaults.windowInsets

View File

@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -145,6 +146,7 @@ import chat.revolt.composables.skeletons.MessageSkeleton
import chat.revolt.composables.skeletons.MessageSkeletonVariant
import chat.revolt.internals.extensions.rememberChannelPermissions
import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.ChannelInfoSheet
import chat.revolt.sheets.MessageContextSheet
import chat.revolt.sheets.ReactSheet
@ -513,126 +515,137 @@ fun ChannelScreen(
Scaffold(
contentWindowInsets = WindowInsets.zero,
topBar = {
TopAppBar(
modifier = Modifier.clickable {
channelInfoSheetShown = true
},
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
viewModel.channel?.let {
when (it.channelType) {
ChannelType.DirectMessage -> {
Column {
AnimatedVisibility(LocalIsConnected.current) {
Spacer(
Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
)
}
TopAppBar(
modifier = Modifier.clickable {
channelInfoSheetShown = true
},
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
viewModel.channel?.let {
when (it.channelType) {
ChannelType.DirectMessage -> {
val partner =
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
UserAvatar(
username = it.name ?: stringResource(R.string.unknown),
userId = ChannelUtils.resolveDMPartner(it) ?: "",
size = 24.dp,
presenceSize = 12.dp,
avatar = partner?.avatar
)
}
ChannelType.Group -> {
GroupIcon(
name = it.name ?: stringResource(R.string.unknown),
size = 24.dp,
icon = it.icon
)
}
else -> {
ChannelIcon(
channelType = it.channelType ?: ChannelType.TextChannel,
modifier = Modifier
.size(24.dp)
.alpha(0.8f)
)
}
}
CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy(
fontSize = 20.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Bottom,
trim = LineHeightStyle.Trim.LastLineBottom
)
)
) {
when (it.channelType) {
ChannelType.TextChannel, ChannelType.VoiceChannel, ChannelType.Group -> Text(
it.name ?: stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ChannelType.SavedMessages -> Text(
stringResource(R.string.channel_notes),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ChannelType.DirectMessage -> Text(
ChannelUtils.resolveName(it)
?: stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
else -> Text(
stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (it.channelType == ChannelType.DirectMessage) {
val partner =
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
UserAvatar(
username = it.name ?: stringResource(R.string.unknown),
userId = ChannelUtils.resolveDMPartner(it) ?: "",
size = 24.dp,
presenceSize = 12.dp,
avatar = partner?.avatar
PresenceBadge(
presence = presenceFromStatus(
partner?.status?.presence,
online = partner?.online == true
),
size = 12.dp
)
}
ChannelType.Group -> {
GroupIcon(
name = it.name ?: stringResource(R.string.unknown),
size = 24.dp,
icon = it.icon
)
}
else -> {
ChannelIcon(
channelType = it.channelType ?: ChannelType.TextChannel,
modifier = Modifier
.size(24.dp)
.alpha(0.8f)
)
}
}
CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy(
fontSize = 20.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Bottom,
trim = LineHeightStyle.Trim.LastLineBottom
)
)
) {
when (it.channelType) {
ChannelType.TextChannel, ChannelType.VoiceChannel, ChannelType.Group -> Text(
it.name ?: stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ChannelType.SavedMessages -> Text(
stringResource(R.string.channel_notes),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ChannelType.DirectMessage -> Text(
ChannelUtils.resolveName(it)
?: stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
else -> Text(
stringResource(R.string.unknown),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (it.channelType == ChannelType.DirectMessage) {
val partner =
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
PresenceBadge(
presence = presenceFromStatus(
partner?.status?.presence,
online = partner?.online == true
),
size = 12.dp
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.alpha(0.5f)
)
}
}
},
windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero,
navigationIcon = {
if (useDrawer) {
IconButton(onClick = onToggleDrawer) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
)
}
}
if (useBackButton) {
IconButton(onClick = backButtonAction ?: {}) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.alpha(0.5f)
)
}
}
},
windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero,
navigationIcon = {
if (useDrawer) {
IconButton(onClick = onToggleDrawer) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
)
}
}
if (useBackButton) {
IconButton(onClick = backButtonAction ?: {}) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
}
}
)
)
}
}
) { pv ->
Crossfade(