Add long click support on timeline item

This commit is contained in:
ganfra
2022-11-21 12:42:42 +01:00
parent 784b4ec659
commit e293b0a203
12 changed files with 205 additions and 13 deletions

View File

@@ -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() }
),

View File

@@ -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()

View File

@@ -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) }
)
}
}
}

View File

@@ -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)
}

View File

@@ -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>
)

View File

@@ -6,7 +6,6 @@ import androidx.compose.runtime.Stable
data class MessagesItemReactionState(
val reactions: List<AggregatedReaction>
)
@Stable
data class AggregatedReaction(
val key: String,

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")