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 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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue