mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-09 05:25:42 +00:00
search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars
This commit is contained in:
+26
-11
@@ -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) {
|
||||
|
||||
+2
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+21
-12
@@ -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 "
|
||||
|
||||
+13
-18
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -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)
|
||||
}
|
||||
|
||||
+213
-161
@@ -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 {
|
||||
|
||||
+49
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
ThemesSection(systemDarkTheme)
|
||||
|
||||
Reference in New Issue
Block a user