desktop: fix rail badges, profile switching and lists

This commit is contained in:
sh committed 2026-10-14 07:43:26 +00:00
1 parent b267060f53
commit f5d4bc1cdc
6 files changed
+69 -29

No files matched your search

@@ -613,12 +613,12 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
DefaultAppBar(
navigationButton = {
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
NavigationButtonMenu {
if (!LocalFlatLayout.current) NavigationButtonMenu {
ModalManager.start.showModalCloseable(cardScreen = true) { close ->
SettingsView(chatModel, setPerformLA, close)
}
}
} else if (!LocalFlatLayout.current) {
} else if (!LocalFlatLayout.current || chatModel.desktopNoUserNoRemote) {
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
@@ -1021,7 +1021,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
// bottom toolbar: search bar below, so on desktop the connect row goes below the tags
TagsOrConnectByName(searchText, connectNameCandidate) { candidate ->
TagsView(searchText)
Divider()
if (!LocalFlatLayout.current) Divider()
ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null)
}
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate)
@@ -1032,7 +1032,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
// top toolbar: search bar above, so on desktop the connect row goes above the tags
TagsOrConnectByName(searchText, connectNameCandidate) { candidate ->
ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null)
Divider()
if (!LocalFlatLayout.current) Divider()
TagsView(searchText)
}
Divider()
@@ -84,12 +84,13 @@ fun ChatPreviewView(
@Composable
fun chatPreviewTitleText(color: Color = Color.Unspecified) {
val flat = LocalFlatLayout.current
Text(
cInfo.chatViewName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Bold,
style = if (flat) MaterialTheme.typography.body1 else MaterialTheme.typography.h3,
fontWeight = if (!flat) FontWeight.Bold else if (chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat) FontWeight.SemiBold else FontWeight.Normal,
color = color
)
}
@@ -466,14 +467,28 @@ fun ChatPreviewView(
}
if (LocalFlatLayout.current) {
LaunchedEffect(chatModel.deletedChats.value) {
val ci = chat.chatItems.lastOrNull()
val playing = AudioPlayer.currentlyPlaying.value
// stops a voice message of a deleted chat, as the voice preview effect does in the full row
if (ci != null && playing != null && playing.fileSource.filePath == ci.file?.fileSource?.filePath && chatModel.deletedChats.value.contains(chatModel.remoteHostId() to chat.id)) {
AudioPlayer.stop(ci)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
ChatInfoImage(cInfo, size = COMPACT_CHAT_IMAGE_SIZE * fontSizeSqrtMultiplier)
Box(contentAlignment = Alignment.BottomEnd) {
ChatInfoImage(cInfo, size = COMPACT_CHAT_IMAGE_SIZE * fontSizeSqrtMultiplier)
chatPreviewImageOverlayIcon()
}
Spacer(Modifier.width(8.dp))
Box(Modifier.weight(1f)) {
chatPreviewTitle()
}
Spacer(Modifier.width(8.sp.toDp()))
chatPreviewUnreadIndicator()
Box(Modifier.offset(y = -COMPACT_BADGE_Y_CORRECTION)) {
chatPreviewUnreadIndicator()
}
Spacer(Modifier.width(4.sp.toDp()))
chatStatusImage()
}
return
@@ -552,7 +567,9 @@ fun ChatPreviewView(
}
}
private val COMPACT_CHAT_IMAGE_SIZE = 32.dp
internal val COMPACT_CHAT_IMAGE_SIZE = 32.dp
// cancels the 3dp yOffset that UnreadBadge applies for the two-line row
private val COMPACT_BADGE_Y_CORRECTION = 3.dp
@Composable
private fun SmallContentPreview(borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f), content: @Composable BoxScope.() -> Unit) {
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.LocalFlatLayout
import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.PendingContactConnection
import chat.simplex.common.model.getTimestampText
@@ -22,8 +23,9 @@ import chat.simplex.res.*
@Composable
fun ContactConnectionView(contactConnection: PendingContactConnection) {
Row {
Box(Modifier.size(72.dp * fontSizeSqrtMultiplier), contentAlignment = Alignment.Center) {
ProfileImage(size = 54.dp * fontSizeSqrtMultiplier, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link)
val flat = LocalFlatLayout.current
Box(Modifier.size((if (flat) COMPACT_CHAT_IMAGE_SIZE else 72.dp) * fontSizeSqrtMultiplier), contentAlignment = Alignment.Center) {
ProfileImage(size = (if (flat) COMPACT_CHAT_IMAGE_SIZE else 54.dp) * fontSizeSqrtMultiplier, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link)
}
Column(
modifier = Modifier
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.LocalFlatLayout
import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.ChatInfo
import chat.simplex.common.model.getTimestampText
@@ -21,7 +22,7 @@ import chat.simplex.res.*
@Composable
fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) {
Row {
ChatInfoImage(contactRequest, size = 72.dp * fontSizeSqrtMultiplier)
ChatInfoImage(contactRequest, size = (if (LocalFlatLayout.current) COMPACT_CHAT_IMAGE_SIZE else 72.dp) * fontSizeSqrtMultiplier)
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.sp.toDp())
@@ -10,6 +10,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -32,9 +33,14 @@ val DESKTOP_RAIL_WIDTH = 64.dp
private val RAIL_ITEM_SIZE = 40.dp
private val RAIL_ICON_SIZE = 22.dp
private val RAIL_ACTIVE_MARK_WIDTH = 4.dp
private val RAIL_ACTIVE_BACKGROUND_WIDTH = 40.dp
private val RAIL_ACTIVE_BACKGROUND_HEIGHT = 32.dp
private val RAIL_UNREAD_DOT_SIZE = 6.dp
private const val RAIL_ACTIVE_BACKGROUND_ALPHA = 0.12f
private const val RAIL_BACKGROUND_ALPHA = 0.04f
@Composable
// Spec: spec/client/chat-list.md#DesktopRail
fun DesktopRail(userPickerState: MutableStateFlow<AnimatedViewState>, setPerformLA: (Boolean) -> Unit) {
Row(
Modifier
@@ -82,7 +88,8 @@ private fun RailUsers(userPickerState: MutableStateFlow<AnimatedViewState>) {
.sortedByDescending { it.user.activeOrder }
}
}
val enabled = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote
val switchAllowed = profileSwitchAllowed(remember { chatModel.sharedContent }.value) && !remember { chatModel.switchingUsersAndHosts }.value
val switchEnabled = switchAllowed && (remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote)
users.forEach { u ->
val user = u.user
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -94,19 +101,22 @@ private fun RailUsers(userPickerState: MutableStateFlow<AnimatedViewState>) {
.background(MaterialTheme.colors.primary, RoundedCornerShape(topEnd = RAIL_ACTIVE_MARK_WIDTH, bottomEnd = RAIL_ACTIVE_MARK_WIDTH))
)
}
Box(
Modifier
.clip(CircleShape)
.clickable(enabled = enabled) {
if (user.activeUser) {
ModalManager.start.closeModals()
userPickerState.value = AnimatedViewState.VISIBLE
} else {
switchToUser(user)
Box {
Box(
Modifier
.clip(CircleShape)
.clickable(enabled = if (user.activeUser) switchAllowed else switchEnabled) {
if (user.activeUser) {
ModalManager.start.closeModals()
userPickerState.value = AnimatedViewState.VISIBLE
} else {
if (userPickerState.value.isVisible()) userPickerState.value = AnimatedViewState.HIDING
switchToUser(user)
}
}
}
) {
ProfileImage(RAIL_ITEM_SIZE * fontSizeSqrtMultiplier, user.profile.image)
) {
ProfileImage(RAIL_ITEM_SIZE * fontSizeSqrtMultiplier, user.profile.image)
}
if (!user.activeUser && u.unreadCount > 0) {
userUnreadBadge(u.unreadCount, user.showNtfs, hasPadding = false)
}
@@ -124,6 +134,7 @@ private fun RailTags() {
val rhId = chatModel.remoteHostId()
RailItem(MR.images.ic_menu, stringResource(MR.strings.chat_list_all), active = activeFilter.value == null) {
ModalManager.start.closeModals()
activeFilter.value = null
}
PresetTagKind.entries.filter { (presetTags[it] ?: 0) > 0 }.forEach { tag ->
@@ -159,6 +170,7 @@ private fun RailTags() {
}
private fun toggleFilter(filter: ActiveFilter) {
ModalManager.start.closeModals()
chatModel.activeChatTagFilter.value = if (chatModel.activeChatTagFilter.value == filter) null else filter
}
@@ -182,14 +194,19 @@ private fun RailItem(
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(contentAlignment = Alignment.Center) {
Box(
Modifier
.size(RAIL_ACTIVE_BACKGROUND_WIDTH * fontSizeSqrtMultiplier, RAIL_ACTIVE_BACKGROUND_HEIGHT * fontSizeSqrtMultiplier)
.background(if (active) MaterialTheme.colors.primary.copy(alpha = RAIL_ACTIVE_BACKGROUND_ALPHA) else Color.Transparent, RoundedCornerShape(DEFAULT_PADDING_HALF)),
contentAlignment = Alignment.Center
) {
if (emoji != null) {
ReactionIcon(emoji, fontSize = 18.sp)
} else {
Icon(painterResource(icon), label, Modifier.size(RAIL_ICON_SIZE), tint = color)
Icon(painterResource(icon), label, Modifier.size(RAIL_ICON_SIZE * fontSizeSqrtMultiplier), tint = color)
}
if (unread) {
Box(Modifier.align(Alignment.TopEnd).size(6.dp).background(MaterialTheme.colors.primary, CircleShape))
Box(Modifier.align(Alignment.TopEnd).padding(DEFAULT_PADDING_HALF / 2).size(RAIL_UNREAD_DOT_SIZE).background(MaterialTheme.colors.primary, CircleShape))
}
}
Text(
@@ -102,7 +102,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
val sharedContent = remember { chatModel.sharedContent }.value
when {
showSearch -> NavigationButtonBack(hideSearchOnBack)
(users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && sharedContent !is SharedContent.Forward && sharedContent !is SharedContent.ChatLink && sharedContent !is SharedContent.MyAddress -> {
(users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && profileSwitchAllowed(sharedContent) -> {
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
@@ -221,3 +221,6 @@ private fun ShareList(
}
}
}
fun profileSwitchAllowed(sharedContent: SharedContent?): Boolean =
sharedContent !is SharedContent.Forward && sharedContent !is SharedContent.ChatLink && sharedContent !is SharedContent.MyAddress