diff --git a/app/build.gradle b/app/build.gradle
index 417b57dc..a58f5cf2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,7 +26,8 @@ android {
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
+ shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
diff --git a/app/src/main/java/chat/revolt/api/routes/user/User.kt b/app/src/main/java/chat/revolt/api/routes/user/User.kt
index 7d0779f4..ba2544a3 100644
--- a/app/src/main/java/chat/revolt/api/routes/user/User.kt
+++ b/app/src/main/java/chat/revolt/api/routes/user/User.kt
@@ -31,10 +31,15 @@ suspend fun fetchSelf(): User {
}
suspend fun fetchUser(id: String): User {
- val response = RevoltHttp.get("/users/$id") {
+ val res = RevoltHttp.get("/users/$id") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
}
- .bodyAsText()
+
+ if (res.status.value == 404) {
+ return User.getPlaceholder(id)
+ }
+
+ val response = res.bodyAsText()
try {
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
diff --git a/app/src/main/java/chat/revolt/api/schemas/User.kt b/app/src/main/java/chat/revolt/api/schemas/User.kt
index ec0a2182..5b758daf 100644
--- a/app/src/main/java/chat/revolt/api/schemas/User.kt
+++ b/app/src/main/java/chat/revolt/api/schemas/User.kt
@@ -37,6 +37,22 @@ data class User(
online = partial.online ?: online
)
}
+
+ companion object {
+ fun getPlaceholder(forId: String) = User(
+ id = forId,
+ username = "Unknown User",
+ avatar = null,
+ badges = 0,
+ status = null,
+ profile = null,
+ flags = 0,
+ privileged = false,
+ bot = null,
+ relationship = null,
+ online = false
+ )
+ }
}
@Serializable
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 48a77d04..a05baa41 100644
--- a/app/src/main/java/chat/revolt/components/chat/MessageField.kt
+++ b/app/src/main/java/chat/revolt/components/chat/MessageField.kt
@@ -8,7 +8,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.KeyboardArrowLeft
+import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -75,7 +75,7 @@ fun MessageField(
modifier = Modifier.height(56.dp)
) {
Icon(
- Icons.Default.KeyboardArrowLeft,
+ Icons.Default.KeyboardArrowRight,
contentDescription = stringResource(id = R.string.show_more_alt),
modifier = Modifier
.size(24.dp + 8.dp)
diff --git a/app/src/main/java/chat/revolt/components/generic/RemoteImage.kt b/app/src/main/java/chat/revolt/components/generic/RemoteImage.kt
index 612f426d..9f634775 100644
--- a/app/src/main/java/chat/revolt/components/generic/RemoteImage.kt
+++ b/app/src/main/java/chat/revolt/components/generic/RemoteImage.kt
@@ -11,6 +11,7 @@ import coil.compose.AsyncImage
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
+import coil.memory.MemoryCache
import coil.request.ImageRequest
@Composable
@@ -20,19 +21,28 @@ fun RemoteImage(
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop
) {
+ val context = LocalContext.current
+
AsyncImage(
- model = ImageRequest.Builder(LocalContext.current)
+ model = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.build(),
- imageLoader = ImageLoader.Builder(LocalContext.current).components {
- if (Build.VERSION.SDK_INT >= 28) {
- add(ImageDecoderDecoder.Factory())
- } else {
- add(GifDecoder.Factory())
+ imageLoader = ImageLoader.Builder(context)
+ .components {
+ if (Build.VERSION.SDK_INT >= 28) {
+ add(ImageDecoderDecoder.Factory())
+ } else {
+ add(GifDecoder.Factory())
+ }
+ add(SvgDecoder.Factory())
}
- add(SvgDecoder.Factory())
- }.build(),
+ .memoryCache {
+ MemoryCache.Builder(context)
+ .maxSizePercent(.25)
+ .build()
+ }
+ .build(),
contentDescription = description,
contentScale = contentScale,
modifier = modifier
diff --git a/app/src/main/java/chat/revolt/components/screens/chat/DrawerChannel.kt b/app/src/main/java/chat/revolt/components/screens/chat/DrawerChannel.kt
new file mode 100644
index 00000000..e2f028cf
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/screens/chat/DrawerChannel.kt
@@ -0,0 +1,75 @@
+package chat.revolt.components.screens.chat
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import chat.revolt.api.schemas.ChannelType
+import chat.revolt.R
+
+@Composable
+fun DrawerChannel(
+ channelType: ChannelType,
+ name: String,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .padding(vertical = 4.dp, horizontal = 8.dp)
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(if (selected) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceVariant)
+ .clickable(onClick = onClick)
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ ) {
+ when (channelType) {
+ ChannelType.TextChannel -> {
+ Icon(
+ modifier = Modifier.padding(end = 8.dp),
+ painter = painterResource(R.drawable.ic_pound_24dp),
+ contentDescription = stringResource(R.string.channel_text)
+ )
+ }
+ ChannelType.VoiceChannel -> {
+ Icon(
+ modifier = Modifier.padding(end = 8.dp),
+ painter = painterResource(R.drawable.ic_volume_up_24dp),
+ contentDescription = stringResource(R.string.channel_voice)
+ )
+ }
+ ChannelType.SavedMessages -> {
+ Icon(
+ modifier = Modifier.padding(end = 8.dp),
+ painter = painterResource(R.drawable.ic_note_24dp),
+ contentDescription = stringResource(R.string.channel_notes)
+ )
+ }
+ else -> {
+ Icon(
+ modifier = Modifier.padding(end = 8.dp),
+ imageVector = Icons.Default.List,
+ contentDescription = "Channel"
+ )
+ }
+ }
+ Text(
+ text = name,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+}
\ No newline at end of file
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 d41a8b00..0b503f4f 100644
--- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
@@ -1,16 +1,23 @@
package chat.revolt.screens.chat
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -19,19 +26,20 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage
-import chat.revolt.components.generic.drawableResource
import chat.revolt.screens.chat.views.HomeScreen
import chat.revolt.R
+import chat.revolt.api.schemas.ChannelType
+import chat.revolt.components.screens.chat.DrawerChannel
import chat.revolt.screens.chat.views.ChannelScreen
import kotlinx.coroutines.launch
class ChatRouterViewModel : ViewModel() {
- private var _currentServer =
- mutableStateOf(RevoltAPI.serverCache.values.firstOrNull()?.id ?: "home")
+ private var _currentServer = mutableStateOf("home")
val currentServer: String
get() = _currentServer.value
@@ -50,71 +58,133 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
val channelDrawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val navController = rememberNavController()
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
- DismissibleNavigationDrawer(drawerState = channelDrawerState, drawerContent = {
- ModalDrawerSheet {
- Column(Modifier.fillMaxWidth()) {
- Row {
- Column(Modifier.verticalScroll(rememberScrollState())) {
- RemoteImage(
- url = drawableResource(R.drawable.ic_launcher_monochrome),
+ DismissibleNavigationDrawer(
+ drawerState = channelDrawerState,
+ drawerContent = {
+ ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant) {
+ Column(Modifier.fillMaxWidth()) {
+ Row {
+ Column(
modifier = Modifier
- .size(48.dp)
- .clip(CircleShape)
- .clickable { viewModel.goToHome() },
- description = "Home",
- )
- RevoltAPI.serverCache.values.forEach { server ->
- server.icon?.let { icon ->
- RemoteImage(
- url = "$REVOLT_FILES/icons/${icon.id!!}/server.png?max_side=256",
- modifier = Modifier
- .size(48.dp)
- .clip(CircleShape)
- .clickable { viewModel.setCurrentServer(server.id!!) },
- description = "${server.name}"
+ .verticalScroll(rememberScrollState())
+ .background(MaterialTheme.colorScheme.surface)
+ ) {
+ IconButton(
+ onClick = { viewModel.goToHome() },
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ ) {
+ Icon(
+ Icons.Default.Home,
+ contentDescription = stringResource(id = R.string.home),
+ modifier = Modifier.padding(4.dp)
)
}
- }
- }
- Column(
- Modifier
- .weight(1f)
- ) {
- if (viewModel.currentServer != "home") {
- val server = RevoltAPI.serverCache[viewModel.currentServer]
- Text(
- text = server?.name ?: "Unknown Server",
- fontWeight = FontWeight.Black,
- fontSize = 24.sp
- )
+ RevoltAPI.serverCache.values.forEach { server ->
+ if (server.name == null) return@forEach
- Column(
- Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
- ) {
- server?.channels?.forEach { channelId ->
- RevoltAPI.channelCache[channelId]?.let {
+ if (server.icon != null) {
+ RemoteImage(
+ url = "$REVOLT_FILES/icons/${server.icon.id!!}/server.png?max_side=256",
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ .clip(CircleShape)
+ .clickable { viewModel.setCurrentServer(server.id!!) },
+ description = "${server.name}"
+ )
+ } else {
+ // return a placeholder icon, currently the first letter of the server name in a circle
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .clickable { viewModel.setCurrentServer(server.id!!) }
+ ) {
Text(
- text = it.name ?: "Unnamed Channel",
- modifier = Modifier.clickable {
- scope.launch { channelDrawerState.close() }
- navController.navigate("channel/${it.id}")
- }
+ text = server.name.first().toString(),
+ fontSize = 20.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onSurface
)
}
}
}
- } else {
- Text(text = "Home not implemented!")
+ }
+
+ Crossfade(targetState = viewModel.currentServer) {
+ Column(
+ Modifier
+ .weight(1f)
+ ) {
+ if (it == "home") {
+ Column(
+ Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.DirectMessage }
+ .forEach { channel ->
+ DrawerChannel(
+ name = "DM #${channel.id}", // TODO get user or group name
+ channelType = ChannelType.DirectMessage,
+ selected = channel.id == (navBackStackEntry?.arguments?.getString(
+ "channelId"
+ ) ?: false),
+ onClick = {
+ navController.navigate("channel/${channel.id}")
+ scope.launch {
+ channelDrawerState.close()
+ }
+ }
+ )
+ }
+ }
+ } else {
+ val server = RevoltAPI.serverCache[it]
+
+ Text(
+ text = server?.name ?: stringResource(R.string.unknown),
+ fontWeight = FontWeight.Black,
+ fontSize = 24.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ Column(
+ Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ server?.channels?.forEach { channelId ->
+ RevoltAPI.channelCache[channelId]?.let { ch ->
+ DrawerChannel(
+ name = ch.name!!,
+ channelType = ch.channelType!!,
+ selected = navBackStackEntry?.arguments?.getString(
+ "channelId"
+ ) == ch.id,
+ onClick = {
+ scope.launch { channelDrawerState.close() }
+ navController.navigate("channel/${ch.id}")
+ })
+ }
+ }
+ }
+ }
+ }
}
}
}
}
}
- }) {
+ ) {
Column(Modifier.fillMaxSize()) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
diff --git a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt
index 00e93f4a..a62cfdb6 100644
--- a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt
@@ -1,15 +1,17 @@
package chat.revolt.screens.chat.views
import android.util.Log
-import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -27,6 +29,8 @@ import chat.revolt.api.schemas.Message as MessageSchema
import chat.revolt.components.chat.Message
import kotlinx.coroutines.launch
import chat.revolt.R
+import chat.revolt.RevoltTweenFloat
+import chat.revolt.RevoltTweenInt
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
import chat.revolt.components.chat.MessageField
@@ -181,30 +185,48 @@ fun ChannelScreen(
}
Column {
- Text(text = "#" + channel.name!!)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surface)
+ ) {
+ Text(
+ text = channel.name ?: channel.id!!,
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
- Divider()
-
- // Column nesting is needed to make the vertical scroll work properly
- Column(Modifier.weight(1f)) {
- Column(Modifier.verticalScroll(scrollState)) {
- viewModel.renderableMessages.forEach {
- Message(message = it)
- }
+ LazyColumn(Modifier.weight(1f)) {
+ items(viewModel.renderableMessages) { message ->
+ Message(message)
}
}
- AnimatedVisibility(visible = viewModel.typingUsers.isNotEmpty()) {
+ AnimatedVisibility(
+ visible = viewModel.typingUsers.isNotEmpty(),
+ enter = slideInVertically(
+ animationSpec = RevoltTweenInt,
+ initialOffsetY = { it }
+ ) + fadeIn(animationSpec = RevoltTweenFloat),
+ exit = slideOutVertically(
+ animationSpec = RevoltTweenInt,
+ targetOffsetY = { it }
+ ) + fadeOut(animationSpec = RevoltTweenFloat)
+ ) {
Row(
Modifier
- .padding(all = 4.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
+ .fillMaxWidth()
+ .padding(all = 4.dp)
) {
Text(
text = stringResource(
id = viewModel.typingMessageResource(),
viewModel.getTypingUsernames()
- )
+ ),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
)
}
}
@@ -218,7 +240,7 @@ fun ChannelScreen(
onMessageContentChange = viewModel::setMessageContent,
onSendMessage = viewModel::sendPendingMessage,
channelType = it,
- channelName = channel.name
+ channelName = channel.name ?: channel.id!!
)
}
}
diff --git a/app/src/main/res/drawable/ic_account_details_24dp.xml b/app/src/main/res/drawable/ic_account_details_24dp.xml
new file mode 100644
index 00000000..d79b9db8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_account_details_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_note_24dp.xml b/app/src/main/res/drawable/ic_note_24dp.xml
new file mode 100644
index 00000000..5d60a80c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_note_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_pound_24dp.xml b/app/src/main/res/drawable/ic_pound_24dp.xml
new file mode 100644
index 00000000..78c07e44
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pound_24dp.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_volume_up_24dp.xml b/app/src/main/res/drawable/ic_volume_up_24dp.xml
new file mode 100644
index 00000000..3ce9f287
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_up_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6f5f9597..c37dc587 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -74,8 +74,17 @@
Welcome to Revolt\'s in-progress Android experience!
Select a server and channel by swiping from the left.
+ Unknown
+ Home
+
%1$s\'s avatar
+ Direct Message
+ Text Channel
+ Voice Channel
+ Group
+ Notes
+
Message @%1$s
Message #%1$s
Message #%1$s