diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 0a3c696d4e..fcddfdbb10 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -613,12 +613,12 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow 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, 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, 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() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 9337cb8c2f..15828c7978 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -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) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt index ddfc4b74ab..03dca4e105 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt index fb575498c7..ea8f44d9f1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt @@ -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()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt index f00fa575e2..9a63535fbb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt @@ -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, setPerformLA: (Boolean) -> Unit) { Row( Modifier @@ -82,7 +88,8 @@ private fun RailUsers(userPickerState: MutableStateFlow) { .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) { .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( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 023aea530f..e237227f1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -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