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 java.net.SocketException
import chat.revolt.api.schemas.Channel as ChannelSchema import chat.revolt.api.schemas.Channel as ChannelSchema
private const val USE_ALPHA_API = false
val REVOLT_BASE = 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_SUPPORT = "https://support.revolt.chat"
const val REVOLT_MARKETING = "https://revolt.chat" const val REVOLT_MARKETING = "https://revolt.chat"
val REVOLT_FILES = 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 = 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_APP = "https://app.revolt.chat"
const val REVOLT_INVITES = "https://rvlt.gg" const val REVOLT_INVITES = "https://rvlt.gg"
val REVOLT_WEBSOCKET = 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" const val REVOLT_KJBOOK = "https://revoltchat.github.io/android"
fun String.api(): String { fun String.api(): String {
@ -72,7 +74,9 @@ fun String.api(): String {
} }
fun buildUserAgent(accessMethod: String = "Ktor"): 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 private const val BACKEND_IS_STABLE = false

View File

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

View File

@ -1,7 +1,9 @@
package chat.revolt.composables.screens.chat.drawer package chat.revolt.composables.screens.chat.drawer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring 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.padding
import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -96,6 +100,7 @@ import chat.revolt.composables.generic.UserAvatar
import chat.revolt.composables.generic.presenceFromStatus import chat.revolt.composables.generic.presenceFromStatus
import chat.revolt.composables.screens.chat.ChannelIcon import chat.revolt.composables.screens.chat.ChannelIcon
import chat.revolt.screens.chat.ChatRouterDestination import chat.revolt.screens.chat.ChatRouterDestination
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.ChannelContextSheet import chat.revolt.sheets.ChannelContextSheet
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -154,6 +159,17 @@ fun ChannelSideDrawer(
), label = "Server banner height" ), 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. // - 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. // - 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. // - Add the servers that aren't in the ordering to the end of the list.
@ -202,30 +218,40 @@ fun ChannelSideDrawer(
) )
) { ) {
stickyHeader(key = "self") { stickyHeader(key = "self") {
UserAvatar( Column(Modifier.background(MaterialTheme.colorScheme.background)) {
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let { AnimatedVisibility(LocalIsConnected.current) {
User.resolveDefaultName( Spacer(
it Modifier
.height(
WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
) )
} }
?: "", UserAvatar(
presence = presenceFromStatus( username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence, User.resolveDefaultName(
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false it
), )
userId = RevoltAPI.selfId ?: "", }
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar, ?: "",
size = 48.dp, presence = presenceFromStatus(
presenceSize = 16.dp, RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
onClick = { RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
onDestinationChanged(ChatRouterDestination.defaultForDMList) ),
}, userId = RevoltAPI.selfId ?: "",
onLongClick = onLongPressAvatar, avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
modifier = Modifier size = 48.dp,
.background(MaterialTheme.colorScheme.background) presenceSize = 16.dp,
.padding(8.dp) onClick = {
.size(48.dp) onDestinationChanged(ChatRouterDestination.defaultForDMList)
) },
onLongClick = onLongPressAvatar,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
}
} }
items( items(
@ -314,12 +340,12 @@ fun ChannelSideDrawer(
) )
val leftIndicatorColour = animateColorAsState( val leftIndicatorColour = animateColorAsState(
targetValue = targetValue =
if (serverInList.id == currentServer) if (serverInList.id == currentServer)
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
else if (serverHasUnread) else if (serverHasUnread)
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
else else
Color.Transparent, Color.Transparent,
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow stiffness = Spring.StiffnessLow
@ -433,27 +459,30 @@ fun ChannelSideDrawer(
} }
Column( Column(
Modifier Modifier
.clip(
MaterialTheme.shapes.extraLarge.copy(
bottomEnd = CornerSize(0)
)
)
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
.weight(1f) .weight(1f)
.fillMaxHeight() .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) { if (server?.banner != null) {
RemoteImage( RemoteImage(
url = "$REVOLT_FILES/banners/${server.banner.id}", url = "$REVOLT_FILES/banners/${server.banner.id}",
description = null, description = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.clip(
MaterialTheme.shapes.medium.copy(
topStart = CornerSize(0), topEnd = CornerSize(0)
)
)
.fillMaxSize() .fillMaxSize()
) )
@ -465,7 +494,7 @@ fun ChannelSideDrawer(
drawRect( drawRect(
Brush.linearGradient( Brush.linearGradient(
listOf( listOf(
surfaceContainer.copy(alpha = 0.8f), Color.Black.copy(alpha = 0.6f),
Color.Transparent Color.Transparent
), ),
Offset.Zero, Offset.Zero,
@ -477,63 +506,72 @@ fun ChannelSideDrawer(
} }
Row( Row(
Modifier.padding(16.dp), Modifier
.padding(16.dp)
.offset(y = serverInfoOffset),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( CompositionLocalProvider(
Modifier.weight(1f), LocalContentColor provides
verticalAlignment = Alignment.CenterVertically, if (server?.banner != null) Color.White
horizontalArrangement = Arrangement.spacedBy(8.dp) else LocalContentColor.current
) { ) {
if (server?.flags has ServerFlags.Official) { Row(
Icon( Modifier.weight(1f),
painter = painterResource( verticalAlignment = Alignment.CenterVertically,
id = R.drawable.ic_revolt_decagram_24dp horizontalArrangement = Arrangement.spacedBy(8.dp)
), ) {
contentDescription = stringResource( if (server?.flags has ServerFlags.Official) {
R.string.server_flag_official Icon(
), painter = painterResource(
tint = LocalContentColor.current, id = R.drawable.ic_revolt_decagram_24dp
modifier = Modifier ),
.size(24.dp) contentDescription = stringResource(
) R.string.server_flag_official
} ),
if (server?.flags has ServerFlags.Verified) { tint = LocalContentColor.current,
Icon( modifier = Modifier
painter = painterResource( .size(24.dp)
id = R.drawable.ic_check_decagram_24dp )
), }
contentDescription = stringResource( if (server?.flags has ServerFlags.Verified) {
R.string.server_flag_verified Icon(
), painter = painterResource(
tint = LocalContentColor.current, id = R.drawable.ic_check_decagram_24dp
modifier = Modifier ),
.size(24.dp) 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( if (currentServer != null) {
text = when (currentServer) { IconButton(onClick = {
null -> stringResource(R.string.direct_messages) server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
else -> server?.name ?: stringResource(R.string.unknown) }) {
}, Icon(
style = MaterialTheme.typography.titleMedium, imageVector = Icons.Default.MoreVert,
maxLines = 1, contentDescription = stringResource(R.string.menu),
overflow = TextOverflow.Ellipsis tint = LocalContentColor.current
) )
} }
} else {
if (currentServer != null) { Spacer(Modifier.height(64.dp))
IconButton(onClick = {
server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.menu)
)
} }
} else {
Spacer(Modifier.height(64.dp))
} }
} }
} }
@ -762,7 +800,8 @@ fun ColumnScope.ServerChannelListRenderer(
items(categorisedChannels?.size ?: 0) { items(categorisedChannels?.size ?: 0) {
when (val channelOrCat = categorisedChannels?.get(it)) { when (val channelOrCat = categorisedChannels?.get(it)) {
is CategorisedChannelList.Channel -> { is CategorisedChannelList.Channel -> {
ChannelItem(channel = channelOrCat.channel, ChannelItem(
channel = channelOrCat.channel,
isCurrent = when (currentDestination) { isCurrent = when (currentDestination) {
is ChatRouterDestination.Channel -> { is ChatRouterDestination.Channel -> {
currentDestination.channelId == channelOrCat.channel.id 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), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp, end = 8.dp) .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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog 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.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -261,6 +268,8 @@ class ChatRouterViewModel @Inject constructor(
} }
} }
val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChatRouterScreen( fun ChatRouterScreen(
@ -276,6 +285,8 @@ fun ChatRouterScreen(
val context = LocalContext.current val context = LocalContext.current
val view = LocalView.current val view = LocalView.current
var drawerWidth by remember { mutableFloatStateOf(0.0f) }
var showPlatformModDMHint by remember { mutableStateOf(false) } var showPlatformModDMHint by remember { mutableStateOf(false) }
var showStatusSheet by remember { mutableStateOf(false) } var showStatusSheet by remember { mutableStateOf(false) }
@ -814,55 +825,11 @@ fun ChatRouterScreen(
) )
} }
AnimatedVisibility( CompositionLocalProvider(
visible = RealtimeSocket.disconnectionState == DisconnectionState.Connected LocalIsConnected provides (RealtimeSocket.disconnectionState == DisconnectionState.Connected)
) { ) {
Spacer(Modifier.windowInsetsPadding(WindowInsets.statusBars)) if (useTabletAwareUI) {
} Row {
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 = {
DismissibleDrawerSheet( DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent, drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.zero windowInsets = WindowInsets.zero
@ -885,30 +852,104 @@ fun ChatRouterScreen(
onOpenSettings = { onOpenSettings = {
topNav.navigate("settings") 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 package chat.revolt.screens.chat.views
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu 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.chat.MemberListItem
import chat.revolt.composables.generic.CountableListHeader import chat.revolt.composables.generic.CountableListHeader
import chat.revolt.internals.extensions.zero import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -48,69 +54,80 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( Column {
title = { AnimatedVisibility(LocalIsConnected.current) {
Text( Spacer(
text = stringResource(R.string.friends), Modifier
maxLines = 1, .height(
overflow = TextOverflow.Ellipsis, WindowInsets.statusBars.asPaddingValues()
.calculateTopPadding()
)
) )
}, }
navigationIcon = { TopAppBar(
if (useDrawer) { 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 = { IconButton(onClick = {
onDrawerClicked() topNav.navigate("create/group")
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Menu, painter = painterResource(R.drawable.ic_account_multiple_plus_24dp),
contentDescription = stringResource(id = R.string.menu) contentDescription = stringResource(R.string.frends_new_group)
) )
} }
} IconButton(onClick = {
}, overflowMenuShown = true
actions = { }) {
IconButton(onClick = { Icon(
topNav.navigate("create/group") imageVector = Icons.Default.MoreVert,
}) { contentDescription = stringResource(R.string.menu)
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
} }
) { DropdownMenu(
DropdownMenuItem( expanded = overflowMenuShown,
text = { onDismissRequest = {
Text(stringResource(R.string.friends_deny_all_incoming)) overflowMenuShown = false
}, }
onClick = { ) {
scope.launch { DropdownMenuItem(
overflowMenuShown = false text = {
} Text(stringResource(R.string.friends_deny_all_incoming))
with(Dispatchers.IO) { },
onClick = {
scope.launch { scope.launch {
FriendRequests.getIncoming() overflowMenuShown = false
.forEach { it.id?.let { id -> unfriendUser(id) } } }
with(Dispatchers.IO) {
scope.launch {
FriendRequests.getIncoming()
.forEach { it.id?.let { id -> unfriendUser(id) } }
}
} }
} }
} )
) }
} },
}, windowInsets = WindowInsets.zero
windowInsets = WindowInsets.zero )
) }
}, },
) { pv -> ) { pv ->
Column( Column(

View File

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

View File

@ -13,11 +13,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.material.icons.Icons 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.screens.settings.UserOverview
import chat.revolt.composables.skeletons.UserOverviewSkeleton import chat.revolt.composables.skeletons.UserOverviewSkeleton
import chat.revolt.internals.extensions.zero import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.UserCardSheet import chat.revolt.sheets.UserCardSheet
import io.sentry.Sentry import io.sentry.Sentry
@ -110,22 +113,33 @@ fun OverviewScreen(
Scaffold( Scaffold(
topBar = { topBar = {
CenterAlignedTopAppBar( Column {
title = { Text(stringResource(R.string.overview_screen_title)) }, AnimatedVisibility(LocalIsConnected.current) {
navigationIcon = { Spacer(
if (useDrawer) { Modifier
IconButton(onClick = { .height(
onDrawerClicked() WindowInsets.statusBars.asPaddingValues()
}) { .calculateTopPadding()
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu)
) )
)
}
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( contentWindowInsets = if (includePadding) ScaffoldDefaults.contentWindowInsets else ScaffoldDefaults.contentWindowInsets.exclude(
NavigationBarDefaults.windowInsets 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.composables.skeletons.MessageSkeletonVariant
import chat.revolt.internals.extensions.rememberChannelPermissions import chat.revolt.internals.extensions.rememberChannelPermissions
import chat.revolt.internals.extensions.zero import chat.revolt.internals.extensions.zero
import chat.revolt.screens.chat.LocalIsConnected
import chat.revolt.sheets.ChannelInfoSheet import chat.revolt.sheets.ChannelInfoSheet
import chat.revolt.sheets.MessageContextSheet import chat.revolt.sheets.MessageContextSheet
import chat.revolt.sheets.ReactSheet import chat.revolt.sheets.ReactSheet
@ -513,126 +515,137 @@ fun ChannelScreen(
Scaffold( Scaffold(
contentWindowInsets = WindowInsets.zero, contentWindowInsets = WindowInsets.zero,
topBar = { topBar = {
TopAppBar( Column {
modifier = Modifier.clickable { AnimatedVisibility(LocalIsConnected.current) {
channelInfoSheetShown = true Spacer(
}, Modifier
title = { .height(
Row( WindowInsets.statusBars.asPaddingValues()
verticalAlignment = Alignment.CenterVertically, .calculateTopPadding()
horizontalArrangement = Arrangement.spacedBy(8.dp) )
) { )
viewModel.channel?.let { }
when (it.channelType) { TopAppBar(
ChannelType.DirectMessage -> { 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 = val partner =
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)] RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
UserAvatar( PresenceBadge(
username = it.name ?: stringResource(R.string.unknown), presence = presenceFromStatus(
userId = ChannelUtils.resolveDMPartner(it) ?: "", partner?.status?.presence,
size = 24.dp, online = partner?.online == true
presenceSize = 12.dp, ),
avatar = partner?.avatar size = 12.dp
) )
} }
ChannelType.Group -> { Icon(
GroupIcon( imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
name = it.name ?: stringResource(R.string.unknown), contentDescription = null,
size = 24.dp, modifier = Modifier
icon = it.icon .size(16.dp)
) .alpha(0.5f)
} )
}
else -> { }
ChannelIcon( },
channelType = it.channelType ?: ChannelType.TextChannel, windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero,
modifier = Modifier navigationIcon = {
.size(24.dp) if (useDrawer) {
.alpha(0.8f) IconButton(onClick = onToggleDrawer) {
) Icon(
} imageVector = Icons.Default.Menu,
} contentDescription = stringResource(id = R.string.menu)
)
CompositionLocalProvider( }
LocalTextStyle provides LocalTextStyle.current.copy( }
fontSize = 20.sp, if (useBackButton) {
lineHeightStyle = LineHeightStyle( IconButton(onClick = backButtonAction ?: {}) {
alignment = LineHeightStyle.Alignment.Bottom, Icon(
trim = LineHeightStyle.Trim.LastLineBottom imageVector = Icons.AutoMirrored.Default.ArrowBack,
) contentDescription = stringResource(id = R.string.back)
)
) {
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)
)
}
}
}
)
} }
) { pv -> ) { pv ->
Crossfade( Crossfade(