diff --git a/app/build.gradle b/app/build.gradle
index 95dea4a1..dd487bd4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -101,6 +101,7 @@ android {
}
lintOptions {
abortOnError false
+ disable 'MissingTranslation'
}
namespace 'chat.revolt'
}
diff --git a/app/src/main/java/chat/revolt/api/internals/ChannelUtils.kt b/app/src/main/java/chat/revolt/api/internals/ChannelUtils.kt
new file mode 100644
index 00000000..32f354df
--- /dev/null
+++ b/app/src/main/java/chat/revolt/api/internals/ChannelUtils.kt
@@ -0,0 +1,15 @@
+package chat.revolt.api.internals
+
+import chat.revolt.api.RevoltAPI
+import chat.revolt.api.schemas.Channel
+
+object ChannelUtils {
+ fun resolveDMName(channel: Channel): String? {
+ return channel.name
+ ?: RevoltAPI.userCache[channel.recipients?.first { u -> u != RevoltAPI.selfId }]?.username
+ }
+
+ fun resolveDMPartner(channel: Channel): String? {
+ return channel.recipients?.first { u -> u != RevoltAPI.selfId }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/components/chat/MessageField.kt b/app/src/main/java/chat/revolt/components/chat/MessageField.kt
index c656e503..279594ff 100644
--- a/app/src/main/java/chat/revolt/components/chat/MessageField.kt
+++ b/app/src/main/java/chat/revolt/components/chat/MessageField.kt
@@ -1,10 +1,17 @@
package chat.revolt.components.chat
-import androidx.compose.animation.*
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@@ -12,7 +19,13 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Send
-import androidx.compose.material3.*
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -107,7 +120,7 @@ fun MessageField(
Icon(
Icons.Default.Add,
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
- contentDescription = stringResource(id = R.string.unknown),
+ contentDescription = stringResource(id = R.string.add_attachment_alt),
modifier = Modifier
.clip(CircleShape)
.size(32.dp)
@@ -132,7 +145,7 @@ fun MessageField(
Icon(
Icons.Default.Send,
tint = MaterialTheme.colorScheme.primary,
- contentDescription = stringResource(id = R.string.unknown),
+ contentDescription = stringResource(id = R.string.send_alt),
modifier = Modifier
.clip(CircleShape)
.size(32.dp)
diff --git a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
index eab7092d..c02165e1 100644
--- a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
+++ b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
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
@@ -123,6 +124,65 @@ fun UserAvatar(
}
}
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun GroupIcon(
+ name: String,
+ modifier: Modifier = Modifier,
+ icon: AutumnResource? = null,
+ rawUrl: String? = null,
+ size: Dp = 40.dp,
+ onLongClick: (() -> Unit)? = null,
+ onClick: (() -> Unit)? = null,
+) {
+ Box(
+ modifier = modifier
+ .size(size),
+ contentAlignment = Alignment.BottomEnd
+ ) {
+ if (icon != null) {
+ RemoteImage(
+ url = rawUrl ?: "$REVOLT_FILES/icons/${icon.id!!}/group.png",
+ contentScale = ContentScale.Crop,
+ description = stringResource(id = R.string.avatar_alt, name),
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.small)
+ .size(size)
+ .then(
+ if (onLongClick != null || onClick != null) Modifier
+ .combinedClickable(
+ onClick = { onClick?.invoke() },
+ onLongClick = { onLongClick?.invoke() }
+ )
+ else Modifier
+ )
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .size(size)
+ .then(
+ if (onLongClick != null || onClick != null) Modifier
+ .combinedClickable(
+ onClick = { onClick?.invoke() },
+ onLongClick = { onLongClick?.invoke() }
+ )
+ else Modifier
+ )
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.primary)
+ ) {
+ Text(
+ text = name.first().toString(),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+ }
+}
+
@Composable
fun UserAvatarWidthPlaceholder(
size: Dp = 40.dp,
diff --git a/app/src/main/java/chat/revolt/components/screens/chat/ChannelHeader.kt b/app/src/main/java/chat/revolt/components/screens/chat/ChannelHeader.kt
index 7859d079..d36edc64 100644
--- a/app/src/main/java/chat/revolt/components/screens/chat/ChannelHeader.kt
+++ b/app/src/main/java/chat/revolt/components/screens/chat/ChannelHeader.kt
@@ -22,7 +22,9 @@ 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.internals.ChannelUtils
import chat.revolt.api.schemas.Channel
+import chat.revolt.api.schemas.ChannelType
@Composable
fun ChannelHeader(
@@ -64,7 +66,13 @@ fun ChannelHeader(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = channel.name ?: "Ch #${channel.id}",
+ text = channel.name
+ ?: ChannelUtils.resolveDMName(channel)
+ ?: if (channel.channelType == ChannelType.SavedMessages) {
+ stringResource(R.string.channel_notes)
+ } else {
+ stringResource(R.string.unknown)
+ },
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
diff --git a/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt b/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt
index e3a89679..41b663ef 100644
--- a/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt
+++ b/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt
@@ -3,12 +3,12 @@ package chat.revolt.components.screens.chat.drawer.channel
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.lazy.LazyColumn
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.ModalBottomSheet
@@ -19,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -30,21 +29,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.revolt.R
import chat.revolt.api.RevoltAPI
+import chat.revolt.api.internals.ChannelUtils
import chat.revolt.api.schemas.ChannelType
+import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import chat.revolt.sheets.ChannelContextSheet
-import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RowScope.ChannelList(
serverId: String,
- drawerState: DrawerState,
+ currentDestination: String?,
currentChannel: String?,
onChannelClick: (String) -> Unit,
+ onSpecialClick: (String) -> Unit,
) {
- val coroutineScope = rememberCoroutineScope()
-
var channelContextSheetShown by remember { mutableStateOf(false) }
var channelContextSheetTarget by remember { mutableStateOf("") }
@@ -67,6 +66,13 @@ fun RowScope.ChannelList(
}
}
+ val dmAbleChannels =
+ RevoltAPI.channelCache.values
+ .filter { it.channelType == ChannelType.DirectMessage || it.channelType == ChannelType.Group }
+ .filter { if (it.channelType == ChannelType.DirectMessage) it.active == true else true }
+ .sortedBy { it.lastMessageID ?: it.id }
+ .reversed()
+
Surface(
tonalElevation = 1.dp,
modifier = Modifier
@@ -74,82 +80,153 @@ fun RowScope.ChannelList(
.clip(RoundedCornerShape(16.dp))
.fillMaxWidth(),
) {
- Column(
+ LazyColumn(
Modifier
.weight(1f)
+ .fillMaxSize(),
) {
if (serverId == "home") {
- Column(
- Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
+ item(
+ key = "header"
) {
- RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
- .forEach { channel ->
- DrawerChannel(
- name = channel.name
- ?: "GDM #${channel.id}",
- channelType = ChannelType.Group,
- selected = currentChannel == channel.id,
- hasUnread = channel.lastMessageID?.let { lastMessageID ->
- RevoltAPI.unreads.hasUnread(
- channel.id!!,
- lastMessageID
- )
- } ?: false,
- onClick = {
- onChannelClick(channel.id ?: return@DrawerChannel)
- coroutineScope.launch { drawerState.close() }
- },
- onLongClick = {
- channelContextSheetTarget = channel.id ?: return@DrawerChannel
- channelContextSheetShown = true
- }
+ Text(
+ text = stringResource(R.string.direct_messages),
+ style = MaterialTheme.typography.labelLarge,
+ fontSize = 24.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+
+ item(
+ key = "home"
+ ) {
+ DrawerChannel(
+ name = stringResource(R.string.home),
+ channelType = ChannelType.TextChannel,
+ selected = currentDestination == "home",
+ hasUnread = false,
+ onClick = {
+ onSpecialClick("home")
+ },
+ large = true,
+ )
+ }
+
+ item(
+ key = "notes"
+ ) {
+ val notesChannelId =
+ RevoltAPI.channelCache.values.firstOrNull { it.channelType == ChannelType.SavedMessages }?.id
+
+ DrawerChannel(
+ name = stringResource(R.string.channel_notes),
+ channelType = ChannelType.SavedMessages,
+ selected = currentDestination == "channel/{channelId}" && currentChannel == notesChannelId,
+ hasUnread = false,
+ onClick = {
+ onChannelClick(notesChannelId ?: return@DrawerChannel)
+ },
+ large = true,
+ )
+ }
+
+ item(
+ key = "divider"
+ ) {
+ Surface(
+ Modifier
+ .padding(vertical = 8.dp)
+ .fillMaxWidth()
+ .height(1.dp),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
+ ) {}
+ }
+
+ items(
+ dmAbleChannels.size,
+ key = { index ->
+ val channel = dmAbleChannels.getOrNull(index)
+ channel?.id ?: index
+ }
+ ) {
+ val channel = dmAbleChannels.getOrNull(it) ?: return@items
+
+ val partner =
+ if (channel.channelType == ChannelType.DirectMessage) RevoltAPI.userCache[ChannelUtils.resolveDMPartner(
+ channel
+ )] else null
+
+ DrawerChannel(
+ name = partner?.username ?: channel.name
+ ?: stringResource(R.string.unknown),
+ channelType = channel.channelType ?: ChannelType.TextChannel,
+ selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id,
+ hasUnread = channel.lastMessageID?.let { lastMessageID ->
+ RevoltAPI.unreads.hasUnread(
+ channel.id!!,
+ lastMessageID
)
+ } ?: false,
+ dmPartnerIcon = partner?.avatar ?: channel.icon,
+ dmPartnerId = partner?.id,
+ dmPartnerName = partner?.username,
+ dmPartnerStatus = presenceFromStatus(
+ partner?.status?.presence ?: "Offline"
+ ),
+ onClick = {
+ onChannelClick(channel.id ?: return@DrawerChannel)
+ },
+ onLongClick = {
+ channelContextSheetTarget = channel.id ?: return@DrawerChannel
+ channelContextSheetShown = true
}
+ )
}
} else {
val server = RevoltAPI.serverCache[serverId]
- Text(
- text = server?.name
- ?: stringResource(R.string.unknown),
- style = MaterialTheme.typography.labelLarge,
- fontSize = 24.sp,
- modifier = Modifier.padding(16.dp)
- )
+ item {
+ Text(
+ text = server?.name
+ ?: stringResource(R.string.unknown),
+ style = MaterialTheme.typography.labelLarge,
+ fontSize = 24.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
if (server?.channels?.isEmpty() == true) {
- Column(
- Modifier.weight(1f),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- text = stringResource(R.string.no_channels_heading),
- style = MaterialTheme.typography.labelLarge,
- textAlign = TextAlign.Center,
- fontSize = 24.sp,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- Text(
- text = stringResource(R.string.no_channels_body),
- style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center,
- )
+ item {
+ Column(
+ Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(R.string.no_channels_heading),
+ style = MaterialTheme.typography.labelLarge,
+ textAlign = TextAlign.Center,
+ fontSize = 24.sp,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ Text(
+ text = stringResource(R.string.no_channels_body),
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ )
+ }
}
} else {
- Column(
- Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
+ items(
+ server?.channels?.size ?: 0,
+ key = { server?.channels?.get(it) ?: "" }
) {
- server?.channels?.forEach { channelId ->
+ server?.channels?.get(it)?.let { channelId ->
RevoltAPI.channelCache[channelId]?.let { ch ->
DrawerChannel(
name = ch.name!!,
channelType = ch.channelType!!,
- selected = currentChannel == ch.id,
+ selected = currentDestination == "channel/{channelId}" && currentChannel == ch.id,
hasUnread = ch.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
ch.id!!,
@@ -158,7 +235,6 @@ fun RowScope.ChannelList(
} ?: true,
onClick = {
onChannelClick(ch.id ?: return@DrawerChannel)
- coroutineScope.launch { drawerState.close() }
},
onLongClick = {
channelContextSheetTarget = ch.id ?: return@DrawerChannel
diff --git a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt
index 14165424..da1c86db 100644
--- a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt
+++ b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt
@@ -6,7 +6,13 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@@ -20,7 +26,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.ChannelType
+import chat.revolt.components.generic.GroupIcon
+import chat.revolt.components.generic.Presence
+import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.screens.chat.ChannelIcon
@OptIn(ExperimentalFoundationApi::class)
@@ -32,6 +42,11 @@ fun DrawerChannel(
hasUnread: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit = {},
+ dmPartnerStatus: Presence? = null,
+ dmPartnerName: String? = null,
+ dmPartnerIcon: AutumnResource? = null,
+ dmPartnerId: String? = null,
+ large: Boolean = false,
) {
val backgroundColor = animateColorAsState(
if (selected) MaterialTheme.colorScheme.background
@@ -66,7 +81,37 @@ fun DrawerChannel(
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
- ChannelIcon(channelType = channelType, modifier = Modifier.padding(end = 8.dp))
+ when (channelType) {
+ ChannelType.DirectMessage -> UserAvatar(
+ username = dmPartnerName ?: "",
+ avatar = dmPartnerIcon,
+ userId = dmPartnerId ?: "",
+ presence = dmPartnerStatus,
+ size = 32.dp,
+ presenceSize = 16.dp,
+ modifier = Modifier.padding(end = 8.dp)
+ )
+
+ ChannelType.Group -> GroupIcon(
+ name = name,
+ icon = dmPartnerIcon,
+ size = 32.dp,
+ modifier = Modifier.padding(end = 8.dp)
+ )
+
+ else -> ChannelIcon(
+ channelType = channelType,
+ modifier = Modifier.then(
+ if (large) Modifier.padding(
+ end = 12.dp,
+ start = 4.dp,
+ top = 4.dp,
+ bottom = 4.dp
+ ) else Modifier.padding(end = 8.dp)
+ )
+ )
+ }
+
Text(
text = name,
fontWeight = FontWeight.Medium,
diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
index 9c77b251..ae4e2bf0 100644
--- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
@@ -153,6 +153,14 @@ class ChatRouterViewModel @Inject constructor(
}
}
}
+
+ fun navigateToSpecial(destination: String, navController: NavController) {
+ navController.navigate(destination) {
+ navController.graph.startDestinationRoute?.let { route ->
+ popUpTo(route)
+ }
+ }
+ }
}
@OptIn(
@@ -379,11 +387,16 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
) {
ChannelList(
serverId = it,
- drawerState = drawerState,
+ currentDestination = navController.currentDestination?.route,
currentChannel = viewModel.currentChannel,
onChannelClick = { channelId ->
viewModel.navigateToChannel(channelId, navController)
- }
+ scope.launch { drawerState.close() }
+ },
+ onSpecialClick = { destination ->
+ viewModel.navigateToSpecial(destination, navController)
+ scope.launch { drawerState.close() }
+ },
)
}
}
diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
index 54d4ed2c..11707c4b 100644
--- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
@@ -54,6 +54,7 @@ import chat.revolt.activities.RevoltTweenDp
import chat.revolt.activities.RevoltTweenFloat
import chat.revolt.activities.RevoltTweenInt
import chat.revolt.api.RevoltAPI
+import chat.revolt.api.internals.ChannelUtils
import chat.revolt.api.routes.microservices.autumn.FileArgs
import chat.revolt.components.chat.Message
import chat.revolt.components.chat.MessageField
@@ -390,7 +391,9 @@ fun ChannelScreen(
pickFileLauncher.launch(arrayOf("*/*"))
},
channelType = channel.channelType,
- channelName = channel.name ?: channel.id!!,
+ channelName = channel.name ?: ChannelUtils.resolveDMName(channel) ?: stringResource(
+ R.string.unknown
+ ),
forceSendButton = viewModel.attachments.isNotEmpty(),
disabled = viewModel.attachments.isNotEmpty() && viewModel.sendingMessage
)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 10f0eb39..849defc0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -109,6 +109,8 @@
%1$s\'s avatar
+ Direct Messages
+
Direct Message
Text Channel
Voice Channel