feat: double drawer is now simple drawer

members will go into bottom sheet

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-05-19 23:16:34 +02:00
parent a8d70041cd
commit 440bf36ea2
9 changed files with 411 additions and 437 deletions

View File

@ -11,11 +11,24 @@ import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.* import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.combinedClickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.* import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -115,6 +128,8 @@ fun Message(
truncate: Boolean = false, truncate: Boolean = false,
parse: (MessageSchema) -> SpannableStringBuilder = { SpannableStringBuilder(it.content) }, parse: (MessageSchema) -> SpannableStringBuilder = { SpannableStringBuilder(it.content) },
onMessageContextMenu: () -> Unit = {}, onMessageContextMenu: () -> Unit = {},
canReply: Boolean = false,
onReply: () -> Unit = {},
) { ) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator() val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
val context = LocalContext.current val context = LocalContext.current

View File

@ -0,0 +1,84 @@
package chat.revolt.components.screens.chat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.revolt.R
import chat.revolt.api.schemas.Channel
@Composable
fun ChannelHeader(
channel: Channel,
onChannelClick: (String) -> Unit,
onToggleDrawer: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
channel.id?.let { onChannelClick(it) }
}
.padding(vertical = 4.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {
onToggleDrawer()
}) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(R.string.menu),
)
}
Spacer(modifier = Modifier.width(4.dp))
channel.channelType?.let {
ChannelIcon(
channelType = it,
modifier = Modifier.alpha(0.6f)
)
}
Spacer(modifier = Modifier.width(8.dp))
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = channel.name ?: "Ch #${channel.id}",
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
contentDescription = stringResource(R.string.menu),
modifier = Modifier
.size(18.dp)
.alpha(0.4f),
)
}
}
}

View File

@ -1,194 +0,0 @@
package chat.revolt.components.screens.chat
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.roundToInt
enum class DoubleDrawerOpenState {
Start,
Center,
End
}
@OptIn(ExperimentalMaterialApi::class)
class DoubleDrawerState(
var initialValue: DoubleDrawerOpenState = DoubleDrawerOpenState.Center,
val confirmStateChange: (DoubleDrawerOpenState) -> Boolean = { true }
) {
val swipeableState = SwipeableState<DoubleDrawerOpenState>(
initialValue = initialValue,
animationSpec = spring(),
confirmStateChange = confirmStateChange
)
suspend fun focusStart() {
swipeableState.animateTo(DoubleDrawerOpenState.Start)
}
suspend fun focusCenter() {
swipeableState.animateTo(DoubleDrawerOpenState.Center)
}
suspend fun focusEnd() {
swipeableState.animateTo(DoubleDrawerOpenState.End)
}
val isStart: Boolean
get() = swipeableState.currentValue == DoubleDrawerOpenState.Start
val isCenter: Boolean
get() = swipeableState.currentValue == DoubleDrawerOpenState.Center
val isEnd: Boolean
get() = swipeableState.currentValue == DoubleDrawerOpenState.End
val currentValue: DoubleDrawerOpenState
get() = swipeableState.currentValue
companion object {
fun Saver(
confirmStateChange: (DoubleDrawerOpenState) -> Boolean
): Saver<DoubleDrawerState, DoubleDrawerOpenState> = Saver(
save = { it.currentValue },
restore = { DoubleDrawerState(it, confirmStateChange) }
)
}
}
@Composable
fun rememberDoubleDrawerState(
initialValue: DoubleDrawerOpenState = DoubleDrawerOpenState.Center,
confirmStateChange: (DoubleDrawerOpenState) -> Boolean = { true }
): DoubleDrawerState = rememberSaveable(
saver = DoubleDrawerState.Saver(confirmStateChange)
) {
DoubleDrawerState(initialValue, confirmStateChange)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DoubleDrawer(
state: DoubleDrawerState = rememberDoubleDrawerState(),
startPanel: @Composable () -> Unit,
endPanel: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
BoxWithConstraints(Modifier.fillMaxSize()) {
val isPortrait =
LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
val drawerWeight =
if (isPortrait) 0.9f else 0.8f
val offsetValue =
(constraints.maxWidth * drawerWeight) + (LocalDensity.current.run { 16.dp.toPx() })
val isAtOffset = abs(state.swipeableState.offset.value) == abs(offsetValue)
val contentCornerRadius by animateDpAsState(
targetValue = if (isAtOffset) 16.dp else 0.dp,
)
Box(
modifier = Modifier
.fillMaxSize()
.swipeable(
state = state.swipeableState,
orientation = Orientation.Horizontal,
velocityThreshold = 500.dp,
anchors = mapOf(
offsetValue to DoubleDrawerOpenState.Start,
0f to DoubleDrawerOpenState.Center,
-offsetValue to DoubleDrawerOpenState.End
),
reverseDirection = layoutDirection == LayoutDirection.Rtl,
resistance = ResistanceConfig(0.5f, 0.5f)
)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(drawerWeight)
.align(Alignment.CenterStart)
.offset {
IntOffset(
x = state.swipeableState.offset.value.roundToInt() - offsetValue.roundToInt(),
y = 0
)
},
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp))
) {
startPanel()
}
}
Box(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
.offset {
IntOffset(
x = state.swipeableState.offset.value.roundToInt(),
y = 0
)
},
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(contentCornerRadius))
) {
content()
}
}
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(drawerWeight)
.clip(
RoundedCornerShape(
topStart = 16.dp,
bottomStart = 16.dp
)
)
.align(Alignment.CenterEnd)
.offset {
IntOffset(
x = state.swipeableState.offset.value.roundToInt() + offsetValue.roundToInt(),
y = 0
)
},
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp))
) {
endPanel()
}
}
}
}
}

View File

@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -23,14 +25,14 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.schemas.ChannelType import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.screens.chat.DoubleDrawerState
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RowScope.ChannelList( fun RowScope.ChannelList(
serverId: String, serverId: String,
drawerState: DoubleDrawerState, drawerState: DrawerState,
currentChannel: String?, currentChannel: String?,
onChannelClick: (String) -> Unit, onChannelClick: (String) -> Unit,
onChannelLongClick: (String) -> Unit, onChannelLongClick: (String) -> Unit,
@ -69,7 +71,7 @@ fun RowScope.ChannelList(
} ?: false, } ?: false,
onClick = { onClick = {
onChannelClick(channel.id ?: return@DrawerChannel) onChannelClick(channel.id ?: return@DrawerChannel)
coroutineScope.launch { drawerState.focusCenter() } coroutineScope.launch { drawerState.close() }
}, },
onLongClick = { onLongClick = {
onChannelLongClick(channel.id ?: return@DrawerChannel) onChannelLongClick(channel.id ?: return@DrawerChannel)
@ -127,7 +129,7 @@ fun RowScope.ChannelList(
} ?: true, } ?: true,
onClick = { onClick = {
onChannelClick(ch.id ?: return@DrawerChannel) onChannelClick(ch.id ?: return@DrawerChannel)
coroutineScope.launch { drawerState.focusCenter() } coroutineScope.launch { drawerState.close() }
}, },
onLongClick = { onLongClick = {
onChannelLongClick(ch.id ?: return@DrawerChannel) onChannelLongClick(ch.id ?: return@DrawerChannel)

View File

@ -1,9 +1,8 @@
package chat.revolt.screens.chat package chat.revolt.screens.chat
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
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.aspectRatio import androidx.compose.foundation.layout.aspectRatio
@ -18,11 +17,15 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -33,10 +36,10 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -52,13 +55,10 @@ import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.components.chat.DisconnectedNotice import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.DoubleDrawer
import chat.revolt.components.screens.chat.DoubleDrawerOpenState
import chat.revolt.components.screens.chat.drawer.channel.ChannelList import chat.revolt.components.screens.chat.drawer.channel.ChannelList
import chat.revolt.components.screens.chat.drawer.server.DrawerServer import chat.revolt.components.screens.chat.drawer.server.DrawerServer
import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon
import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
import chat.revolt.components.screens.chat.rememberDoubleDrawerState
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.sheets.AddServerSheet import chat.revolt.screens.chat.sheets.AddServerSheet
@ -168,10 +168,14 @@ class ChatRouterViewModel @Inject constructor(
} }
} }
@OptIn(ExperimentalMaterialNavigationApi::class, ExperimentalComposeUiApi::class) @OptIn(
ExperimentalMaterialNavigationApi::class,
ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class
)
@Composable @Composable
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) { fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) {
val drawerState = rememberDoubleDrawerState() val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@ -184,11 +188,17 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
composition = sidebarSparkComposition, composition = sidebarSparkComposition,
) )
BackHandler(enabled = drawerState.isClosed) {
scope.launch {
drawerState.open()
}
}
LaunchedEffect(drawerState) { LaunchedEffect(drawerState) {
snapshotFlow { drawerState.currentValue } snapshotFlow { drawerState.currentValue }
.distinctUntilChanged() .distinctUntilChanged()
.collect { state -> .collect { state ->
if (state != DoubleDrawerOpenState.Center) { if (state == DrawerValue.Closed) {
keyboardController?.hide() keyboardController?.hide()
} }
} }
@ -274,165 +284,167 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
}) })
} }
DoubleDrawer( DismissibleNavigationDrawer(
state = drawerState, drawerState = drawerState,
startPanel = { drawerContent = {
Column(Modifier.fillMaxWidth()) { DismissibleDrawerSheet(
Row { drawerContainerColor = Color.Transparent,
Column( ) {
modifier = Modifier Column(Modifier.fillMaxWidth()) {
.fillMaxHeight() Row {
.verticalScroll(rememberScrollState()), Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
UserAvatar(
username = RevoltAPI.userCache[RevoltAPI.selfId]?.username
?: "",
presence = presenceFromStatus(
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence
?: ""
),
userId = RevoltAPI.selfId ?: "",
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
viewModel.navigateToServer("home", navController)
},
onLongClick = {
navController.navigate("status")
},
modifier = Modifier modifier = Modifier
.padding(8.dp) .fillMaxHeight()
.size(48.dp) .verticalScroll(rememberScrollState()),
) horizontalAlignment = Alignment.CenterHorizontally
ServerDrawerSeparator()
RevoltAPI.serverCache.values
.sortedBy { it.id }
.forEach { server ->
if (server.id == null || server.name == null) return@forEach
DrawerServer(
iconId = server.icon?.id,
serverName = server.name,
hasUnreads = RevoltAPI.unreads.serverHasUnread(server.id),
) {
viewModel.navigateToServer(
server.id,
navController
)
}
}
DrawerServerlikeIcon(
onClick = {
navController.navigate("add_server")
}
) { ) {
Icon( UserAvatar(
Icons.Default.Add, username = RevoltAPI.userCache[RevoltAPI.selfId]?.username
contentDescription = stringResource(id = R.string.server_plus_alt), ?: "",
modifier = Modifier.padding(4.dp) presence = presenceFromStatus(
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence
?: ""
),
userId = RevoltAPI.selfId ?: "",
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
viewModel.navigateToServer("home", navController)
},
onLongClick = {
navController.navigate("status")
},
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
ServerDrawerSeparator()
RevoltAPI.serverCache.values
.sortedBy { it.id }
.forEach { server ->
if (server.id == null || server.name == null) return@forEach
DrawerServer(
iconId = server.icon?.id,
serverName = server.name,
hasUnreads = RevoltAPI.unreads.serverHasUnread(
server.id
),
) {
viewModel.navigateToServer(
server.id,
navController
)
}
}
DrawerServerlikeIcon(
onClick = {
navController.navigate("add_server")
}
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(id = R.string.server_plus_alt),
modifier = Modifier.padding(4.dp)
)
}
}
Crossfade(
targetState = viewModel.currentServer,
label = "Channel List"
) {
ChannelList(
serverId = it,
drawerState = drawerState,
currentChannel = viewModel.currentChannel,
onChannelClick = { channelId ->
viewModel.navigateToChannel(channelId, navController)
},
onChannelLongClick = { channelId ->
navController.navigate("channel/$channelId/info")
},
) )
} }
} }
Crossfade(
targetState = viewModel.currentServer,
label = "Channel List"
) {
ChannelList(
serverId = it,
drawerState = drawerState,
currentChannel = viewModel.currentChannel,
onChannelClick = { channelId ->
viewModel.navigateToChannel(channelId, navController)
},
onChannelLongClick = { channelId ->
navController.navigate("channel/$channelId/info")
},
)
}
} }
} }
}, },
endPanel = { content = {
Box( Column(Modifier.fillMaxSize()) {
modifier = Modifier NavHost(navController = navController, startDestination = "home") {
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) composable("home") {
.fillMaxSize(), HomeScreen(navController = topNav)
contentAlignment = Alignment.Center }
) { composable("channel/{channelId}") { backStackEntry ->
Text(text = "👋", fontSize = 64.sp) val channelId = backStackEntry.arguments?.getString("channelId")
} if (channelId != null) {
}, ChannelScreen(
) { navController = navController,
Column(Modifier.fillMaxSize()) { channelId = channelId,
NavHost(navController = navController, startDestination = "home") { onToggleDrawer = {
composable("home") { scope.launch {
HomeScreen(navController = topNav) if (drawerState.isOpen) drawerState.close()
} else drawerState.open()
composable("channel/{channelId}") { backStackEntry -> }
val channelId = backStackEntry.arguments?.getString("channelId") }
if (channelId != null) { )
ChannelScreen( }
navController = navController, }
channelId = channelId composable("no_current_channel") {
) NoCurrentChannelScreen()
} }
}
composable("no_current_channel") {
NoCurrentChannelScreen()
}
bottomSheet("channel/{channelId}/info") { backStackEntry -> bottomSheet("channel/{channelId}/info") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId") val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) { if (channelId != null) {
ChannelInfoSheet( ChannelInfoSheet(
navController = navController, navController = navController,
channelId = channelId channelId = channelId
) )
}
} }
} bottomSheet("channel/{channelId}/menu") { backStackEntry ->
bottomSheet("channel/{channelId}/menu") { backStackEntry -> val channelId = backStackEntry.arguments?.getString("channelId")
val channelId = backStackEntry.arguments?.getString("channelId") if (channelId != null) {
if (channelId != null) { ChannelContextSheet(
ChannelContextSheet( navController = navController,
navController = navController, channelId = channelId
channelId = channelId )
) }
} }
} bottomSheet("message/{messageId}/menu") { backStackEntry ->
bottomSheet("message/{messageId}/menu") { backStackEntry -> val messageId = backStackEntry.arguments?.getString("messageId")
val messageId = backStackEntry.arguments?.getString("messageId") if (messageId != null) {
if (messageId != null) { MessageContextSheet(
MessageContextSheet( navController = navController,
navController = navController, messageId = messageId
messageId = messageId )
) }
}
bottomSheet("status") {
StatusSheet(navController = navController, topNav = topNav)
}
bottomSheet("add_server") {
AddServerSheet()
} }
}
bottomSheet("status") {
StatusSheet(navController = navController, topNav = topNav)
}
bottomSheet("add_server") {
AddServerSheet()
}
dialog("report/message/{messageId}") { backStackEntry -> dialog("report/message/{messageId}") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId") val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) { if (messageId != null) {
ReportMessageDialog( ReportMessageDialog(
navController = navController, navController = navController,
messageId = messageId messageId = messageId
) )
}
} }
} }
} }
} })
}
} }
} }
} }

View File

@ -1,21 +1,26 @@
package chat.revolt.screens.chat.sheets package chat.revolt.screens.chat.sheets
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.schemas.ChannelType import chat.revolt.components.generic.SheetClickable
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.chat.ChannelIcon
@Composable @Composable
fun ChannelInfoSheet( fun ChannelInfoSheet(
@ -33,20 +38,8 @@ fun ChannelInfoSheet(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
ChannelIcon(
channelType = channel.channelType ?: ChannelType.TextChannel,
modifier = Modifier.size(32.dp)
)
PageHeader(
text = channel.name ?: channel.id ?: "",
modifier = Modifier.offset((-4).dp, 0.dp)
)
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = R.string.channel_info_sheet_description), text = stringResource(id = R.string.channel_info_sheet_description),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@ -57,5 +50,69 @@ fun ChannelInfoSheet(
?: stringResource(id = R.string.channel_info_sheet_description_empty), ?: stringResource(id = R.string.channel_info_sheet_description_empty),
modifier = Modifier.padding(bottom = 10.dp) modifier = Modifier.padding(bottom = 10.dp)
) )
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.channel_info_sheet_options),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 4.dp)
)
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.List,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.channel_info_sheet_options_members),
style = style
)
}
) {
}
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.channel_info_sheet_options_invite),
style = style
)
}
) {
}
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage),
style = style
)
}
) {
}
Spacer(modifier = Modifier.height(8.dp))
} }
} }

View File

@ -4,13 +4,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.* import androidx.compose.material3.*
@ -21,7 +18,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -36,7 +32,7 @@ import chat.revolt.api.routes.microservices.autumn.FileArgs
import chat.revolt.components.chat.Message import chat.revolt.components.chat.Message
import chat.revolt.components.chat.MessageField import chat.revolt.components.chat.MessageField
import chat.revolt.components.screens.chat.AttachmentManager import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.ChannelIcon import chat.revolt.components.screens.chat.ChannelHeader
import chat.revolt.components.screens.chat.ReplyManager import chat.revolt.components.screens.chat.ReplyManager
import chat.revolt.components.screens.chat.TypingIndicator import chat.revolt.components.screens.chat.TypingIndicator
import chat.revolt.internals.markdown.ChannelMentionRule import chat.revolt.internals.markdown.ChannelMentionRule
@ -58,6 +54,7 @@ import java.io.File
fun ChannelScreen( fun ChannelScreen(
navController: NavController, navController: NavController,
channelId: String, channelId: String,
onToggleDrawer: () -> Unit,
viewModel: ChannelScreenViewModel = viewModel() viewModel: ChannelScreenViewModel = viewModel()
) { ) {
val channel = viewModel.channel val channel = viewModel.channel
@ -117,33 +114,13 @@ fun ChannelScreen(
} }
Column { Column {
Row( ChannelHeader(
modifier = Modifier channel = channel,
.clickable { onChannelClick = {
navController.navigate("channel/${channel.id}/info") navController.navigate("channel/${channel.id}/info")
} },
.fillMaxWidth() onToggleDrawer = onToggleDrawer
.clip( )
RoundedCornerShape(
bottomStart = 16.dp,
bottomEnd = 16.dp
)
)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
ChannelIcon(
channelType = channel.channelType,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = channel.name ?: channel.id!!,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
)
}
val isScrolledToBottom = remember(lazyListState) { val isScrolledToBottom = remember(lazyListState) {
derivedStateOf { derivedStateOf {
@ -189,41 +166,49 @@ fun ChannelScreen(
items = viewModel.renderableMessages, items = viewModel.renderableMessages,
key = { it.id!! } key = { it.id!! }
) { message -> ) { message ->
Message(message, parse = { Message(
val parser = MarkdownParser() message,
.addRules( parse = {
SimpleMarkdownRules.createEscapeRule(), val parser = MarkdownParser()
UserMentionRule(), .addRules(
ChannelMentionRule(), SimpleMarkdownRules.createEscapeRule(),
CustomEmoteRule(), UserMentionRule(),
) ChannelMentionRule(),
.addRules( CustomEmoteRule(),
createCodeRule(context, codeBlockColor.toArgb()), )
createInlineCodeRule(context, codeBlockColor.toArgb()), .addRules(
) createCodeRule(context, codeBlockColor.toArgb()),
.addRules( createInlineCodeRule(context, codeBlockColor.toArgb()),
SimpleMarkdownRules.createSimpleMarkdownRules( )
includeEscapeRule = false .addRules(
SimpleMarkdownRules.createSimpleMarkdownRules(
includeEscapeRule = false
)
)
SimpleRenderer.render(
source = it.content ?: "",
parser = parser,
initialState = MarkdownState(0),
renderContext = MarkdownContext(
memberMap = mapOf(),
userMap = RevoltAPI.userCache.toMap(),
channelMap = RevoltAPI.channelCache.mapValues { ch ->
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
},
emojiMap = RevoltAPI.emojiCache,
serverId = channel.server ?: "",
) )
) )
},
SimpleRenderer.render( onMessageContextMenu = {
source = it.content ?: "", navController.navigate("message/${message.id}/menu")
parser = parser, },
initialState = MarkdownState(0), canReply = true,
renderContext = MarkdownContext( onReply = {
memberMap = mapOf(), viewModel.replyToMessage(message)
userMap = RevoltAPI.userCache.toMap(), },
channelMap = RevoltAPI.channelCache.mapValues { ch -> )
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
},
emojiMap = RevoltAPI.emojiCache,
serverId = channel.server ?: "",
)
)
}) {
navController.navigate("message/${message.id}/menu")
}
} }
item { item {

View File

@ -358,4 +358,13 @@ class ChannelScreenViewModel : ViewModel() {
Log.d("ChannelScreen", "Acking channel") Log.d("ChannelScreen", "Acking channel")
} }
} }
fun replyToMessage(message: Message) {
addInReplyTo(
SendMessageReply(
id = message.id!!,
mention = false
)
)
}
} }

View File

@ -149,6 +149,10 @@
<string name="channel_info_sheet_description">Channel description</string> <string name="channel_info_sheet_description">Channel description</string>
<string name="channel_info_sheet_description_empty">There hasn\'t been a description set for this channel yet.</string> <string name="channel_info_sheet_description_empty">There hasn\'t been a description set for this channel yet.</string>
<string name="channel_info_sheet_options">Options</string>
<string name="channel_info_sheet_options_members">Members</string>
<string name="channel_info_sheet_options_invite">Invite</string>
<string name="channel_info_sheet_options_notifications_manage">Manage notifications</string>
<string name="message_context_sheet_actions_copy">Copy</string> <string name="message_context_sheet_actions_copy">Copy</string>
<string name="message_context_sheet_actions_copy_failed_empty">Message is empty, nothing to copy</string> <string name="message_context_sheet_actions_copy_failed_empty">Message is empty, nothing to copy</string>