search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars

This commit is contained in:
Avently
2024-10-18 23:34:12 +07:00
parent e8b83f946c
commit fb593de335
10 changed files with 336 additions and 225 deletions
@@ -8,6 +8,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Dp
import chat.simplex.common.views.chatlist.NavigationBarBackground
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.filter
@@ -23,23 +24,37 @@ actual fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarHeight: State<Dp>?,
content: LazyListScope.() -> Unit
) {
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
val connection = LocalAppBarHandler.current?.connection
LaunchedEffect(Unit) {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (reverseLayout) {
// always show app bar in reverse layout
connection?.appBarOffset = -1000f
} else if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
if (reverseLayout) {
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null) {
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
} else {
// show always when last item is not visible
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
} else {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && (offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
}
if (connection != null) {
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) {
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
@@ -21,6 +22,7 @@ expect fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
additionalBarHeight: State<Dp>? = null,
content: LazyListScope.() -> Unit
)
@@ -9,12 +9,13 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
@@ -23,7 +24,9 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.NavigationBarBackground
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@Composable
@@ -76,14 +79,17 @@ fun TerminalLayout(
composeState.value = composeState.value.copy(message = s)
}
Box(Modifier.fillMaxSize()) {
TerminalLog(floating)
val composeViewHeight = remember { mutableStateOf(0.dp) }
TerminalLog(floating, composeViewHeight)
NavigationBarBackground()
val density = LocalDensity.current
Column(
Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.imePadding()
.background(MaterialTheme.colors.background.copy(remember { appPrefs.barsAlpha.state }.value))
.onSizeChanged { composeViewHeight.value = with(density) { it.height.toDp() } }
) {
Divider()
Box(Modifier.padding(horizontal = 8.dp)) {
@@ -116,17 +122,18 @@ fun TerminalLayout(
}
@Composable
fun TerminalLog(floating: Boolean) {
fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
val reversedTerminalItems by remember {
derivedStateOf { chatModel.terminalItems.value.asReversed() }
}
val clipboard = LocalClipboardManager.current
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
LaunchedEffect(Unit) {
var autoScrollToBottom = listState.firstVisibleItemIndex == 0
var autoScrollToBottom = listState.firstVisibleItemIndex <= 1
launch {
snapshotFlow { listState.layoutInfo.totalItemsCount }
snapshotFlow { listState.layoutInfo.totalItemsCount to composeViewHeight.value }
.filter { autoScrollToBottom }
.onEach { delay(100) }
.collect {
try {
listState.scrollToItem(0)
@@ -137,19 +144,21 @@ fun TerminalLog(floating: Boolean) {
}
launch {
snapshotFlow { listState.firstVisibleItemIndex }
.onEach { delay(100) }
.collect {
autoScrollToBottom = listState.firstVisibleItemIndex == 0
autoScrollToBottom = it == 0
}
}
}
LazyColumnWithScrollBar (
reverseLayout = true,
contentPadding = PaddingValues(
top = topPaddingToContent(),
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier
),
state = listState
contentPadding = PaddingValues(top = topPaddingToContent()),
state = listState,
additionalBarHeight = composeViewHeight
) {
item {
Spacer(Modifier.imePadding().navigationBarsPadding().padding(bottom = composeViewHeight.value))
}
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId "
@@ -10,8 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.*
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.*
@@ -646,26 +645,24 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>) {
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
val topPaddingToContent = topPaddingToContent()
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
LazyColumnWithScrollBar(
Modifier.fillMaxSize().then(if (!oneHandUI.value) Modifier.imePadding() else Modifier),
listState,
reverseLayout = oneHandUI.value
) {
item {
Spacer(Modifier.height(if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent))
}
item { Spacer(Modifier.height(blankSpaceSize)) }
stickyHeader {
Column(
Modifier
.zIndex(1f)
.offset {
val y = if (searchText.value.text.isEmpty()) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
if (
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) {
if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset
else -offsetMultiplier * blankSpaceSize.roundToPx()
} else {
if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
listState.firstVisibleItemScrollOffset
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) {
0
@@ -674,13 +671,11 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>) {
} else {
offsetMultiplier * 1000
}
} else {
0
}
println("LALAL ${listState.firstVisibleItemIndex} ${listState.firstVisibleItemScrollOffset} ${listState.layoutInfo.beforeContentPadding}")
println("LALAL ${listState.firstVisibleItemIndex} ${listState.firstVisibleItemScrollOffset} ${listState.layoutInfo.beforeContentPadding} ${listState.layoutInfo.viewportStartOffset}")
IntOffset(0, y)
}
.background(MaterialTheme.colors.background),
.background(MaterialTheme.colors.background)
) {
if (oneHandUI.value) {
Divider()
@@ -715,7 +710,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>) {
}
}
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
}
}
@@ -9,7 +9,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.draw.*
@@ -17,9 +16,7 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@@ -50,7 +47,7 @@ fun CloseSheetBar(
.clickable(interactionSource = interactionSource, indication = null) { /* receive clicks to not allow to click through */ }
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
.drawWithCache {
val backgroundColor = if (connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(false, connection)) else Color.Transparent
val backgroundColor = if (arrangement == Arrangement.Bottom) themeBackgroundMix else if (connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(false, connection)) else Color.Transparent
onDrawBehind {
drawRect(backgroundColor)
}
@@ -12,8 +12,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.*
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
@@ -176,76 +175,124 @@ private fun ModalData.NewChatSheetLayout(
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
}
Box {
val topPaddingToContent = topPaddingToContent()
LazyColumnWithScrollBar(
Modifier.fillMaxSize().then(if (!oneHandUI.value) Modifier.imePadding() else Modifier),
listState,
contentPadding = PaddingValues(
top = if (!oneHandUI.value) topPaddingToContent else 0.dp,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
val actionButtonsOriginal = listOf(
Triple(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.add_contact_tab),
addContact,
),
reverseLayout = oneHandUI.value
) {
if (!oneHandUI.value) {
item {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.new_message),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
Triple(
painterResource(MR.images.ic_qr_code),
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
scanPaste,
),
Triple(
painterResource(MR.images.ic_group),
stringResource(MR.strings.create_group_button),
createGroup,
)
)
@Composable
fun DeletedChatsItem(actionButtons: List<Triple<Painter, String, () -> Unit>>) {
if (searchText.value.text.isEmpty()) {
Spacer(Modifier.padding(bottom = 27.dp))
}
if (searchText.value.text.isEmpty()) {
Row {
SectionView {
actionButtons.map {
NewChatButton(
icon = it.first,
text = it.second,
click = it.third,
)
}
}
}
if (deletedChats.isNotEmpty()) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
}
}
) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
}
}
}
}
@Composable
fun NoFilteredContactsItem() {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary
)
}
}
}
stickyHeader {
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 } }
Column(
Modifier
.offset {
val y = if (searchText.value.text.isEmpty()) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
}
if (
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
listState.firstVisibleItemScrollOffset
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) {
0
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) {
-listState.firstVisibleItemScrollOffset
@Composable
fun OneHandLazyColumn() {
val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
listState,
contentPadding = PaddingValues(bottom = blankSpaceSize),
reverseLayout = oneHandUI.value
) {
stickyHeader {
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
Column(
Modifier
.zIndex(1f)
.offset {
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx())
else -blankSpaceSize.roundToPx()
} else {
offsetMultiplier * 1000
if (listState.firstVisibleItemIndex == 0) {
listState.firstVisibleItemScrollOffset
} else {
1000
}
}
} else {
0
IntOffset(0, y)
}
IntOffset(0, y)
}
// show background when something is scrolled because otherwise the bar is transparent.
// not using background always because of gradient in SimpleX theme
.background(if (scrolledSomething && keyboardState == KeyboardState.Opened) {
MaterialTheme.colors.background
} else {
Color.Unspecified
}
)
) {
Divider()
if (!oneHandUI.value) {
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
// show background when something is scrolled because otherwise the bar is transparent.
// not using background always because of gradient in SimpleX theme
.background(
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
MaterialTheme.colors.background
} else {
Color.Unspecified
}
)
) {
Divider()
} else {
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
ContactsSearchBar(
listState = listState,
@@ -258,120 +305,125 @@ private fun ModalData.NewChatSheetLayout(
}
}
}
}
item {
if (searchText.value.text.isEmpty()) {
Spacer(Modifier.padding(bottom = 27.dp))
item {
DeletedChatsItem(actionButtonsOriginal.asReversed())
}
val actionButtonsOriginal = listOf(
Triple(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.add_contact_tab),
addContact,
),
Triple(
painterResource(MR.images.ic_qr_code),
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
scanPaste,
),
Triple(
painterResource(MR.images.ic_group),
stringResource(MR.strings.create_group_button),
createGroup,
)
)
val actionButtons by remember(oneHandUI.value) {
derivedStateOf {
if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal
}
}
if (searchText.value.text.isEmpty()) {
Row {
SectionView {
actionButtons.map {
NewChatButton(
icon = it.first,
text = it.second,
click = it.third,
)
}
}
}
if (deletedChats.isNotEmpty()) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
}
}
) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
}
}
}
}
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
if (!oneHandUI.value) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
} else {
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
}
}
}
item {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary
)
item {
NoFilteredContactsItem()
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
if (appPlatform.isAndroid) {
item {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
if (appPlatform.isAndroid) {
item {
Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
@Composable
fun NonOneHandLazyColumn() {
val blankSpaceSize = topPaddingToContent()
LazyColumnWithScrollBar(
Modifier.fillMaxSize().imePadding(),
listState,
reverseLayout = false
) {
item {
Box(Modifier.padding(top = blankSpaceSize)) {
AppBarTitle(
stringResource(MR.strings.new_message),
hostDevice(rh?.remoteHostId),
bottomPadding = DEFAULT_PADDING
)
}
}
stickyHeader {
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
Column(
Modifier
.zIndex(1f)
.offset {
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0)
else blankSpaceSize.roundToPx()
} else {
when (listState.firstVisibleItemIndex) {
0 -> 0
1 -> -listState.firstVisibleItemScrollOffset
else -> -1000
}
}
IntOffset(0, y)
}
// show background when something is scrolled because otherwise the bar is transparent.
// not using background always because of gradient in SimpleX theme
.background(
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
MaterialTheme.colors.background
} else {
Color.Unspecified
}
)
) {
Divider()
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Divider()
}
}
item {
DeletedChatsItem(actionButtonsOriginal)
}
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
}
}
item {
NoFilteredContactsItem()
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
if (appPlatform.isAndroid) {
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
}
Box {
if (oneHandUI.value) {
OneHandLazyColumn()
StatusBarBackground()
} else {
NonOneHandLazyColumn()
}
NavigationBarBackground()
}
@@ -198,10 +198,14 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height =
768.dp)
Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) {
val data = remember { ModalData() }
SimpleXTheme {
Column {
CloseSheetBar(close = { hiddenUntilRestart = true })
TerminalView(true)
CompositionLocalProvider(
LocalAppBarHandler provides data.appBarHandler
) {
ModalView({ hiddenUntilRestart = true }) {
TerminalView(true)
}
ModalManager.floatingTerminal.showInView()
DisposableEffect(Unit) {
onDispose {
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
@@ -31,6 +32,7 @@ actual fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarHeight: State<Dp>?,
content: LazyListScope.() -> Unit
) {
val scope = rememberCoroutineScope()
@@ -57,24 +59,56 @@ actual fun LazyColumnWithScrollBar(
// (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row)
val scrollBarDraggingState = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (reverseLayout) {
// always show app bar in reverse layout
connection?.appBarOffset = -1000f
} else if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
if (reverseLayout) {
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null) {
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
} else {
// show always when last item is not visible
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
} else {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1 || scrollBarDraggingState.value)) {
connection.appBarOffset = -scrollPosition.toFloat()
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
}
Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) {
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState)
}
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarHeight)
}
}
@Composable
private fun ScrollBar(
reverseLayout: Boolean,
state: LazyListState,
scrollBarAlpha: Animatable<Float, AnimationVector1D>,
scrollJob: MutableState<Job>,
scrollBarDraggingState: MutableState<Boolean>,
additionalBarHeight: State<Dp>?
) {
val padding = if (additionalBarHeight != null) {
PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value)
} else if (reverseLayout) {
PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier)
} else {
PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier)
}
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState)
}
}
@@ -133,7 +167,7 @@ actual fun ColumnWithScrollBar(
}
content()
}
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState)
}
}
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.*
@@ -95,7 +96,8 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }),
contentAlignment = Alignment.TopStart
) {
ColumnWithScrollBar(modifier) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
ColumnWithScrollBar(modifier.align(if (oneHandUI.value) Alignment.BottomCenter else Alignment.TopCenter)) {
content()
}
}
@@ -59,6 +59,7 @@ fun AppearanceScope.AppearanceLayout(
}
}
}
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
}
SectionDividerSpaced()
ThemesSection(systemDarkTheme)