adding swiping to start reply process
This commit is contained in:
parent
86a8865cd1
commit
a919e1b966
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue