From ab0c320fcb9480015263fafd3cfcbac63e5589a8 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 1 Jan 2025 22:18:15 +0000 Subject: [PATCH] android, desktop: chat tags UX improvements (#5455) * show "all" in meny when any active filter or text enabled, reset search when all selected * show active preset filter as blue * label changes * edit, delete and change order via context menu * simplify filter logic to match and make sure active chat always present * notes preset * remove no longer needed code * reorder mode boolean, rememberSaveable * avoid glitch in dropdown menu animation * move dropdown menu to tagListview * tagsRow via actual/expect * current chat id always on top * avoid recompose * fix android * selected preset should be blue * show change list in context menu if chat already had tag * swap icons --------- Co-authored-by: Evgeny Poberezkin --- .../views/chatlist/ChatListView.android.kt | 13 + .../chat/simplex/common/model/ChatModel.kt | 8 +- .../views/chatlist/ChatListNavLinkView.kt | 4 +- .../common/views/chatlist/ChatListView.kt | 223 +++++++++--------- .../common/views/chatlist/TagListView.kt | 59 +++-- .../views/helpers/DefaultDropdownMenu.kt | 9 +- .../commonMain/resources/MR/base/strings.xml | 3 + .../resources/MR/images/ic_folder_closed.svg | 1 + .../MR/images/ic_folder_closed_filled.svg | 1 + .../views/chatlist/ChatListView.desktop.kt | 6 + 10 files changed, 178 insertions(+), 149 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index 7db39b7d3e..8c3b161a5c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -29,6 +29,19 @@ private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFF private val CALL_BOTTOM_ICON_OFFSET = (-15).dp private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET +@Composable +actual fun TagsRow(content: @Composable() (() -> Unit)) { + Row( + modifier = Modifier + .padding(horizontal = 14.dp) + .horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + content() + } +} + @Composable actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index c1a9971e9c..b7da45f2a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1354,7 +1354,13 @@ sealed class ChatInfo: SomeChat, NamedChat { is Group -> groupInfo.chatTags else -> null } -} + + val contactCard: Boolean + get() = when (this) { + is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active + else -> false + } + } @Serializable sealed class NetworkStatus { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index fd63c0d315..994d56d1fc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -355,14 +355,14 @@ fun TagListAction( ) { val userTags = remember { chatModel.userTags } ItemAction( - stringResource(MR.strings.list_menu), + stringResource(if (chat.chatInfo.chatTags.isNullOrEmpty()) MR.strings.add_to_list else MR.strings.change_list), painterResource(MR.images.ic_label), onClick = { ModalManager.start.showModalCloseable { close -> if (userTags.value.isEmpty()) { TagListEditor(rhId = chat.remoteHostId, chat = chat, close = close) } else { - TagListView(rhId = chat.remoteHostId, chat = chat, close = close) + TagListView(rhId = chat.remoteHostId, chat = chat, close = close, reorderMode = false) } } showMenu.value = false 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 4648ac5037..b4a381809d 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 @@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds -enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS } +enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES } sealed class ActiveFilter { data class PresetTag(val tag: PresetTagKind) : ActiveFilter() @@ -815,13 +815,13 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat if (oneHandUI.value) { Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { Divider() - TagsView() + TagsView(searchText) ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } else { ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - TagsView() + TagsView(searchText) Divider() } } @@ -925,25 +925,13 @@ private fun ChatListFeatureCards() { private val TAG_MIN_HEIGHT = 35.dp @Composable -private fun TagsView() { +private fun TagsView(searchText: MutableState) { val userTags = remember { chatModel.userTags } val presetTags = remember { chatModel.presetTags } val activeFilter = remember { chatModel.activeChatTagFilter } val unreadTags = remember { chatModel.unreadTags } val rhId = chatModel.remoteHostId() - fun showTagList() { - ModalManager.start.showCustomModal { close -> - val editMode = remember { stateGetOrPut("editMode") { false } } - ModalView(close, showClose = true, endButtons = { - TextButton(onClick = { editMode.value = !editMode.value }, modifier = Modifier.clip(shape = CircleShape)) { - Text(stringResource(if (editMode.value) MR.strings.cancel_verb else MR.strings.edit_verb)) - } - }) { - TagListView(rhId = rhId, close = close, editMode = editMode) - } - } - } val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) TagsRow { @@ -953,7 +941,7 @@ private fun TagsView() { ExpandedTagFilterView(tag) } } else { - CollapsedTagsFilterView() + CollapsedTagsFilterView(searchText) } } @@ -963,69 +951,75 @@ private fun TagsView() { else -> false } val interactionSource = remember { MutableInteractionSource() } - Row( - rowSizeModifier - .clip(shape = CircleShape) - .combinedClickable( - onClick = { - if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) { - chatModel.activeChatTagFilter.value = null - } else { - chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag) + val showMenu = rememberSaveable { mutableStateOf(false) } + val saving = remember { mutableStateOf(false) } + Box { + Row( + rowSizeModifier + .clip(shape = CircleShape) + .combinedClickable( + onClick = { + if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) { + chatModel.activeChatTagFilter.value = null + } else { + chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag) + } + }, + onLongClick = { showMenu.value = true }, + interactionSource = interactionSource, + indication = LocalIndication.current, + enabled = !saving.value + ) + .onRightClick { showMenu.value = true } + .padding(4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (tag.chatTagEmoji != null) { + ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp) + } else { + Icon( + painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label), + null, + Modifier.size(18.sp.toDp()), + tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + ) + } + Spacer(Modifier.width(4.dp)) + Box { + val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) " ●" else "" + val invisibleText = buildAnnotatedString { + append(tag.chatTagText) + withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) { + append(badgeText) } - }, - onLongClick = { showTagList() }, - interactionSource = interactionSource, - indication = LocalIndication.current - ) - .onRightClick { showTagList() } - .padding(4.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - if (tag.chatTagEmoji != null) { - ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp) - } else { - Icon( - painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label), - null, - Modifier.size(18.sp.toDp()), - tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground - ) - } - Spacer(Modifier.width(4.dp)) - Box { - val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) " ●" else "" - val invisibleText = buildAnnotatedString { - append(tag.chatTagText) - withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) { - append(badgeText) } - } - Text( - text = invisibleText, - fontWeight = FontWeight.Medium, - fontSize = 15.sp, - color = Color.Transparent, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - // Visible text with styles - val visibleText = buildAnnotatedString { - append(tag.chatTagText) - withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) { - append(badgeText) + Text( + text = invisibleText, + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = Color.Transparent, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Visible text with styles + val visibleText = buildAnnotatedString { + append(tag.chatTagText) + withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) { + append(badgeText) + } } + Text( + text = visibleText, + fontWeight = if (current) FontWeight.Medium else FontWeight.Normal, + fontSize = 15.sp, + color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } - Text( - text = visibleText, - fontWeight = if (current) FontWeight.Medium else FontWeight.Normal, - fontSize = 15.sp, - color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) } + TagsDropdownMenu(rhId, tag, showMenu, saving) } } val plusClickModifier = Modifier @@ -1051,23 +1045,8 @@ private fun TagsView() { } } -@OptIn(ExperimentalLayoutApi::class) @Composable -private fun TagsRow(content: @Composable() (() -> Unit)) { - if (appPlatform.isAndroid) { - Row( - modifier = Modifier - .padding(horizontal = 14.dp) - .horizontalScroll(rememberScrollState()), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp) - ) { - content() - } - } else { - FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() } - } -} +expect fun TagsRow(content: @Composable() (() -> Unit)) @Composable private fun ExpandedTagFilterView(tag: PresetTagKind) { @@ -1076,12 +1055,12 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) { is ActiveFilter.PresetTag -> af.tag == tag else -> false } - val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) val (icon, text) = presetTagLabel(tag, active) val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Row( - modifier = rowSizeModifier + modifier = Modifier + .sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) .clip(shape = CircleShape) .clickable { if (activeFilter.value == ActiveFilter.PresetTag(tag)) { @@ -1121,7 +1100,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) { @Composable -private fun CollapsedTagsFilterView() { +private fun CollapsedTagsFilterView(searchText: MutableState) { val activeFilter = remember { chatModel.activeChatTagFilter } val presetTags = remember { chatModel.presetTags } val showMenu = remember { mutableStateOf(false) } @@ -1145,7 +1124,7 @@ private fun CollapsedTagsFilterView() { painterResource(icon), stringResource(text), Modifier.size(18.sp.toDp()), - tint = MaterialTheme.colors.secondary + tint = MaterialTheme.colors.primary ) } else { Icon( @@ -1155,20 +1134,26 @@ private fun CollapsedTagsFilterView() { ) } - DefaultDropdownMenu(showMenu = showMenu) { - if (selectedPresetTag != null) { + val onCloseMenuAction = remember { mutableStateOf<(() -> Unit)>({}) } + + DefaultDropdownMenu(showMenu = showMenu, onClosed = onCloseMenuAction) { + if (activeFilter.value != null || searchText.value.text.isNotBlank()) { ItemAction( stringResource(MR.strings.chat_list_all), painterResource(MR.images.ic_menu), onClick = { - chatModel.activeChatTagFilter.value = null + onCloseMenuAction.value = { + searchText.value = TextFieldValue() + chatModel.activeChatTagFilter.value = null + onCloseMenuAction.value = {} + } showMenu.value = false } ) } PresetTagKind.entries.forEach { tag -> if ((presetTags[tag] ?: 0) > 0) { - ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu) + ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction) } } } @@ -1179,14 +1164,19 @@ private fun CollapsedTagsFilterView() { fun ItemPresetFilterAction( presetTag: PresetTagKind, active: Boolean, - showMenu: MutableState + showMenu: MutableState, + onCloseMenuAction: MutableState<(() -> Unit)> ) { val (icon, text) = presetTagLabel(presetTag, active) ItemAction( stringResource(text), painterResource(icon), + color = if (active) MaterialTheme.colors.primary else Color.Unspecified, onClick = { - chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag) + onCloseMenuAction.value = { + chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag) + onCloseMenuAction.value = {} + } showMenu.value = false } ) @@ -1205,26 +1195,18 @@ fun filteredChats( } else { val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() if (s.isEmpty()) - chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD && filtered(chat, activeFilter) } + chats.filter { chat -> chat.id == chatModel.chatId.value || (!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat, activeFilter)) } else { chats.filter { chat -> - when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && ( - if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat, activeFilter) - } else { - cInfo.anyNameContains(s) - }) - is ChatInfo.Group -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat, activeFilter) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited - } else { - cInfo.anyNameContains(s) + chat.id == chatModel.chatId.value || + when (val cInfo = chat.chatInfo) { + is ChatInfo.Direct -> !cInfo.contact.chatDeleted && !chat.chatInfo.contactCard && cInfo.anyNameContains(s) + is ChatInfo.Group -> cInfo.anyNameContains(s) + is ChatInfo.Local -> cInfo.anyNameContains(s) + is ChatInfo.ContactRequest -> cInfo.anyNameContains(s) + is ChatInfo.ContactConnection -> cInfo.contactConnection.localAlias.lowercase().contains(s) + is ChatInfo.InvalidJSON -> false } - is ChatInfo.Local -> s.isEmpty() || cInfo.anyNameContains(s) - is ChatInfo.ContactRequest -> s.isEmpty() || cInfo.anyNameContains(s) - is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.anyNameContains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value) - is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value - } } } } @@ -1256,6 +1238,10 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean = is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business else -> false } + PresetTagKind.NOTES -> when (chatInfo) { + is ChatInfo.Local -> !chatInfo.noteFolder.chatDeleted + else -> false + } } private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair = @@ -1264,6 +1250,7 @@ private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses + PresetTagKind.NOTES -> (if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed) to MR.strings.chat_list_notes } fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index 2cd0c953c7..f8ddc16bde 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -5,8 +5,7 @@ import SectionDivider import SectionItemView import TextIconSpaced import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed @@ -44,12 +43,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: MutableState = remember { mutableStateOf(false) }) { - if (remember { editMode }.value) { - BackHandler { - editMode.value = false - } - } +fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) { val userTags = remember { chatModel.userTags } val oneHandUI = remember { appPrefs.oneHandUI.state } val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() @@ -77,7 +71,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu val topPaddingToContent = topPaddingToContent(false) LazyColumnWithScrollBar( - modifier = if (editMode.value) Modifier.dragContainer(dragDropState) else Modifier, + modifier = if (reorderMode) Modifier.dragContainer(dragDropState) else Modifier, contentPadding = PaddingValues( top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp @@ -97,7 +91,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu } } - if (oneHandUI.value && !editMode.value) { + if (oneHandUI.value && !reorderMode) { item { CreateList() } @@ -111,15 +105,14 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu backgroundColor = if (isDragging) colors.surface else Color.Unspecified ) { Column { - val showMenu = remember { mutableStateOf(false) } val selected = chatTagIds.value.contains(tag.chatTagId) Row( Modifier .fillMaxWidth() .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) - .combinedClickable( - enabled = !saving.value, + .clickable( + enabled = !saving.value && !reorderMode, onClick = { if (chat == null) { ModalManager.start.showModalCloseable { close -> @@ -139,13 +132,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu }) } }, - onLongClick = if (editMode.value) null else { - { showMenu.value = true } - }, - interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current ) - .onRightClick { showMenu.value = true } .padding(PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)), verticalAlignment = Alignment.CenterVertically ) { @@ -163,21 +150,17 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu if (selected) { Spacer(Modifier.weight(1f)) Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - } else if (editMode.value) { + } else if (reorderMode) { Spacer(Modifier.weight(1f)) Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } - DefaultDropdownMenu(showMenu, dropdownMenuItems = { - EditTagAction(rhId, tag, showMenu) - DeleteTagAction(rhId, tag, showMenu, saving) - }) } SectionDivider() } } } } - if (!oneHandUI.value && !editMode.value) { + if (!oneHandUI.value && !reorderMode) { item { CreateList() } @@ -279,7 +262,7 @@ fun ModalData.TagListEditor( SectionItemView(click = { if (tagId == null) createTag() else updateTag() }, disabled = disabled) { Text( - generalGetString(if (chat != null) MR.strings.add_to_list else if (tagId == null) MR.strings.create_list else MR.strings.save_list), + generalGetString(if (chat != null) MR.strings.add_to_list else MR.strings.save_list), color = if (disabled) colors.secondary else colors.primary ) } @@ -309,6 +292,15 @@ fun ModalData.TagListEditor( } } +@Composable +fun TagsDropdownMenu(rhId: Long?, tag: ChatTag, showMenu: MutableState, saving: MutableState) { + DefaultDropdownMenu(showMenu, dropdownMenuItems = { + EditTagAction(rhId, tag, showMenu) + DeleteTagAction(rhId, tag, showMenu, saving) + ChangeOrderTagAction(rhId, showMenu) + }) +} + @Composable private fun DeleteTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState, saving: MutableState) { ItemAction( @@ -343,6 +335,21 @@ private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState) { + ItemAction( + stringResource(MR.strings.change_order_chat_list_menu_action), + painterResource(MR.images.ic_drag_handle), + onClick = { + showMenu.value = false + ModalManager.start.showModalCloseable { close -> + TagListView(rhId = rhId, close = close, reorderMode = true) + } + }, + color = MenuTextColor + ) +} + @Composable expect fun ChatTagInput(name: MutableState, showError: State, emoji: MutableState) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt index 1f00af2809..c6a566c6f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt @@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -16,6 +15,7 @@ fun DefaultDropdownMenu( showMenu: MutableState, modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), + onClosed: State<() -> Unit> = remember { mutableStateOf({}) }, dropdownMenuItems: (@Composable () -> Unit)? ) { MaterialTheme( @@ -31,6 +31,11 @@ fun DefaultDropdownMenu( offset = offset, ) { dropdownMenuItems?.invoke() + DisposableEffect(Unit) { + onDispose { + onClosed.value() + } + } } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ffbe473df8..e45ba24822 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -421,6 +421,7 @@ Contacts Groups Businesses + Notes All Add list @@ -644,6 +645,7 @@ Create list Add to list + Change list Save list List name... List name and emoji should be different for all lists. @@ -651,6 +653,7 @@ Delete list? All chats will be removed from the list %s, and the list deleted Edit + Change order You invited a contact diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg new file mode 100644 index 0000000000..0f9889083d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg new file mode 100644 index 0000000000..6291f7ab8e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index 3fa78bbbb5..e295144191 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -21,6 +21,12 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +@OptIn(ExperimentalLayoutApi::class) +@Composable +actual fun TagsRow(content: @Composable() (() -> Unit)) { + FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() } +} + @Composable actual fun ActiveCallInteractiveArea(call: Call) { val showMenu = remember { mutableStateOf(false) }