mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 16:25:57 +00:00
android, desktop: enhancements to floating buttons (#5763)
* android, desktop: enhancements to floating buttons * size * size
This commit is contained in:
committed by
GitHub
parent
6b75f61537
commit
cd20dc0a04
@@ -1185,6 +1185,22 @@ fun BoxScope.ChatItemsList(
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val loadingTopItems = remember { mutableStateOf(false) }
|
||||
val loadingBottomItems = remember { mutableStateOf(false) }
|
||||
// just for changing local var here based on request
|
||||
val loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit = { chatId, pagination, visibleItemIndexesNonReversed ->
|
||||
val loadingSide = when (pagination) {
|
||||
is ChatPagination.Before -> loadingTopItems
|
||||
is ChatPagination.Last -> loadingBottomItems
|
||||
is ChatPagination.After, is ChatPagination.Around, is ChatPagination.Initial -> null
|
||||
}
|
||||
loadingSide?.value = true
|
||||
try {
|
||||
loadMessages(chatId, pagination, visibleItemIndexesNonReversed)
|
||||
} finally {
|
||||
loadingSide?.value = false
|
||||
}
|
||||
}
|
||||
val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } }
|
||||
val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } }
|
||||
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
|
||||
@@ -1582,7 +1598,25 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingButtons(reversedChatItems, chatInfoUpdated, topPaddingToContent, topPaddingToContentPx, contentTag, loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState, loadMessages)
|
||||
FloatingButtons(
|
||||
reversedChatItems,
|
||||
chatInfoUpdated,
|
||||
topPaddingToContent,
|
||||
topPaddingToContentPx,
|
||||
contentTag,
|
||||
loadingMoreItems,
|
||||
loadingTopItems,
|
||||
loadingBottomItems,
|
||||
animatedScrollingInProgress,
|
||||
mergedItems,
|
||||
unreadCount,
|
||||
maxHeight,
|
||||
composeViewHeight,
|
||||
searchValue,
|
||||
markChatRead,
|
||||
listState,
|
||||
loadMessages
|
||||
)
|
||||
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -1607,14 +1641,17 @@ private suspend fun loadLastItems(chatId: State<ChatId>, contentTag: MsgContentT
|
||||
val itemsCanCoverScreen = lastVisible != null && listState.value.layoutInfo.viewportEndOffset - listState.value.layoutInfo.afterContentPadding <= lastVisible.offset + lastVisible.size
|
||||
if (!itemsCanCoverScreen) return
|
||||
|
||||
val chatState = chatModel.chatStateForContent(contentTag)
|
||||
val lastItemsLoaded = chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatModel.chatItemsForContent(contentTag).value.lastOrNull()?.id
|
||||
if (lastItemsLoaded) return
|
||||
if (lastItemsLoaded(contentTag)) return
|
||||
|
||||
delay(500)
|
||||
loadItems.value(chatId.value, ChatPagination.Last(ChatPagination.INITIAL_COUNT))
|
||||
}
|
||||
|
||||
private fun lastItemsLoaded(contentTag: MsgContentTag?): Boolean {
|
||||
val chatState = chatModel.chatStateForContent(contentTag)
|
||||
return chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatModel.chatItemsForContent(contentTag).value.lastOrNull()?.id
|
||||
}
|
||||
|
||||
// TODO: in extra rare case when after loading last items only 1 item is loaded, the view will jump like when receiving new message
|
||||
// can be reproduced by forwarding a message to notes that is (ChatPagination.INITIAL_COUNT - 1) away from bottom and going to that message
|
||||
@Composable
|
||||
@@ -1681,6 +1718,8 @@ fun BoxScope.FloatingButtons(
|
||||
topPaddingToContentPx: State<Int>,
|
||||
contentTag: MsgContentTag?,
|
||||
loadingMoreItems: MutableState<Boolean>,
|
||||
loadingTopItems: MutableState<Boolean>,
|
||||
loadingBottomItems: MutableState<Boolean>,
|
||||
animatedScrollingInProgress: MutableState<Boolean>,
|
||||
mergedItems: State<MergedItems>,
|
||||
unreadCount: State<Int>,
|
||||
@@ -1692,6 +1731,40 @@ fun BoxScope.FloatingButtons(
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
fun scrollToBottom() {
|
||||
scope.launch {
|
||||
animatedScrollingInProgress.value = true
|
||||
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) }
|
||||
}
|
||||
}
|
||||
fun scrollToTopUnread() {
|
||||
scope.launch {
|
||||
tryBlockAndSetLoadingMore(loadingMoreItems) {
|
||||
if (chatModel.chatStateForContent(contentTag).splits.value.isNotEmpty()) {
|
||||
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
val oldSize = reversedChatItems.value.size
|
||||
loadMessages(chatInfo.value.id, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
|
||||
}
|
||||
var repeatsLeft = 100
|
||||
while (oldSize == reversedChatItems.value.size && repeatsLeft > 0) {
|
||||
delay(10)
|
||||
repeatsLeft--
|
||||
}
|
||||
if (oldSize == reversedChatItems.value.size) {
|
||||
return@tryBlockAndSetLoadingMore
|
||||
}
|
||||
}
|
||||
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
|
||||
if (index != -1) {
|
||||
// scroll to the top unread item
|
||||
animatedScrollingInProgress.value = true
|
||||
listState.value.animateScrollToItem(index + 1, -maxHeight.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bottomUnreadCount = remember {
|
||||
derivedStateOf {
|
||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||
@@ -1717,19 +1790,48 @@ fun BoxScope.FloatingButtons(
|
||||
allowToShowBottomWithArrow.value = shouldShow
|
||||
shouldShow && allow
|
||||
} }
|
||||
|
||||
val requestedTopScroll = remember { mutableStateOf(false) }
|
||||
val requestedBottomScroll = remember { mutableStateOf(false) }
|
||||
|
||||
BottomEndFloatingButton(
|
||||
bottomUnreadCount,
|
||||
showBottomButtonWithCounter,
|
||||
showBottomButtonWithArrow,
|
||||
requestedBottomScroll,
|
||||
animatedScrollingInProgress,
|
||||
composeViewHeight,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
animatedScrollingInProgress.value = true
|
||||
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) }
|
||||
if (loadingBottomItems.value || !lastItemsLoaded(contentTag)) {
|
||||
requestedTopScroll.value = false
|
||||
requestedBottomScroll.value = true
|
||||
} else {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { loadingTopItems.value }
|
||||
.drop(1)
|
||||
.collect { top ->
|
||||
if (!top && requestedTopScroll.value) {
|
||||
requestedTopScroll.value = false
|
||||
scrollToTopUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { loadingBottomItems.value }
|
||||
.drop(1)
|
||||
.collect { bottom ->
|
||||
if (!bottom && requestedBottomScroll.value) {
|
||||
requestedBottomScroll.value = false
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't show top FAB if is in search
|
||||
if (searchValue.value.isNotEmpty()) return
|
||||
val fabSize = 56.dp
|
||||
@@ -1741,33 +1843,15 @@ fun BoxScope.FloatingButtons(
|
||||
TopEndFloatingButton(
|
||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent).align(Alignment.TopEnd),
|
||||
topUnreadCount,
|
||||
requestedTopScroll,
|
||||
animatedScrollingInProgress,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
tryBlockAndSetLoadingMore(loadingMoreItems) {
|
||||
if (chatModel.chatStateForContent(contentTag).splits.value.isNotEmpty()) {
|
||||
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
val oldSize = reversedChatItems.value.size
|
||||
loadMessages(chatInfo.value.id, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
|
||||
}
|
||||
var repeatsLeft = 100
|
||||
while (oldSize == reversedChatItems.value.size && repeatsLeft > 0) {
|
||||
delay(10)
|
||||
repeatsLeft--
|
||||
}
|
||||
if (oldSize == reversedChatItems.value.size) {
|
||||
return@tryBlockAndSetLoadingMore
|
||||
}
|
||||
}
|
||||
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
|
||||
if (index != -1) {
|
||||
// scroll to the top unread item
|
||||
animatedScrollingInProgress.value = true
|
||||
listState.value.animateScrollToItem(index + 1, -maxHeight.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loadingTopItems.value) {
|
||||
requestedBottomScroll.value = false
|
||||
requestedTopScroll.value = true
|
||||
} else {
|
||||
scrollToTopUnread()
|
||||
}
|
||||
},
|
||||
onLongClick = { showDropDown.value = true }
|
||||
)
|
||||
@@ -1896,6 +1980,7 @@ fun MemberImage(member: GroupMember) {
|
||||
private fun TopEndFloatingButton(
|
||||
modifier: Modifier = Modifier,
|
||||
unreadCount: State<Int>,
|
||||
requestedTopScroll: State<Boolean>,
|
||||
animatedScrollingInProgress: State<Boolean>,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
@@ -1909,11 +1994,15 @@ private fun TopEndFloatingButton(
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
if (requestedTopScroll.value) {
|
||||
LoadingProgressIndicator()
|
||||
} else {
|
||||
Text(
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2281,39 +2370,50 @@ private fun BoxScope.BottomEndFloatingButton(
|
||||
unreadCount: State<Int>,
|
||||
showButtonWithCounter: State<Boolean>,
|
||||
showButtonWithArrow: State<Boolean>,
|
||||
requestedBottomScroll: State<Boolean>,
|
||||
animatedScrollingInProgress: State<Boolean>,
|
||||
composeViewHeight: State<Dp>,
|
||||
onClick: () -> Unit
|
||||
) = when {
|
||||
showButtonWithCounter.value && !animatedScrollingInProgress.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClick,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
) {
|
||||
when {
|
||||
showButtonWithCounter.value && !animatedScrollingInProgress.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClick,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
if (requestedBottomScroll.value) {
|
||||
LoadingProgressIndicator()
|
||||
} else {
|
||||
Text(
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showButtonWithArrow.value && !animatedScrollingInProgress.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClick,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
showButtonWithArrow.value && !animatedScrollingInProgress.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClick,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
if (requestedBottomScroll.value) {
|
||||
LoadingProgressIndicator()
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -2339,6 +2439,20 @@ fun SelectedListItem(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingProgressIndicator() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectUnselectChatItem(
|
||||
select: Boolean,
|
||||
ci: ChatItem,
|
||||
|
||||
Reference in New Issue
Block a user