feat: consolidate the last week of changes

- unfortunately forgot to do atomic commits
 - design improved in many many areas
 - app now crashes less
 - messages are now virtualised
 - remote images are now cached in-memory
 - more strings (most unused)
This commit is contained in:
Infi 2023-01-03 01:58:20 +01:00
parent 2eeb44e800
commit 8f068edeec
13 changed files with 319 additions and 80 deletions

View File

@ -26,7 +26,8 @@ android {
buildTypes {
release {
minifyEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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") {

View File

@ -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!!
)
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,9c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3m3,11H2v-2c0,-2.21 2.69,-4 6,-4s6,1.79 6,4m8,-6v2h-9v-2m9,-4v2h-9V8m9,-4v2h-9V4Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1s-2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,2.75c0.41,0 0.75,0.34 0.75,0.75s-0.34,0.75 -0.75,0.75s-0.75,-0.34 -0.75,-0.75s0.34,-0.75 0.75,-0.75zM9.1,17L7,17v-2.14l5.96,-5.96l2.12,2.12L9.1,17zM16.85,9.27l-1.06,1.06l-2.12,-2.12l1.06,-1.06c0.2,-0.2 0.51,-0.2 0.71,0l1.41,1.41c0.2,0.2 0.2,0.51 0,0.71z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="m5.41,21l0.71,-4h-4l0.35,-2h4l1.06,-6h-4l0.35,-2h4l0.71,-4h2l-0.71,4h6l0.71,-4h2l-0.71,4h4l-0.35,2h-4l-1.06,6h4l-0.35,2h-4l-0.71,4h-2l0.71,-4h-6l-0.71,4h-2M9.53,9l-1.06,6h6l1.06,-6h-6Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12A4.5,4.5 0,0 0,14 7.97v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -74,8 +74,17 @@
<string name="tutorial">Welcome to Revolt\'s in-progress Android experience!</string>
<string name="select_channel">Select a server and channel by swiping from the left.</string>
<string name="unknown">Unknown</string>
<string name="home">Home</string>
<string name="avatar_alt">%1$s\'s avatar</string>
<string name="channel_dm">Direct Message</string>
<string name="channel_text">Text Channel</string>
<string name="channel_voice">Voice Channel</string>
<string name="channel_group">Group</string>
<string name="channel_notes">Notes</string>
<string name="message_field_placeholder_dm">Message @%1$s</string>
<string name="message_field_placeholder_text">Message #%1$s</string>
<string name="message_field_placeholder_voice">Message #%1$s</string>