Floating button with unread counter and go to bottom action (#929)

* Floating buttons with unread counters and go to bottom action

* Fixed marking read of long chats without preloaded messages

* Apply suggestions from code review

* Counters fix

* update button size/color

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2022-08-14 00:00:26 +03:00
committed by GitHub
parent aac80dacf7
commit 5d8d636adc
5 changed files with 251 additions and 39 deletions

View File

@@ -211,7 +211,7 @@ class ChatModel(val controller: ChatController) {
} }
} }
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null) { fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
val markedRead = markItemsReadInCurrentChat(cInfo, range) val markedRead = markItemsReadInCurrentChat(cInfo, range)
// update preview // update preview
val chatIdx = getChatIndex(cInfo.id) val chatIdx = getChatIndex(cInfo.id)
@@ -221,7 +221,7 @@ class ChatModel(val controller: ChatController) {
if (lastId != null) { if (lastId != null) {
chats[chatIdx] = chat.copy( chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy( chatStats = chat.chatStats.copy(
unreadCount = if (range != null) chat.chatStats.unreadCount - markedRead else 0, unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0,
// Can't use minUnreadItemId currently since chat items can have unread items between read items // Can't use minUnreadItemId currently since chat items can have unread items between read items
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
) )

View File

@@ -1279,8 +1279,8 @@ sealed class ChatPagination {
companion object { companion object {
const val INITIAL_COUNT = 100 const val INITIAL_COUNT = 100
const val PRELOAD_COUNT = 20 const val PRELOAD_COUNT = 100
const val UNTIL_PRELOAD_COUNT = 10 const val UNTIL_PRELOAD_COUNT = 50
} }
} }

View File

@@ -4,12 +4,14 @@ import android.content.res.Configuration
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
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
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.mapSaver
@@ -24,8 +26,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
@@ -57,9 +58,18 @@ fun ChatView(chatModel: ChatModel) {
val chat = activeChat!! val chat = activeChat!!
BackHandler { chatModel.chatId.value = null } BackHandler { chatModel.chatId.value = null }
// We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags)
val unreadCount = remember {
derivedStateOf {
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
}
}
ChatLayout( ChatLayout(
user, user,
chat, chat,
unreadCount,
composeState, composeState,
composeView = { composeView = {
if (chat.chatInfo.sendMsgEnabled) { if (chat.chatInfo.sendMsgEnabled) {
@@ -167,8 +177,8 @@ fun ChatView(chatModel: ChatModel) {
} }
} }
}, },
markRead = { range -> markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range) chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
withApi { withApi {
chatModel.controller.apiChatRead( chatModel.controller.apiChatRead(
@@ -186,6 +196,7 @@ fun ChatView(chatModel: ChatModel) {
fun ChatLayout( fun ChatLayout(
user: User, user: User,
chat: Chat, chat: Chat,
unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
composeView: (@Composable () -> Unit), composeView: (@Composable () -> Unit),
attachmentOption: MutableState<AttachmentOption?>, attachmentOption: MutableState<AttachmentOption?>,
@@ -203,7 +214,7 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit, startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
addMembers: (GroupInfo) -> Unit, addMembers: (GroupInfo) -> Unit,
markRead: (CC.ItemRange) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
) { ) {
Surface( Surface(
Modifier Modifier
@@ -223,15 +234,19 @@ fun ChatLayout(
sheetState = attachmentBottomSheetState, sheetState = attachmentBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) }
Scaffold( Scaffold(
topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers) }, topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers) },
bottomBar = composeView, bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding() modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = floatingButton.value,
) { contentPadding -> ) { contentPadding ->
BoxWithConstraints(Modifier.padding(contentPadding)) { BoxWithConstraints(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, composeState, chatItems, ChatItemsList(
user, chat, unreadCount, composeState, chatItems,
useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, markRead) receiveFile, joinGroup, acceptCall, markRead, floatingButton
)
} }
} }
} }
@@ -337,6 +352,7 @@ val CIListStateSaver = run {
fun BoxWithConstraintsScope.ChatItemsList( fun BoxWithConstraintsScope.ChatItemsList(
user: User, user: User,
chat: Chat, chat: Chat,
unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
chatItems: List<ChatItem>, chatItems: List<ChatItem>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
@@ -346,28 +362,20 @@ fun BoxWithConstraintsScope.ChatItemsList(
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
markRead: (CC.ItemRange) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
floatingButton: MutableState<@Composable () -> Unit>
) { ) {
val firstVisibleOffset = -with(LocalDensity.current) { maxHeight.roundToPx() } val listState = rememberLazyListState()
// Places first unread message at the top of a screen
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0),
initialFirstVisibleItemScrollOffset = firstVisibleOffset
)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val cxt = LocalContext.current val cxt = LocalContext.current
// Prevent scrolling to bottom on orientation change // Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change
var shouldAutoScroll by rememberSaveable { mutableStateOf(true) } var shouldAutoScroll by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType) { LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType) {
val firstUnreadIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0) if (shouldAutoScroll && listState.firstVisibleItemIndex != 0) {
if (shouldAutoScroll && listState.firstVisibleItemIndex != firstUnreadIndex) { scope.launch { listState.scrollToItem(0) }
scope.launch {
// Places first unread message at the top of a screen after moving from group to direct chat
listState.scrollToItem(firstUnreadIndex, firstVisibleOffset)
}
} }
// Don't autoscroll next time until it will be needed // Don't autoscroll next time until it will be needed
shouldAutoScroll = false shouldAutoScroll = false
@@ -432,12 +440,89 @@ fun BoxWithConstraintsScope.ChatItemsList(
LaunchedEffect(cItem.id) { LaunchedEffect(cItem.id) {
scope.launch { scope.launch {
delay(750) delay(750)
markRead(CC.ItemRange(cItem.id, cItem.id)) markRead(CC.ItemRange(cItem.id, cItem.id), null)
} }
} }
} }
} }
} }
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, markRead, floatingButton, listState)
}
@Composable
fun BoxWithConstraintsScope.FloatingButtons(
chatItems: List<ChatItem>,
unreadCount: State<Int>,
minUnreadItemId: Long,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
floatingButton: MutableState<@Composable () -> Unit>,
listState: LazyListState
) {
val scope = rememberCoroutineScope()
val bottomUnreadCount by remember { derivedStateOf {
chatItems.subList(
chatItems.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex,
chatItems.size
).count { it.isRcvNew } }
}
val firstItemIsVisible by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) {
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible
floatingButton.value = bottomEndFloatingButton(
bottomUnreadCount,
showButtonWithCounter,
showButtonWithArrow,
onClickArrowDown = {
scope.launch { listState.animateScrollToItem(0) }
},
onClickCounter = {
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) }
}
)
}
val fabSize = 56.dp
val topUnreadCount by remember {
derivedStateOf { unreadCount.value - bottomUnreadCount }
}
val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() }
var showDropDown by remember { mutableStateOf(false) }
TopEndFloatingButton(
Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd),
topUnreadCount,
showButtonWithCounter,
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown = true }
)
DropdownMenu(
expanded = showDropDown,
onDismissRequest = { showDropDown = false },
Modifier.width(220.dp),
offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)
) {
DropdownMenuItem(
onClick = {
markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount
)
showDropDown = false
}
) {
Text(
generalGetString(R.string.mark_read),
maxLines = 1,
)
}
}
} }
@Composable @Composable
@@ -480,6 +565,75 @@ fun MemberImage(member: GroupMember) {
ProfileImage(38.dp, member.memberProfile.image) ProfileImage(38.dp, member.memberProfile.image)
} }
@Composable
private fun TopEndFloatingButton(
modifier: Modifier = Modifier,
unreadCount: Int,
showButtonWithCounter: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit
) = when {
showButtonWithCounter -> {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton(
{}, // no action here
modifier.size(48.dp),
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
interactionSource = interactionSource,
) {
Text(
unreadCountStr(unreadCount),
color = MaterialTheme.colors.primary,
fontSize = 14.sp,
)
}
}
else -> {
}
}
private fun bottomEndFloatingButton(
unreadCount: Int,
showButtonWithCounter: Boolean,
showButtonWithArrow: Boolean,
onClickArrowDown: () -> Unit,
onClickCounter: () -> Unit
): @Composable () -> Unit = when {
showButtonWithCounter -> {
{
FloatingActionButton(
onClick = onClickCounter,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp)
) {
Text(
unreadCountStr(unreadCount),
color = MaterialTheme.colors.primary,
fontSize = 14.sp,
)
}
}
}
showButtonWithArrow -> {
{
FloatingActionButton(
onClick = onClickArrowDown,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colors.primary
)
}
}
}
else -> {
{}
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Preview( @Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -507,6 +661,7 @@ fun PreviewChatLayout() {
6, CIDirection.DirectRcv(), Clock.System.now(), "hello" 6, CIDirection.DirectRcv(), Clock.System.now(), "hello"
) )
) )
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
ChatLayout( ChatLayout(
user = User.sampleData, user = User.sampleData,
chat = Chat( chat = Chat(
@@ -514,6 +669,7 @@ fun PreviewChatLayout() {
chatItems = chatItems, chatItems = chatItems,
chatStats = Chat.ChatStats() chatStats = Chat.ChatStats()
), ),
unreadCount = unreadCount,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
@@ -531,7 +687,7 @@ fun PreviewChatLayout() {
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
addMembers = { _ -> }, addMembers = { _ -> },
markRead = { _ -> }, markRead = { _, _ -> },
) )
} }
} }
@@ -558,6 +714,7 @@ fun PreviewGroupChatLayout() {
6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello" 6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
) )
) )
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
ChatLayout( ChatLayout(
user = User.sampleData, user = User.sampleData,
chat = Chat( chat = Chat(
@@ -565,6 +722,7 @@ fun PreviewGroupChatLayout() {
chatItems = chatItems, chatItems = chatItems,
chatStats = Chat.ChatStats() chatStats = Chat.ChatStats()
), ),
unreadCount = unreadCount,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
@@ -582,7 +740,7 @@ fun PreviewGroupChatLayout() {
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
addMembers = { _ -> }, addMembers = { _ -> },
markRead = { _ -> }, markRead = { _, _ -> },
) )
} }
} }

View File

@@ -140,7 +140,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation), unreadCountStr(n),
color = MaterialTheme.colors.onPrimary, color = MaterialTheme.colors.onPrimary,
fontSize = 11.sp, fontSize = 11.sp,
modifier = Modifier modifier = Modifier
@@ -163,6 +163,11 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
} }
} }
@Composable
fun unreadCountStr(n: Int): String {
return if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation)
}
@Composable @Composable
fun ChatStatusImage(chat: Chat) { fun ChatStatusImage(chat: Chat) {
val s = chat.serverInfo.networkStatus val s = chat.serverInfo.networkStatus

View File

@@ -16,7 +16,13 @@
package chat.simplex.app.views.helpers package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.interaction.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
@@ -31,18 +37,18 @@ import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.consumeDownChange import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.isOutOfBounds import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.positionChangeConsumed import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.coroutineScope import chat.simplex.app.TAG
import kotlinx.coroutines.launch import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
/** /**
* See original code here: [androidx.compose.foundation.gestures.detectTapGestures] * See original code here: [androidx.compose.foundation.gestures.detectTapGestures]
* */ * */
interface PressGestureScope: Density { interface PressGestureScope: Density {
suspend fun tryAwaitRelease(): Boolean suspend fun tryAwaitRelease(): Boolean
} }
@@ -67,7 +73,6 @@ suspend fun PointerInputScope.detectGesture(
if (onPress !== NoPressGesture) launch { if (onPress !== NoPressGesture) launch {
pressScope.onPress(down.position) pressScope.onPress(down.position)
} }
val longPressTimeout = onLongPress?.let { val longPressTimeout = onLongPress?.let {
viewConfiguration.longPressTimeoutMillis viewConfiguration.longPressTimeoutMillis
} ?: (Long.MAX_VALUE / 2) } ?: (Long.MAX_VALUE / 2)
@@ -81,7 +86,6 @@ suspend fun PointerInputScope.detectGesture(
} else { } else {
if (shouldConsume) if (shouldConsume)
upOrCancel.consumeDownChange() upOrCancel.consumeDownChange()
// If onLongPress event is needed, cancel short press event // If onLongPress event is needed, cancel short press event
if (onLongPress != null) if (onLongPress != null)
pressScope.cancel() pressScope.cancel()
@@ -138,7 +142,6 @@ suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange
) { ) {
return null return null
} }
val consumeCheck = awaitPointerEvent(PointerEventPass.Final) val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) { if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
return null return null
@@ -176,3 +179,49 @@ private class PressGestureScopeImpl(
return isCanceled return isCanceled
} }
} }
/**
* Captures click events and calls [onLongClick] or [onClick] when such even happens. Otherwise, does nothing.
* Apply [MutableInteractionSource] to any element that allows to pass it in (for example, in [Modifier.clickable]).
* Works in situations when using [Modifier.combinedClickable] doesn't work because external element overrides [Modifier.clickable]
* */
@Composable
fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit): MutableInteractionSource {
val interactionSource = remember { MutableInteractionSource() }
val longPressTimeoutMillis = LocalViewConfiguration.current.longPressTimeoutMillis
var topLevelInteraction: Interaction? by remember { mutableStateOf(null) }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
topLevelInteraction = interaction
}
}
LaunchedEffect(topLevelInteraction is PressInteraction.Press) {
if (topLevelInteraction !is PressInteraction.Press) return@LaunchedEffect
try {
withTimeout(longPressTimeoutMillis) {
while (isActive) {
delay(10)
when (topLevelInteraction) {
is PressInteraction.Press -> {}
is PressInteraction.Release -> {
onClick(); break
}
is PressInteraction.Cancel -> break
}
}
}
} catch (_: TimeoutCancellationException) {
// Long click happened
onLongClick()
} catch (ex: CancellationException) {
// Canceled coroutine + PressInteraction.Release == short click
if (topLevelInteraction is PressInteraction.Release)
onClick()
Log.e(TAG, ex.stackTraceToString())
} catch (ex: Exception) {
// Should never be called
Log.e(TAG, ex.stackTraceToString())
}
}
return interactionSource
}