ui: chat tag fixes (#5427)

* ui: chat tag fixes

* fix switching tags

* change

* android: fix switching profile

* change

* sp

* change

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
Evgeny
2024-12-25 22:09:18 +00:00
committed by GitHub
parent a0cc177eb5
commit 086e375bac
6 changed files with 178 additions and 163 deletions
+3 -3
View File
@@ -163,7 +163,7 @@ class ChatTagsModel: ObservableObject {
func markChatTagRead(_ chat: Chat) -> Void {
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
markChatTagRead_(chat, tags)
decTagsReadCount(tags)
}
}
@@ -175,11 +175,11 @@ class ChatTagsModel: ObservableObject {
unreadTags[tag] = (unreadTags[tag] ?? 0) + 1
}
} else if !nowUnread && wasUnread {
markChatTagRead_(chat, tags)
decTagsReadCount(tags)
}
}
private func markChatTagRead_(_ chat: Chat, _ tags: [Int64]) -> Void {
func decTagsReadCount(_ tags: [Int64]) -> Void {
for tag in tags {
if let count = unreadTags[tag] {
unreadTags[tag] = max(0, count - 1)
@@ -748,7 +748,11 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
await MainActor.run {
let m = ChatModel.shared
ChatTagsModel.shared.userTags = userTags
let tm = ChatTagsModel.shared
tm.userTags = userTags
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
tm.decTagsReadCount(tags)
}
if var contact = chat.chatInfo.contact {
contact.chatTags = chatTags
m.updateContact(contact)
@@ -756,6 +760,7 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
group.chatTags = chatTags
m.updateGroup(group)
}
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false)
closeSheet()
}
} catch let error {
@@ -625,6 +625,7 @@ object ChatController {
updateChats(chats)
}
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
chatModel.activeChatTagFilter.value = null
chatModel.updateChatTags(rhId)
}
@@ -33,8 +33,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.chat.item.CIFileViewScope
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.*
@@ -877,7 +876,7 @@ private fun NoChatsView(searchText: MutableState<TextFieldValue>) {
is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
is ActiveFilter.Unread -> {
Row(
Modifier.clip(shape = RoundedCornerShape(percent = 50)).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF),
Modifier.clip(shape = CircleShape).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -917,6 +916,8 @@ private fun ChatListFeatureCards() {
}
}
private val TAG_MIN_HEIGHT = 35.dp
@Composable
private fun TagsView() {
val userTags = remember { chatModel.userTags }
@@ -929,7 +930,7 @@ private fun TagsView() {
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 = RoundedCornerShape(percent = 50))) {
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))
}
}) {
@@ -937,7 +938,7 @@ private fun TagsView() {
}
}
}
val rowSizeModifier = Modifier.sizeIn(minHeight = 35.dp * fontSizeSqrtMultiplier)
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
TagsRow {
if (presetTags.size > 1) {
@@ -946,9 +947,7 @@ private fun TagsView() {
ExpandedTagFilterView(tag)
}
} else {
Column(rowSizeModifier, verticalArrangement = Arrangement.Center) {
CollapsedTagsFilterView()
}
CollapsedTagsFilterView()
}
}
@@ -958,71 +957,68 @@ private fun TagsView() {
else -> false
}
val interactionSource = remember { MutableInteractionSource() }
Column(rowSizeModifier, verticalArrangement = Arrangement.Center) {
Row(
Modifier
.clip(shape = RoundedCornerShape(percent = 50))
.combinedClickable(
onClick = {
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag)
}
},
onLongClick = { showTagList() },
interactionSource = interactionSource,
indication = LocalIndication.current
)
.onRightClick { showTagList() }
.padding(4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
Text(
tag.chatTagEmoji
)
} else {
Icon(
painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label),
null,
Modifier.size(20.dp),
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)
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 = { 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.SemiBold,
color = Color.Transparent,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Visible text with styles
val visibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.sp, color = MaterialTheme.colors.primary)) {
append(badgeText)
}
}
Text(
text = visibleText,
fontWeight = if (current) FontWeight.SemiBold else FontWeight.Normal,
color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
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
)
}
}
}
@@ -1033,16 +1029,16 @@ private fun TagsView() {
}
}
Column(rowSizeModifier, verticalArrangement = Arrangement.Center) {
if (userTags.value.isEmpty()) {
Row(Modifier.clip(shape = RoundedCornerShape(percent = 50)).then(plusClickModifier).padding(vertical = 4.dp), horizontalArrangement = Arrangement.Center) {
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(2.dp))
Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary)
}
} else {
if (userTags.value.isEmpty()) {
Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(2.dp))
Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp)
}
} else {
Box(rowSizeModifier, contentAlignment = Alignment.Center) {
Icon(
painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(4.dp), tint = MaterialTheme.colors.secondary
painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary
)
}
}
@@ -1074,12 +1070,13 @@ 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 = Modifier
.clip(shape = RoundedCornerShape(percent = 50))
modifier = rowSizeModifier
.clip(shape = CircleShape)
.clickable {
if (activeFilter.value == ActiveFilter.PresetTag(tag)) {
chatModel.activeChatTagFilter.value = null
@@ -1087,7 +1084,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) {
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag)
}
}
.padding(4.dp)
.padding(horizontal = 5.dp, vertical = 4.dp)
,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
@@ -1095,6 +1092,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) {
Icon(
painterResource(icon),
stringResource(text),
Modifier.size(18.sp.toDp()),
tint = color
)
Spacer(Modifier.width(4.dp))
@@ -1102,12 +1100,14 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) {
Text(
stringResource(text),
color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontWeight = if (active) FontWeight.SemiBold else FontWeight.Normal,
fontWeight = if (active) FontWeight.Medium else FontWeight.Normal,
fontSize = 15.sp
)
Text(
stringResource(text),
color = Color.Transparent,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.Medium,
fontSize = 15.sp
)
}
}
@@ -1125,17 +1125,20 @@ private fun CollapsedTagsFilterView() {
else -> null
}
Column(Modifier
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
Box(rowSizeModifier
.padding(vertical = 4.dp)
.clip(shape = CircleShape)
.clickable { showMenu.value = true }
.padding(4.dp)
.size(30.sp.toDp())
.clickable { showMenu.value = true },
contentAlignment = Alignment.Center
) {
if (selectedPresetTag != null) {
val (icon, text) = presetTagLabel(selectedPresetTag, true)
Icon(
painterResource(icon),
stringResource(text),
Modifier.size(18.sp.toDp()),
tint = MaterialTheme.colors.secondary
)
} else {
@@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
@@ -35,6 +36,7 @@ import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.ReactionIcon
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -148,11 +150,9 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
Text(
tag.chatTagEmoji
)
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
} else {
Icon(painterResource(MR.images.ic_label), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_label), null, Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.onBackground)
}
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -196,7 +196,6 @@ fun ModalData.TagListEditor(
) {
val userTags = remember { chatModel.userTags }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val newEmoji = remember { stateGetOrPutNullable("chatTagEmoji") { emoji } }
val newName = remember { stateGetOrPut("chatTagName") { name } }
val saving = remember { mutableStateOf<Boolean?>(null) }
@@ -351,53 +350,45 @@ expect fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, e
fun TagListNameTextField(name: MutableState<String>, showError: State<Boolean>) {
var focused by rememberSaveable { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val strokeColor by remember {
derivedStateOf {
if (showError.value) {
Color.Red
} else {
if (focused) {
CurrentColors.value.colors.secondary.copy(alpha = 0.6f)
} else {
CurrentColors.value.colors.secondary.copy(alpha = 0.3f)
}
}
val interactionSource = remember { MutableInteractionSource() }
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
cursorColor = MaterialTheme.colors.secondary,
)
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
interactionSource = interactionSource,
modifier = Modifier
.fillMaxWidth()
.indicatorLine(true, showError.value, interactionSource, colors)
.heightIn(min = TextFieldDefaults.MinHeight)
.onFocusChanged { focused = it.isFocused }
.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = MaterialTheme.colors.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = name.value,
innerTextField = innerTextField,
placeholder = {
Text(generalGetString(MR.strings.list_name_field_placeholder), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp))
},
contentPadding = PaddingValues(),
label = null,
visualTransformation = VisualTransformation.None,
leadingIcon = null,
singleLine = true,
enabled = true,
isError = false,
interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
)
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 50.dp)
.onFocusChanged { focused = it.isFocused }
.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = name.value,
innerTextField = innerTextField,
placeholder = {
Text(generalGetString(MR.strings.list_name_field_placeholder), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp))
},
contentPadding = PaddingValues(),
label = null,
visualTransformation = VisualTransformation.None,
leadingIcon = null,
singleLine = true,
enabled = true,
isError = false,
interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
)
}
)
Divider(color = strokeColor, thickness = if (focused) 2.dp else 1.dp)
}
)
}
private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) {
@@ -5,13 +5,19 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import androidx.compose.ui.unit.sp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.views.chat.item.isHeartEmoji
import chat.simplex.common.views.chat.item.isShortEmoji
import chat.simplex.common.views.helpers.toDp
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@@ -27,31 +33,40 @@ actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, e
private fun SingleEmojiInput(
emoji: MutableState<String?>
) {
val state = remember { mutableStateOf(TextFieldValue(emoji.value ?: "")) }
val colors = TextFieldDefaults.textFieldColors(
textColor = if (isHeartEmoji(emoji.value ?: "")) Color(0xffD63C31) else MaterialTheme.colors.onPrimary,
backgroundColor = Color.Unspecified,
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
cursorColor = MaterialTheme.colors.secondary,
)
TextField(
value = emoji.value?.let { TextFieldValue(it) } ?: TextFieldValue(""),
value = state.value,
onValueChange = { newValue ->
if (newValue.text == emoji.value) return@TextField
if (newValue.text == emoji.value) {
state.value = newValue
return@TextField
}
val newValueClamped = newValue.text.replace(emoji.value ?: "", "")
emoji.value = if (isShortEmoji(newValueClamped)) newValueClamped else null
val isEmoji = isShortEmoji(newValueClamped)
emoji.value = if (isEmoji) newValueClamped else null
state.value = if (isEmoji) newValue else TextFieldValue()
},
singleLine = true,
maxLines = 1,
modifier = Modifier
.size(60.dp)
.padding(4.dp),
.padding(4.dp)
.size(width = TextFieldDefaults.MinHeight.value.sp.toDp(), height = TextFieldDefaults.MinHeight),
textStyle = LocalTextStyle.current.copy(fontFamily = EmojiFont, textAlign = TextAlign.Center),
placeholder = {
Icon(
painter = painterResource(MR.images.ic_add_reaction),
contentDescription = null,
tint = MaterialTheme.colors.secondary
)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(MR.images.ic_add_reaction),
contentDescription = null,
tint = MaterialTheme.colors.secondary
)
}
},
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
cursorColor = MaterialTheme.colors.secondary,
),
colors = colors,
)
}