feat: revamp message field, colours, typing, attachments

This commit is contained in:
Infi 2023-02-05 04:43:42 +01:00
parent e1abc4fff5
commit fcddb8a968
11 changed files with 220 additions and 180 deletions

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import chat.revolt.api.settings.GlobalState
@ -43,6 +44,7 @@ class MainActivity : ComponentActivity() {
val RevoltTweenInt: FiniteAnimationSpec<IntOffset> = tween(400, easing = EaseInOutExpo)
val RevoltTweenIntSize: FiniteAnimationSpec<IntSize> = tween(400, easing = EaseInOutExpo)
val RevoltTweenFloat: FiniteAnimationSpec<Float> = tween(400, easing = EaseInOutExpo)
val RevoltTweenDp: FiniteAnimationSpec<Dp> = tween(400, easing = EaseInOutExpo)
@OptIn(ExperimentalAnimationApi::class)
@Composable

View File

@ -1,32 +1,37 @@
package chat.revolt.components.chat
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
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.KeyboardArrowRight
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.revolt.R
import chat.revolt.RevoltTweenFloat
import chat.revolt.RevoltTweenInt
import chat.revolt.api.schemas.ChannelType
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageField(
showButtons: Boolean,
onToggleButtons: (Boolean) -> Unit,
messageContent: String,
onMessageContentChange: (String) -> Unit,
onAddAttachment: () -> Unit,
@ -46,90 +51,85 @@ fun MessageField(
ChannelType.SavedMessages -> R.string.message_field_placeholder_notes
}
val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled
Row(modifier) {
// Additional buttons (currently adding an attachment)
AnimatedVisibility(visible = showButtons) {
Row(Modifier.weight(1f)) {
ElevatedButton(
onClick = {
focusRequester.freeFocus() // hide keyboard because it's annoying
onAddAttachment()
},
modifier = Modifier.size(56.dp),
contentPadding = PaddingValues(0.dp),
shape = CircleShape
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(id = R.string.add_attachment_alt)
)
}
}
}
// The small chevron you see when the buttons are hidden
AnimatedVisibility(visible = !showButtons) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.height(56.dp)
) {
Icon(
Icons.Default.KeyboardArrowRight,
contentDescription = stringResource(id = R.string.show_more_alt),
modifier = Modifier
.clickable(onClick = {
onToggleButtons(true)
})
.size(24.dp + 8.dp)
.padding(vertical = 4.dp)
)
}
}
TextField(
BasicTextField(
value = messageContent,
onValueChange = onMessageContentChange,
singleLine = false,
shape = MaterialTheme.shapes.extraLarge,
enabled = !disabled,
placeholder = {
Text(
stringResource(
id = placeholderResource,
channelName
)
)
},
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
placeholderColor = Color.Gray,
),
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
.focusRequester(focusRequester)
)
// Send button (visible when text is entered or when forceSendButton is true)
AnimatedVisibility(visible = (messageContent.isNotBlank() || forceSendButton) && !disabled) {
Button(
onClick = onSendMessage,
modifier = Modifier
.padding(start = 8.dp)
.size(56.dp),
contentPadding = PaddingValues(0.dp),
shape = CircleShape
) {
Icon(
Icons.Default.Send,
contentDescription = stringResource(id = R.string.send_alt)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions.Default,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = messageContent,
innerTextField = innerTextField,
enabled = !disabled,
singleLine = false,
visualTransformation = VisualTransformation.None,
interactionSource = remember { MutableInteractionSource() },
placeholder = {
Text(
stringResource(
id = placeholderResource,
channelName
)
)
},
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
placeholderColor = Color.Gray
),
contentPadding = PaddingValues(16.dp),
leadingIcon = {
Icon(
Icons.Default.Add,
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
contentDescription = stringResource(id = R.string.unknown),
modifier = Modifier
.clip(CircleShape)
.size(32.dp)
.clickable {
focusRequester.freeFocus() // hide keyboard because it's annoying
onAddAttachment()
}
.padding(4.dp)
)
},
trailingIcon = {
AnimatedVisibility(sendButtonVisible,
enter = slideInVertically(
animationSpec = RevoltTweenInt,
initialOffsetY = { it }
) + fadeIn(animationSpec = RevoltTweenFloat),
exit = slideOutVertically(
animationSpec = RevoltTweenInt,
targetOffsetY = { it }
) + fadeOut(animationSpec = RevoltTweenFloat)) {
Icon(
Icons.Default.Send,
tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(id = R.string.unknown),
modifier = Modifier
.clip(CircleShape)
.size(32.dp)
.clickable { onSendMessage() }
.padding(4.dp)
)
}
}
)
}
}
)
}
}
@ -137,8 +137,6 @@ fun MessageField(
@Composable
fun MessageFieldPreview() {
MessageField(
showButtons = true,
onToggleButtons = {},
messageContent = "Hello world!",
onMessageContentChange = {},
onSendMessage = {},

View File

@ -1,22 +1,17 @@
package chat.revolt.components.screens.chat
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.border
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.revolt.R
@ -30,24 +25,30 @@ fun AttachmentManager(
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
AnimatedVisibility(uploading) {
CircularProgressIndicator()
CircularProgressIndicator(
modifier = Modifier.padding(4.dp),
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
)
}
attachments.forEach { attachment ->
Row(
modifier = Modifier
.padding(4.dp)
.border(
1.dp,
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
.clickable {
onRemove(attachment)
}
.background(
color = MaterialTheme.colorScheme.background,
shape = MaterialTheme.shapes.small
)
.padding(8.dp)
) {
Text(attachment.filename, maxLines = 1)

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,7 +27,10 @@ fun DrawerChannel(
.padding(vertical = 4.dp, horizontal = 8.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(if (selected) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceVariant)
.background(
if (selected) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
.clickable(onClick = onClick)
.padding(vertical = 8.dp, horizontal = 16.dp)
) {

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -41,7 +42,7 @@ fun DrawerServer(
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.clickable(onClick = onClick)
) {
Text(

View File

@ -22,7 +22,7 @@ fun DrawerServerlikeIcon(
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(MaterialTheme.colorScheme.surface)
) {
content()
}

View File

@ -0,0 +1,64 @@
package chat.revolt.components.screens.chat
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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 chat.revolt.R
import chat.revolt.RevoltTweenFloat
import chat.revolt.RevoltTweenInt
import chat.revolt.api.RevoltAPI
@Composable
fun TypingIndicator(
users: List<String>,
) {
fun typingMessageResource(): Int {
return when (users.size) {
0 -> R.string.typing_blank
1 -> R.string.typing_one
in 2..4 -> R.string.typing_many
else -> R.string.typing_several
}
}
AnimatedVisibility(
visible = users.isNotEmpty(),
enter = slideInVertically(
animationSpec = RevoltTweenInt,
initialOffsetY = { it }
) + fadeIn(animationSpec = RevoltTweenFloat),
exit = slideOutVertically(
animationSpec = RevoltTweenInt,
targetOffsetY = { it }
) + fadeOut(animationSpec = RevoltTweenFloat)
) {
Row(
Modifier
.background(MaterialTheme.colorScheme.surface)
.fillMaxWidth()
.padding(all = 4.dp)
) {
Text(
text = stringResource(
id = typingMessageResource(),
users.joinToString {
RevoltAPI.userCache[it]?.let { u ->
u.username ?: u.id
} ?: it
}
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View File

@ -85,14 +85,16 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
DismissibleNavigationDrawer(
drawerState = channelDrawerState,
drawerContent = {
ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant) {
ModalDrawerSheet(
drawerContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
) {
Column(Modifier.fillMaxWidth()) {
Row {
Column(
modifier = Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.background(MaterialTheme.colorScheme.surface)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
) {
DrawerServerlikeIcon(
onClick = {

View File

@ -5,6 +5,7 @@ import android.widget.Toast
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.*
@ -24,7 +25,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@ -34,6 +34,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.RevoltTweenDp
import chat.revolt.RevoltTweenFloat
import chat.revolt.RevoltTweenInt
import chat.revolt.api.RevoltAPI
@ -55,6 +56,7 @@ import chat.revolt.components.generic.CollapsibleCard
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.ChannelIcon
import chat.revolt.components.screens.chat.TypingIndicator
import io.ktor.http.*
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
@ -89,20 +91,6 @@ class ChannelScreenViewModel : ViewModel() {
fun setMessageContent(content: String) {
_messageContent = content
if (content.isEmpty()) {
_showButtons = true
} else if (showButtons) {
_showButtons = false
}
}
private var _showButtons by mutableStateOf(true)
val showButtons: Boolean
get() = _showButtons
fun setShowButtons(show: Boolean) {
_showButtons = show
}
private var _attachments = mutableStateListOf<FileArgs>()
@ -262,23 +250,6 @@ class ChannelScreenViewModel : ViewModel() {
}
}
fun typingMessageResource(): Int {
return when (typingUsers.size) {
0 -> R.string.typing_blank
1 -> R.string.typing_one
in 2..4 -> R.string.typing_many
else -> R.string.typing_several
}
}
fun getTypingUsernames(): String {
return typingUsers.joinToString {
RevoltAPI.userCache[it]?.let { u ->
u.username ?: u.id
} ?: it
}
}
private fun regroupMessages(newMessages: List<MessageSchema> = renderableMessages) {
val groupedMessages = arrayListOf<MessageSchema>()
@ -430,6 +401,11 @@ fun ChannelScreen(
}
}
val scrollDownFABPadding by animateDpAsState(
if (viewModel.typingUsers.isNotEmpty()) 32.dp else 0.dp,
animationSpec = RevoltTweenDp
)
LaunchedEffect(channelId) {
viewModel.fetchChannel(channelId)
}
@ -488,7 +464,7 @@ fun ChannelScreen(
val isScrolledToBottom = remember(lazyListState) {
derivedStateOf {
lazyListState.firstVisibleItemIndex <= 5
lazyListState.firstVisibleItemIndex <= 6
}
}
@ -505,6 +481,10 @@ fun ChannelScreen(
contentAlignment = Alignment.BottomEnd
) {
LazyColumn(state = lazyListState, reverseLayout = true) {
item {
Spacer(modifier = Modifier.height(32.dp))
}
items(viewModel.renderableMessages) { message ->
Message(message)
}
@ -540,6 +520,7 @@ fun ChannelScreen(
) {
ExtendedFloatingActionButton(
modifier = Modifier
.padding(bottom = scrollDownFABPadding)
.align(Alignment.BottomEnd)
.padding(16.dp),
text = {
@ -560,34 +541,10 @@ fun ChannelScreen(
containerColor = MaterialTheme.colorScheme.primary
)
}
}
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
.background(MaterialTheme.colorScheme.surfaceVariant)
.fillMaxWidth()
.padding(all = 4.dp)
) {
Text(
text = stringResource(
id = viewModel.typingMessageResource(),
viewModel.getTypingUsernames()
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
TypingIndicator(
users = viewModel.typingUsers
)
}
AnimatedVisibility(visible = viewModel.attachments.isNotEmpty()) {
@ -599,8 +556,6 @@ fun ChannelScreen(
}
MessageField(
showButtons = viewModel.showButtons,
onToggleButtons = viewModel::setShowButtons,
messageContent = viewModel.messageContent,
onMessageContentChange = viewModel::setMessageContent,
onSendMessage = viewModel::sendPendingMessage,

View File

@ -1,6 +1,7 @@
package chat.revolt.screens.settings
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -12,9 +13,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -75,13 +78,25 @@ fun AppearanceSettingsScreen(
modifier = Modifier.padding(bottom = 10.dp)
)
Text(
text = "old revolt blue will come back soon i promise, needs a bit of optimisation first 🐈",
style = MaterialTheme.typography.headlineMedium.copy(
color = MaterialTheme.colorScheme.background,
fontWeight = FontWeight.Bold
),
modifier = Modifier
.padding(bottom = 10.dp)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary)
.padding(10.dp)
)
FlowRow(
mainAxisSpacing = 10.dp,
crossAxisSpacing = 10.dp,
) {
ThemeChip(
color = Color(0xff172333),
color = Color(0xff1e1e1e),
text = stringResource(id = R.string.settings_appearance_theme_revolt),
selected = GlobalState.theme == Theme.Revolt,
modifier = Modifier.weight(1f),
@ -108,7 +123,7 @@ fun AppearanceSettingsScreen(
}
ThemeChip(
color = if (isSystemInDarkTheme()) Color(0xff172333) else Color(0xfff7f7f7),
color = if (isSystemInDarkTheme()) Color(0xff1e1e1e) else Color(0xfff7f7f7),
text = stringResource(id = R.string.settings_appearance_theme_none),
selected = GlobalState.theme == Theme.None,
modifier = Modifier.weight(1f),

View File

@ -14,18 +14,17 @@ import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
val RevoltColorScheme = darkColorScheme(
primary = Color(0xfffe4654),
primary = Color(0xffda4e5b),
onPrimary = Color(0xffffffff),
secondary = Color(0xfffd6671),
secondary = Color(0xffe96a7a),
onSecondary = Color(0xffffffff),
tertiary = Color(0xffff6667),
onTertiary = Color(0xffffffff),
background = Color(0xff101823),
background = Color(0xff121212),
onBackground = Color(0xffffffff),
surfaceVariant = Color(0xff172333),
surfaceVariant = Color(0xff1e1e1e),
onSurfaceVariant = Color(0xffffffff),
surface = Color(0xff111a26),
surface = Color(0xff2b2b2b),
onSurface = Color(0xffffffff),
surfaceTint = Color(0xffc0c0c0),
)
val AmoledColorScheme = RevoltColorScheme.copy(
@ -38,18 +37,17 @@ val AmoledColorScheme = RevoltColorScheme.copy(
)
val LightColorScheme = lightColorScheme(
primary = Color(0xfffe4654),
primary = Color(0xffda4e5b),
onPrimary = Color(0xffffffff),
secondary = Color(0xfffd6671),
secondary = Color(0xffe96a7a),
onSecondary = Color(0xffffffff),
tertiary = Color(0xffff6667),
onTertiary = Color(0xffffffff),
background = Color(0xffffffff),
onBackground = Color(0xff000000),
surfaceVariant = Color(0xffe6e6e6),
surfaceVariant = Color(0xffe0e0e0),
onSurfaceVariant = Color(0xff000000),
surface = Color(0xffdddddd),
surface = Color(0xfff5f5f5),
onSurface = Color(0xff000000),
surfaceTint = Color(0xff000000),
)
enum class Theme {