android, desktop: member name position depends on length (#4918)

* android, desktop: member name position depends on length

* maxWidth limit

* fix

* optimization

* paddings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-09-23 14:57:59 +07:00
committed by GitHub
parent 55d180466a
commit d5507f2fa3
3 changed files with 112 additions and 61 deletions

View File

@@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -1033,25 +1034,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) {
scope.launch {
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
}
}
}
false
}
val swipeableModifier = SwipeToDismissModifier(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
)
val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
scope.launch {
@@ -1066,16 +1048,35 @@ fun BoxWithConstraintsScope.ChatItemsList(
val revealed = remember { mutableStateOf(false) }
@Composable
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) {
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?, fillMaxWidth: Boolean = true) {
tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) {
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
}
}
@Composable
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) {
scope.launch {
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
}
}
}
false
}
val swipeableModifier = SwipeToDismissModifier(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
)
val sent = cItem.chatDir.sent
Box(Modifier.padding(bottom = 4.dp)) {
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
@@ -1095,43 +1096,61 @@ fun BoxWithConstraintsScope.ChatItemsList(
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.fillMaxWidth()
.then(swipeableModifier),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.Start
) {
if (cItem.content.showMemberName) {
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
buildAnnotatedString {
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
append(" ")
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
}
} else {
buildAnnotatedString {
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
@Composable
fun MemberNameAndRole() {
Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
Text(
memberNames(member, prevMember, memCount),
Modifier
.padding(start = MEMBER_IMAGE_SIZE + DEFAULT_PADDING_HALF)
.weight(1f, false),
fontSize = 13.5.sp,
color = MaterialTheme.colors.secondary,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
Text(
member.memberRole.text,
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF),
fontSize = 13.5.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.secondary,
maxLines = 1
)
}
}
Text(
memberNameString,
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
maxLines = 2
)
}
Box(contentAlignment = Alignment.CenterStart) {
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
}
Row(
swipeableOrSelectionModifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
@Composable
fun Item() {
Box(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID), contentAlignment = Alignment.CenterStart) {
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
}
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() },
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
}
ChatItemViewShortHand(cItem, range, false)
}
ChatItemViewShortHand(cItem, range)
}
}
if (cItem.content.showMemberName) {
DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
MemberNameAndRole()
Item()
}
} else {
Item()
}
}
} else {
Box(contentAlignment = Alignment.CenterStart) {

View File

@@ -51,6 +51,7 @@ fun ChatItemView(
revealed: MutableState<Boolean>,
range: IntRange?,
selectedChatItems: MutableState<Set<Long>?>,
fillMaxWidth: Boolean = true,
selectChatItem: () -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
@@ -83,7 +84,7 @@ fun ChatItemView(
val live = composeState.value.liveMessage != null
Box(
modifier = Modifier.fillMaxWidth(),
modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,
contentAlignment = alignment,
) {
val info = cItem.meta.itemStatus.statusInto

View File

@@ -304,6 +304,7 @@ fun CIMarkdownText(
}
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
/**
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
@@ -311,23 +312,23 @@ const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
* */
const val MAX_SAFE_WIDTH = 0x3FFFF - 1
/**
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
* */
private fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
else -> 0x1FFF // shouldn't happen since width is limited already
}
@Composable
fun PriorityLayout(
modifier: Modifier = Modifier,
priorityLayoutId: String,
content: @Composable () -> Unit
) {
/**
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
* */
fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
else -> 0x1FFF // shouldn't happen since width is limited already
}
Layout(
content = content,
modifier = modifier
@@ -352,6 +353,36 @@ fun PriorityLayout(
}
}
}
@Composable
fun DependentLayout(
modifier: Modifier = Modifier,
mainLayoutId: String,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measureable, constraints ->
// Find important element which should tell what min width it needs to draw itself.
// Expecting only one such element. Can be less than one but not more
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }?.measure(constraints)
val placeables: List<Placeable> = measureable.map {
if (it.layoutId == mainLayoutId)
mainPlaceable!!
else
it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = min(MAX_SAFE_WIDTH, constraints.maxWidth))) }
val width = mainPlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width })
val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height })
layout(width, height) {
var y = 0
placeables.forEach {
it.place(0, y)
y += it.measuredHeight
}
}
}
}
/*
class EditedProvider: PreviewParameterProvider<Boolean> {