Add long click support on timeline item
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
|
||||
import Avatar
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -12,14 +13,14 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.End
|
||||
import androidx.compose.ui.Alignment.Companion.Start
|
||||
@@ -49,6 +50,8 @@ import io.element.android.x.features.messages.model.content.MessagesTimelineItem
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
|
||||
import io.element.android.x.textcomposer.TextComposer
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private val BUBBLE_RADIUS = 16.dp
|
||||
private val COMPOSER_HEIGHT = 112.dp
|
||||
@@ -61,6 +64,10 @@ fun MessagesScreen(
|
||||
val viewModel: MessagesViewModel = mavericksViewModel(argsFactory = { roomId })
|
||||
val composerViewModel: MessageComposerViewModel = mavericksViewModel(argsFactory = { roomId })
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
val actionsSheetState = rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomTitle by viewModel.collectAsState(MessagesViewState::roomName)
|
||||
val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar)
|
||||
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
|
||||
@@ -84,6 +91,21 @@ fun MessagesScreen(
|
||||
onComposerTextChange = composerViewModel::updateText,
|
||||
composerCanSendMessage = composerCanSendMessage,
|
||||
composerText = composerText,
|
||||
onClick = {
|
||||
Timber.v("onClick on timeline item: ${it.id}")
|
||||
},
|
||||
onLongClick = {
|
||||
viewModel.computeActionsSheetState(it)
|
||||
coroutineScope.launch {
|
||||
actionsSheetState.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
val itemActionsSheetState by viewModel.collectAsState(prop1 = MessagesViewState::itemActionsSheetState)
|
||||
TimelineItemActionsScreen(
|
||||
sheetState = actionsSheetState,
|
||||
actionsSheetState = itemActionsSheetState(),
|
||||
onActionClicked = viewModel::handleItemAction
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,6 +118,8 @@ fun MessagesContent(
|
||||
onReachedLoadMore: () -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
onSendMessage: (String) -> Unit,
|
||||
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onLongClick: ((MessagesTimelineItemState.MessageEvent)) -> Unit,
|
||||
composerFullScreen: Boolean,
|
||||
onComposerFullScreenChange: () -> Unit,
|
||||
onComposerTextChange: (CharSequence) -> Unit,
|
||||
@@ -145,7 +169,9 @@ fun MessagesContent(
|
||||
timelineItems = timelineItems,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
onReachedLoadMore = onReachedLoadMore,
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
TextComposer(
|
||||
@@ -175,6 +201,8 @@ fun TimelineItems(
|
||||
lazyListState: LazyListState,
|
||||
timelineItems: List<MessagesTimelineItemState>,
|
||||
hasMoreToLoad: Boolean,
|
||||
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onLongClick: ((MessagesTimelineItemState.MessageEvent)) -> Unit,
|
||||
onReachedLoadMore: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -186,7 +214,11 @@ fun TimelineItems(
|
||||
reverseLayout = true
|
||||
) {
|
||||
itemsIndexed(timelineItems) { index, timelineItem ->
|
||||
TimelineItemRow(timelineItem = timelineItem)
|
||||
TimelineItemRow(
|
||||
timelineItem = timelineItem,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
if (hasMoreToLoad) {
|
||||
item {
|
||||
@@ -199,17 +231,25 @@ fun TimelineItems(
|
||||
|
||||
@Composable
|
||||
fun TimelineItemRow(
|
||||
timelineItem: MessagesTimelineItemState
|
||||
timelineItem: MessagesTimelineItemState,
|
||||
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
) {
|
||||
when (timelineItem) {
|
||||
is MessagesTimelineItemState.Virtual -> return
|
||||
is MessagesTimelineItemState.MessageEvent -> MessageEventRow(messageEvent = timelineItem)
|
||||
is MessagesTimelineItemState.MessageEvent -> MessageEventRow(
|
||||
messageEvent = timelineItem,
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageEventRow(
|
||||
messageEvent: MessagesTimelineItemState.MessageEvent,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (parentAlignment, contentAlignment) = if (messageEvent.isMine) {
|
||||
@@ -241,6 +281,8 @@ fun MessageEventRow(
|
||||
MessageEventBubble(
|
||||
groupPosition = messageEvent.groupPosition,
|
||||
isMine = messageEvent.isMine,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
) {
|
||||
@@ -304,11 +346,14 @@ private fun MessageSenderInformation(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageEventBubble(
|
||||
groupPosition: MessagesItemGroupPosition,
|
||||
isMine: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
fun bubbleShape(): Shape {
|
||||
@@ -360,8 +405,9 @@ fun MessageEventBubble(
|
||||
.widthIn(min = 80.dp)
|
||||
.offsetForItem()
|
||||
.clip(bubbleShape)
|
||||
.clickable(
|
||||
onClick = { },
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
|
||||
@@ -5,6 +5,9 @@ import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.x.features.messages.model.MessagesItemAction
|
||||
import io.element.android.x.features.messages.model.MessagesItemActionsSheetState
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.model.MessagesViewState
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.MatrixInstance
|
||||
@@ -16,6 +19,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private const val PAGINATION_COUNT = 50
|
||||
|
||||
@@ -66,6 +70,28 @@ class MessagesViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun handleItemAction(action: MessagesItemAction) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val currentState = awaitState()
|
||||
Timber.v("Handle $action for ${currentState.itemActionsSheetState}")
|
||||
}
|
||||
}
|
||||
|
||||
fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent) {
|
||||
suspend {
|
||||
val actions = listOf(
|
||||
MessagesItemAction.Forward,
|
||||
MessagesItemAction.Copy,
|
||||
)
|
||||
MessagesItemActionsSheetState(
|
||||
targetItem = messagesTimelineItemState,
|
||||
actions = actions
|
||||
)
|
||||
}.execute(Dispatchers.Default) {
|
||||
copy(itemActionsSheetState = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInit() {
|
||||
timeline.initialize()
|
||||
room.syncUpdateFlow()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.x.features.messages.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.x.designsystem.components.VectorIcon
|
||||
import io.element.android.x.features.messages.model.MessagesItemAction
|
||||
import io.element.android.x.features.messages.model.MessagesItemActionsSheetState
|
||||
|
||||
@Composable
|
||||
fun TimelineItemActionsScreen(
|
||||
sheetState: ModalBottomSheetState,
|
||||
actionsSheetState: MessagesItemActionsSheetState?,
|
||||
onActionClicked: (MessagesItemAction) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ModalBottomSheetLayout(
|
||||
modifier = modifier,
|
||||
sheetState = sheetState,
|
||||
sheetContent = {
|
||||
SheetContent(
|
||||
actionsSheetState = actionsSheetState,
|
||||
onActionClicked = onActionClicked
|
||||
)
|
||||
}
|
||||
) {}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
actionsSheetState: MessagesItemActionsSheetState?,
|
||||
onActionClicked: (MessagesItemAction) -> Unit,
|
||||
) {
|
||||
if (actionsSheetState == null || actionsSheetState.actions.isEmpty()) {
|
||||
// Crashes if sheetContent size is zero
|
||||
Box(modifier = Modifier.size(1.dp))
|
||||
return
|
||||
}
|
||||
LazyColumn {
|
||||
items(actionsSheetState.actions) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
onActionClicked(it)
|
||||
},
|
||||
text = { Text(it.title) },
|
||||
icon = { VectorIcon(it.icon) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.element.android.x.features.messages.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.x.designsystem.VectorIcons
|
||||
|
||||
@Stable
|
||||
sealed class MessagesItemAction(val title: String, @DrawableRes val icon: Int) {
|
||||
object Forward : MessagesItemAction("Forward", VectorIcons.ArrowForward)
|
||||
object Copy : MessagesItemAction("Copy", VectorIcons.Copy)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.element.android.x.features.messages.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
data class MessagesItemActionsSheetState(
|
||||
val targetItem: MessagesTimelineItemState.MessageEvent,
|
||||
val actions: List<MessagesItemAction>
|
||||
)
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.runtime.Stable
|
||||
data class MessagesItemReactionState(
|
||||
val reactions: List<AggregatedReaction>
|
||||
)
|
||||
|
||||
@Stable
|
||||
data class AggregatedReaction(
|
||||
val key: String,
|
||||
|
||||
@@ -11,6 +11,7 @@ data class MessagesViewState(
|
||||
val roomAvatar: AvatarData? = null,
|
||||
val timelineItems: Async<List<MessagesTimelineItemState>> = Uninitialized,
|
||||
val hasMoreToLoad: Boolean = true,
|
||||
val itemActionsSheetState: Async<MessagesItemActionsSheetState> = Uninitialized
|
||||
) : MavericksState {
|
||||
|
||||
@Suppress("unused")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.element.android.x.designsystem
|
||||
|
||||
import io.element.android.x.libraries.designsystem.R
|
||||
|
||||
object VectorIcons {
|
||||
val Copy = R.drawable.ic_content_copy
|
||||
val ArrowForward = R.drawable.ic_content_arrow_forward
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.element.android.x.designsystem.components
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
||||
@Composable
|
||||
fun VectorIcon(
|
||||
resourceId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = LocalContentColor.current,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = resourceId),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#000000" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
@@ -17,6 +17,7 @@ dependencies {
|
||||
implementation(platform("androidx.compose:compose-bom:2022.10.00"))
|
||||
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.material:material")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
|
||||
|
||||
Reference in New Issue
Block a user