From d5507f2fa35d7414178f68992e11767716d55ef8 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:57:59 +0700 Subject: [PATCH] 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 --- .../simplex/common/views/chat/ChatView.kt | 117 ++++++++++-------- .../common/views/chat/item/ChatItemView.kt | 3 +- .../common/views/chat/item/FramedItemView.kt | 53 ++++++-- 3 files changed, 112 insertions(+), 61 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 835f884f98..18deb48597 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 516e47e7ed..df30e85161 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -51,6 +51,7 @@ fun ChatItemView( revealed: MutableState, range: IntRange?, selectedChatItems: MutableState?>, + fillMaxWidth: Boolean = true, selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> 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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 8a579d5289..ddcf7c340b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -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 = 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 {