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.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.ExperimentalFoundationApi
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.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.ui.Alignment
import androidx.compose.ui.Modifier
@ -115,6 +128,8 @@ fun Message(
truncate: Boolean = false,
parse: (MessageSchema) -> SpannableStringBuilder = { SpannableStringBuilder(it.content) },
onMessageContextMenu: () -> Unit = {},
canReply: Boolean = false,
onReply: () -> Unit = {},
) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -23,14 +25,14 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.screens.chat.DoubleDrawerState
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RowScope.ChannelList(
serverId: String,
drawerState: DoubleDrawerState,
drawerState: DrawerState,
currentChannel: String?,
onChannelClick: (String) -> Unit,
onChannelLongClick: (String) -> Unit,
@ -69,7 +71,7 @@ fun RowScope.ChannelList(
} ?: false,
onClick = {
onChannelClick(channel.id ?: return@DrawerChannel)
coroutineScope.launch { drawerState.focusCenter() }
coroutineScope.launch { drawerState.close() }
},
onLongClick = {
onChannelLongClick(channel.id ?: return@DrawerChannel)
@ -127,7 +129,7 @@ fun RowScope.ChannelList(
} ?: true,
onClick = {
onChannelClick(ch.id ?: return@DrawerChannel)
coroutineScope.launch { drawerState.focusCenter() }
coroutineScope.launch { drawerState.close() }
},
onLongClick = {
onChannelLongClick(ch.id ?: return@DrawerChannel)

View File

@ -1,9 +1,8 @@
package chat.revolt.screens.chat
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
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.Row
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.filled.Add
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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -33,10 +36,10 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -52,13 +55,10 @@ import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.UserAvatar
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.server.DrawerServer
import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon
import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
import chat.revolt.components.screens.chat.rememberDoubleDrawerState
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
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
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) {
val drawerState = rememberDoubleDrawerState()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val keyboardController = LocalSoftwareKeyboardController.current
@ -184,11 +188,17 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
composition = sidebarSparkComposition,
)
BackHandler(enabled = drawerState.isClosed) {
scope.launch {
drawerState.open()
}
}
LaunchedEffect(drawerState) {
snapshotFlow { drawerState.currentValue }
.distinctUntilChanged()
.collect { state ->
if (state != DoubleDrawerOpenState.Center) {
if (state == DrawerValue.Closed) {
keyboardController?.hide()
}
}
@ -274,165 +284,167 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
})
}
DoubleDrawer(
state = drawerState,
startPanel = {
Column(Modifier.fillMaxWidth()) {
Row {
Column(
modifier = Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
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")
},
DismissibleNavigationDrawer(
drawerState = drawerState,
drawerContent = {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
) {
Column(Modifier.fillMaxWidth()) {
Row {
Column(
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")
}
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(id = R.string.server_plus_alt),
modifier = Modifier.padding(4.dp)
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
.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 = {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "👋", fontSize = 64.sp)
}
},
) {
Column(Modifier.fillMaxSize()) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController = topNav)
}
composable("channel/{channelId}") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelScreen(
navController = navController,
channelId = channelId
)
content = {
Column(Modifier.fillMaxSize()) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController = topNav)
}
composable("channel/{channelId}") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelScreen(
navController = navController,
channelId = channelId,
onToggleDrawer = {
scope.launch {
if (drawerState.isOpen) drawerState.close()
else drawerState.open()
}
}
)
}
}
composable("no_current_channel") {
NoCurrentChannelScreen()
}
}
composable("no_current_channel") {
NoCurrentChannelScreen()
}
bottomSheet("channel/{channelId}/info") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelInfoSheet(
navController = navController,
channelId = channelId
)
bottomSheet("channel/{channelId}/info") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelInfoSheet(
navController = navController,
channelId = channelId
)
}
}
}
bottomSheet("channel/{channelId}/menu") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelContextSheet(
navController = navController,
channelId = channelId
)
bottomSheet("channel/{channelId}/menu") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelContextSheet(
navController = navController,
channelId = channelId
)
}
}
}
bottomSheet("message/{messageId}/menu") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) {
MessageContextSheet(
navController = navController,
messageId = messageId
)
bottomSheet("message/{messageId}/menu") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) {
MessageContextSheet(
navController = navController,
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 ->
val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) {
ReportMessageDialog(
navController = navController,
messageId = messageId
)
dialog("report/message/{messageId}") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) {
ReportMessageDialog(
navController = navController,
messageId = messageId
)
}
}
}
}
}
}
})
}
}
}

View File

@ -1,21 +1,26 @@
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.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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.chat.ChannelIcon
import chat.revolt.components.generic.SheetClickable
@Composable
fun ChannelInfoSheet(
@ -33,20 +38,8 @@ fun ChannelInfoSheet(
.padding(horizontal = 16.dp)
.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))
Text(
text = stringResource(id = R.string.channel_info_sheet_description),
style = MaterialTheme.typography.bodySmall,
@ -57,5 +50,69 @@ fun ChannelInfoSheet(
?: stringResource(id = R.string.channel_info_sheet_description_empty),
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.compose.animation.*
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.*
@ -21,7 +18,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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.MessageField
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.TypingIndicator
import chat.revolt.internals.markdown.ChannelMentionRule
@ -58,6 +54,7 @@ import java.io.File
fun ChannelScreen(
navController: NavController,
channelId: String,
onToggleDrawer: () -> Unit,
viewModel: ChannelScreenViewModel = viewModel()
) {
val channel = viewModel.channel
@ -117,34 +114,14 @@ fun ChannelScreen(
}
Column {
Row(
modifier = Modifier
.clickable {
navController.navigate("channel/${channel.id}/info")
}
.fillMaxWidth()
.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),
)
}
ChannelHeader(
channel = channel,
onChannelClick = {
navController.navigate("channel/${channel.id}/info")
},
onToggleDrawer = onToggleDrawer
)
val isScrolledToBottom = remember(lazyListState) {
derivedStateOf {
lazyListState.firstVisibleItemIndex <= 6
@ -189,41 +166,49 @@ fun ChannelScreen(
items = viewModel.renderableMessages,
key = { it.id!! }
) { message ->
Message(message, parse = {
val parser = MarkdownParser()
.addRules(
SimpleMarkdownRules.createEscapeRule(),
UserMentionRule(),
ChannelMentionRule(),
CustomEmoteRule(),
)
.addRules(
createCodeRule(context, codeBlockColor.toArgb()),
createInlineCodeRule(context, codeBlockColor.toArgb()),
)
.addRules(
SimpleMarkdownRules.createSimpleMarkdownRules(
includeEscapeRule = false
Message(
message,
parse = {
val parser = MarkdownParser()
.addRules(
SimpleMarkdownRules.createEscapeRule(),
UserMentionRule(),
ChannelMentionRule(),
CustomEmoteRule(),
)
.addRules(
createCodeRule(context, codeBlockColor.toArgb()),
createInlineCodeRule(context, codeBlockColor.toArgb()),
)
.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(
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 ?: "",
)
)
}) {
navController.navigate("message/${message.id}/menu")
}
},
onMessageContextMenu = {
navController.navigate("message/${message.id}/menu")
},
canReply = true,
onReply = {
viewModel.replyToMessage(message)
},
)
}
item {

View File

@ -358,4 +358,13 @@ class ChannelScreenViewModel : ViewModel() {
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_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_failed_empty">Message is empty, nothing to copy</string>