adding swiping to start reply process

This commit is contained in:
Adam Brown 2022-09-30 16:07:38 +01:00
parent bce7a98546
commit 6989c1399a
3 changed files with 139 additions and 39 deletions

View File

@ -2,10 +2,17 @@ package app.dapk.st.messenger
import android.content.res.Configuration
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
@ -53,6 +60,7 @@ import app.dapk.st.navigator.Navigator
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
internal fun MessengerScreen(
@ -83,7 +91,9 @@ internal fun MessengerScreen(
})
when (state.composerState) {
is ComposerState.Text -> {
Room(state.roomState)
Room(state.roomState, onReply = {
viewModel.post(MessengerAction.ComposerEnterReplyMode(it))
})
TextComposer(
state.composerState,
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
@ -119,11 +129,11 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun
}
@Composable
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>) {
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, onReply: (RoomEvent) -> Unit) {
when (val state = roomStateLce) {
is Lce.Loading -> CenteredLoading()
is Lce.Content -> {
RoomContent(state.value.self, state.value.roomState)
RoomContent(state.value.self, state.value.roomState, onReply)
val eventBarHeight = 14.dp
val typing = state.value.typing
when {
@ -166,7 +176,7 @@ private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>) {
}
@Composable
private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
private fun ColumnScope.RoomContent(self: UserId, state: RoomState, onReply: (RoomEvent) -> Unit) {
val listState: LazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = 0
)
@ -192,7 +202,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
) { index, item ->
val previousEvent = if (index != 0) state.events[index - 1] else null
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
AlignedBubble(item, self, wasPreviousMessageSameSender) {
AlignedBubble(item, self, wasPreviousMessageSameSender, onReply) {
when (item) {
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
@ -215,6 +225,7 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
message: T,
self: UserId,
wasPreviousMessageSameSender: Boolean,
onReply: (RoomEvent) -> Unit,
content: @Composable (BubbleContent<T>) -> Unit
) {
when (message.author.id == self) {
@ -224,7 +235,8 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
Bubble(
message = message,
isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
wasPreviousMessageSameSender = wasPreviousMessageSameSender,
onReply = onReply,
) {
content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message))
}
@ -237,7 +249,8 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
Bubble(
message = message,
isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
wasPreviousMessageSameSender = wasPreviousMessageSameSender,
onReply = onReply,
) {
content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message))
}
@ -332,9 +345,39 @@ private fun Bubble(
message: RoomEvent,
isNotSelf: Boolean,
wasPreviousMessageSameSender: Boolean,
onReply: (RoomEvent) -> Unit,
content: @Composable () -> Unit
) {
Row(Modifier.padding(horizontal = 12.dp)) {
val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
val localDensity = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
val offsetX = remember { Animatable(0f) }
Row(
Modifier.padding(horizontal = 12.dp)
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState {
if ((offsetX.value + it) > 0) {
coroutineScope.launch { offsetX.snapTo(offsetX.value + it) }
}
},
onDragStopped = {
with(localDensity) {
if (offsetX.value > (screenWidthDp.toPx() * 0.15)) {
onReply(message)
}
}
coroutineScope.launch {
offsetX.animateTo(targetValue = 0f)
}
}
)
) {
when {
isNotSelf -> {
val displayImageSize = 32.dp
@ -583,36 +626,57 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
.fillMaxWidth()
.height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom
) {
Box(
Column(
modifier = Modifier
.align(Alignment.Bottom)
.weight(1f)
.fillMaxHeight()
.background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)),
contentAlignment = Alignment.TopStart,
) {
Box(Modifier.padding(14.dp)) {
if (state.value.isEmpty()) {
Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f))
}
BasicTextField(
modifier = Modifier.fillMaxWidth(),
value = state.value,
onValueChange = { onTextChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true),
decorationBox = { innerField ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() }
Icon(
modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom),
imageVector = Icons.Filled.Image,
contentDescription = "",
)
}
// AnimatedVisibility(
// visible = state.reply?.let { it is Message } ?: false,
// enter = slideInVertically { it - 50 },
// exit = slideOutVertically { it - 50 },
// ) {
//
// val message = state.reply as Message
// Column(
// modifier = Modifier
// .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
// ) {
// Text(message.author.displayName ?: message.author.id.value)
// Text(message.content)
// Spacer(Modifier.height(50.dp))
// }
// }
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)),
contentAlignment = Alignment.TopStart,
) {
Box(Modifier.padding(14.dp)) {
if (state.value.isEmpty()) {
Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f))
}
)
BasicTextField(
modifier = Modifier.fillMaxWidth(),
value = state.value,
onValueChange = { onTextChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true),
decorationBox = { innerField ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() }
Icon(
modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom),
imageVector = Icons.Filled.Image,
contentDescription = "",
)
}
}
)
}
}
}
Spacer(modifier = Modifier.width(6.dp))

View File

@ -2,6 +2,7 @@ package app.dapk.st.messenger
import app.dapk.st.core.Lce
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.navigator.MessageAttachment
data class MessengerScreenState(
@ -16,12 +17,16 @@ sealed interface MessengerEvent {
sealed interface ComposerState {
val reply: RoomEvent?
data class Text(
val value: String,
override val reply: RoomEvent?,
) : ComposerState
data class Attachments(
val values: List<MessageAttachment>,
override val reply: RoomEvent?,
) : ComposerState
}

View File

@ -34,7 +34,7 @@ internal class MessengerViewModel(
initialState = MessengerScreenState(
roomId = null,
roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "")
composerState = ComposerState.Text(value = "", reply = null)
),
factory = factory,
) {
@ -45,15 +45,40 @@ internal class MessengerViewModel(
when (action) {
is MessengerAction.OnMessengerVisible -> start(action)
MessengerAction.OnMessengerGone -> syncJob?.cancel()
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) }
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue, composerState.reply)) }
MessengerAction.ComposerSendText -> sendMessage()
MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) }
is MessengerAction.ComposerImageUpdate -> updateState { copy(composerState = ComposerState.Attachments(listOf(action.newValue))) }
MessengerAction.ComposerClear -> resetComposer()
is MessengerAction.ComposerImageUpdate -> updateState {
copy(
composerState = ComposerState.Attachments(
listOf(action.newValue),
composerState.reply
)
)
}
is MessengerAction.ComposerEnterReplyMode -> updateState {
copy(
composerState = when (composerState) {
is ComposerState.Attachments -> composerState.copy(reply = action.replyingTo)
is ComposerState.Text -> composerState.copy(reply = action.replyingTo)
}
)
}
MessengerAction.ComposerExitReplyMode -> updateState {
copy(
composerState = when (composerState) {
is ComposerState.Attachments -> composerState.copy(reply = null)
is ComposerState.Text -> composerState.copy(reply = null)
}
)
}
}
}
private fun start(action: MessengerAction.OnMessengerVisible) {
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it) } ?: composerState) }
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
syncJob = viewModelScope.launch {
roomStore.markRead(action.roomId)
@ -104,7 +129,7 @@ internal class MessengerViewModel(
is ComposerState.Attachments -> {
val copy = composerState.copy()
updateState { copy(composerState = ComposerState.Text("")) }
resetComposer()
state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState
@ -125,6 +150,10 @@ internal class MessengerViewModel(
}
}
private fun resetComposer() {
updateState { copy(composerState = ComposerState.Text("", reply = null)) }
}
fun startAttachment() {
viewModelScope.launch {
_events.emit(MessengerEvent.SelectImageAttachment)
@ -141,6 +170,8 @@ private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roo
sealed interface MessengerAction {
data class ComposerTextUpdate(val newValue: String) : MessengerAction
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
object ComposerExitReplyMode : MessengerAction
data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction
object ComposerSendText : MessengerAction
object ComposerClear : MessengerAction