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 <evgeny@poberezkin.com>
This commit is contained in:
Diogo
2025-01-01 22:18:15 +00:00
committed by GitHub
parent cab938b9f0
commit ab0c320fcb
10 changed files with 178 additions and 149 deletions

View File

@@ -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) }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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<TextFieldValue>, 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<TextFieldValue>) {
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<TextFieldValue>) {
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<Boolean>
showMenu: MutableState<Boolean>,
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<ImageResource, StringResource> =
@@ -1264,6 +1250,7 @@ private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResou
PresetTagKind.CONTACTS -> (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) {

View File

@@ -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<Boolean> = 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<Boolean>, saving: MutableState<Boolean>) {
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<Boolean>, saving: MutableState<Boolean>) {
ItemAction(
@@ -343,6 +335,21 @@ private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Bool
)
}
@Composable
private fun ChangeOrderTagAction(rhId: Long?, showMenu: MutableState<Boolean>) {
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<String>, showError: State<Boolean>, emoji: MutableState<String?>)

View File

@@ -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<Boolean>,
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()
}
}
}
}
}

View File

@@ -421,6 +421,7 @@
<string name="chat_list_contacts">Contacts</string>
<string name="chat_list_groups">Groups</string>
<string name="chat_list_businesses">Businesses</string>
<string name="chat_list_notes">Notes</string>
<string name="chat_list_all">All</string>
<string name="chat_list_add_list">Add list</string>
@@ -644,6 +645,7 @@
<!-- Tags - ChatListNavLinkView.kt -->
<string name="create_list">Create list</string>
<string name="add_to_list">Add to list</string>
<string name="change_list">Change list</string>
<string name="save_list">Save list</string>
<string name="list_name_field_placeholder">List name...</string>
<string name="duplicated_list_error">List name and emoji should be different for all lists.</string>
@@ -651,6 +653,7 @@
<string name="delete_chat_list_question">Delete list?</string>
<string name="delete_chat_list_warning">All chats will be removed from the list %s, and the list deleted</string>
<string name="edit_chat_list_menu_action">Edit</string>
<string name="change_order_chat_list_menu_action">Change order</string>
<!-- Pending contact connection alert dialogues -->
<string name="you_invited_a_contact">You invited a contact</string>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Zm0-75h630v-395H449l-75-75H165v470Zm0 0v-470 470Z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Z"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -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) }