adding swiping to start reply process

This commit is contained in:
Adam Brown 2022-09-30 16:07:38 +01:00 committed by Adam Brown
parent 86a8865cd1
commit a919e1b966
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 android.content.res.Configuration
import androidx.activity.result.ActivityResultLauncher 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable 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.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -53,6 +60,7 @@ import app.dapk.st.navigator.Navigator
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable @Composable
internal fun MessengerScreen( internal fun MessengerScreen(
@ -83,7 +91,9 @@ internal fun MessengerScreen(
}) })
when (state.composerState) { when (state.composerState) {
is ComposerState.Text -> { is ComposerState.Text -> {
Room(state.roomState) Room(state.roomState, onReply = {
viewModel.post(MessengerAction.ComposerEnterReplyMode(it))
})
TextComposer( TextComposer(
state.composerState, state.composerState,
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
@ -119,11 +129,11 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun
} }
@Composable @Composable
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>) { private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, onReply: (RoomEvent) -> Unit) {
when (val state = roomStateLce) { when (val state = roomStateLce) {
is Lce.Loading -> CenteredLoading() is Lce.Loading -> CenteredLoading()
is Lce.Content -> { is Lce.Content -> {
RoomContent(state.value.self, state.value.roomState) RoomContent(state.value.self, state.value.roomState, onReply)
val eventBarHeight = 14.dp val eventBarHeight = 14.dp
val typing = state.value.typing val typing = state.value.typing
when { when {
@ -166,7 +176,7 @@ private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>) {
} }
@Composable @Composable
private fun ColumnScope.RoomContent(self: UserId, state: RoomState) { private fun ColumnScope.RoomContent(self: UserId, state: RoomState, onReply: (RoomEvent) -> Unit) {
val listState: LazyListState = rememberLazyListState( val listState: LazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = 0 initialFirstVisibleItemIndex = 0
) )
@ -192,7 +202,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
) { index, item -> ) { index, item ->
val previousEvent = if (index != 0) state.events[index - 1] else null val previousEvent = if (index != 0) state.events[index - 1] else null
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
AlignedBubble(item, self, wasPreviousMessageSameSender) { AlignedBubble(item, self, wasPreviousMessageSameSender, onReply) {
when (item) { when (item) {
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>) is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>) is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
@ -215,6 +225,7 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
message: T, message: T,
self: UserId, self: UserId,
wasPreviousMessageSameSender: Boolean, wasPreviousMessageSameSender: Boolean,
onReply: (RoomEvent) -> Unit,
content: @Composable (BubbleContent<T>) -> Unit content: @Composable (BubbleContent<T>) -> Unit
) { ) {
when (message.author.id == self) { when (message.author.id == self) {
@ -224,7 +235,8 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
Bubble( Bubble(
message = message, message = message,
isNotSelf = false, isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender,
onReply = onReply,
) { ) {
content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)) content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message))
} }
@ -237,7 +249,8 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
Bubble( Bubble(
message = message, message = message,
isNotSelf = true, isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender,
onReply = onReply,
) { ) {
content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)) content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message))
} }
@ -332,9 +345,39 @@ private fun Bubble(
message: RoomEvent, message: RoomEvent,
isNotSelf: Boolean, isNotSelf: Boolean,
wasPreviousMessageSameSender: Boolean, wasPreviousMessageSameSender: Boolean,
onReply: (RoomEvent) -> Unit,
content: @Composable () -> 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 { when {
isNotSelf -> { isNotSelf -> {
val displayImageSize = 32.dp val displayImageSize = 32.dp
@ -583,36 +626,57 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
.fillMaxWidth() .fillMaxWidth()
.height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom .height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom
) { ) {
Box( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.Bottom)
.weight(1f) .weight(1f)
.fillMaxHeight() .fillMaxHeight()
.background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)),
contentAlignment = Alignment.TopStart,
) { ) {
Box(Modifier.padding(14.dp)) { // AnimatedVisibility(
if (state.value.isEmpty()) { // visible = state.reply?.let { it is Message } ?: false,
Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) // enter = slideInVertically { it - 50 },
} // exit = slideOutVertically { it - 50 },
BasicTextField( // ) {
modifier = Modifier.fillMaxWidth(), //
value = state.value, // val message = state.reply as Message
onValueChange = { onTextChange(it) }, // Column(
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), // modifier = Modifier
textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), // .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), // ) {
decorationBox = { innerField -> // Text(message.author.displayName ?: message.author.id.value)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { // Text(message.content)
Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() } // Spacer(Modifier.height(50.dp))
Icon( // }
modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), // }
imageVector = Icons.Filled.Image, Box(
contentDescription = "", 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)) 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.core.Lce
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.MessageAttachment
data class MessengerScreenState( data class MessengerScreenState(
@ -16,12 +17,16 @@ sealed interface MessengerEvent {
sealed interface ComposerState { sealed interface ComposerState {
val reply: RoomEvent?
data class Text( data class Text(
val value: String, val value: String,
override val reply: RoomEvent?,
) : ComposerState ) : ComposerState
data class Attachments( data class Attachments(
val values: List<MessageAttachment>, val values: List<MessageAttachment>,
override val reply: RoomEvent?,
) : ComposerState ) : ComposerState
} }

View File

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