1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3251 Space-level chat | Fix | Attachment gallery + misc. design fixes (#2127)

This commit is contained in:
Evgenii Kozlov 2025-02-28 17:38:58 +01:00 committed by GitHub
parent 43cf0dbc34
commit 86ad11c4f5
Signed by: github
GPG key ID: B5690EEEBB952194
6 changed files with 266 additions and 106 deletions

View file

@ -58,6 +58,27 @@ sealed interface ChatView {
)
sealed class Attachment {
data class Gallery(val images: List<Image>): Attachment() {
val rowConfig = getRowConfiguration(images.size)
private fun getRowConfiguration(imageCount: Int): List<Int> {
return when (imageCount) {
2 -> listOf(2)
3 -> listOf(1, 2)
4 -> listOf(2, 2)
5 -> listOf(2, 3)
6 -> listOf(3, 3)
7 -> listOf(2, 2, 3)
8 -> listOf(2, 3, 3)
9 -> listOf(3, 3, 3)
10 -> listOf(2, 2, 3, 3)
else -> listOf()
}
}
}
data class Image(
val target: Id,
val url: String,

View file

@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.chats.Chat
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.text.splitByMarks
@ -249,6 +250,21 @@ class ChatViewModel @Inject constructor(
}
}
}
}.let { results ->
if (results.size >= 2) {
val images = results.filterIsInstance<ChatView.Message.Attachment.Image>()
if (images.size == results.size) {
listOf(
ChatView.Message.Attachment.Gallery(
images = images
)
)
} else {
results
}
} else {
results
}
},
avatar = if (member != null && !member.iconImage.isNullOrEmpty()) {
ChatView.Message.Avatar.Image(
@ -558,29 +574,43 @@ class ChatViewModel @Inject constructor(
fun onRequestEditMessageClicked(msg: ChatView.Message) {
Timber.d("onRequestEditMessageClicked")
viewModelScope.launch {
chatBoxAttachments.value = msg.attachments.mapNotNull { a ->
when(a) {
is ChatView.Message.Attachment.Image -> {
ChatView.Message.ChatBoxAttachment.Existing.Image(
target = a.target,
url = a.url
)
}
is ChatView.Message.Attachment.Link -> {
val wrapper = a.wrapper
if (wrapper != null) {
val type = wrapper.type.firstOrNull()
ChatView.Message.ChatBoxAttachment.Existing.Link(
target = wrapper.id,
name = wrapper.name.orEmpty(),
icon = wrapper.objectIcon(urlBuilder),
typeName = if (type != null)
storeOfObjectTypes.get(type)?.name.orEmpty()
else
""
chatBoxAttachments.value = buildList {
msg.attachments.forEach { a ->
when(a) {
is ChatView.Message.Attachment.Image -> {
add(
ChatView.Message.ChatBoxAttachment.Existing.Image(
target = a.target,
url = a.url
)
)
} else {
null
}
is ChatView.Message.Attachment.Gallery -> {
a.images.forEach { image ->
add(
ChatView.Message.ChatBoxAttachment.Existing.Image(
target = image.target,
url = image.url
)
)
}
}
is ChatView.Message.Attachment.Link -> {
val wrapper = a.wrapper
if (wrapper != null) {
val type = wrapper.type.firstOrNull()
add(
ChatView.Message.ChatBoxAttachment.Existing.Link(
target = wrapper.id,
name = wrapper.name.orEmpty(),
icon = wrapper.objectIcon(urlBuilder),
typeName = if (type != null)
storeOfObjectTypes.get(type)?.name.orEmpty()
else
""
)
)
}
}
}
}
@ -646,6 +676,18 @@ class ChatViewModel @Inject constructor(
attachment.name
}
}
is ChatView.Message.Attachment.Gallery -> {
val first = attachment.images.firstOrNull()
if (first != null) {
if (first.ext.isNotEmpty()) {
"${first.name}.${first.ext}"
} else {
first.name
}
} else {
EMPTY_STRING_VALUE
}
}
is ChatView.Message.Attachment.Link -> {
attachment.wrapper?.name.orEmpty()
}
@ -685,6 +727,9 @@ class ChatViewModel @Inject constructor(
)
)
}
is ChatView.Message.Attachment.Gallery -> {
// TODO
}
is ChatView.Message.Attachment.Link -> {
val wrapper = attachment.wrapper
if (wrapper != null) {

View file

@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -33,43 +35,52 @@ import com.bumptech.glide.integration.compose.GlideImage
@Composable
@OptIn(ExperimentalGlideComposeApi::class)
fun BubbleAttachments(
fun ColumnScope.BubbleAttachments(
attachments: List<ChatView.Message.Attachment>,
onAttachmentClicked: (ChatView.Message.Attachment) -> Unit,
isUserAuthor: Boolean
) {
attachments.forEachIndexed { idx, attachment ->
when (attachment) {
is ChatView.Message.Attachment.Gallery -> {
val rowConfig = attachment.rowConfig
var index = 0
rowConfig.forEachIndexed { idx, rowSize ->
BubbleGalleryRowLayout(
onAttachmentClicked = onAttachmentClicked,
images = attachment.images.slice(index until index + rowSize)
)
if (idx != rowConfig.lastIndex) {
Spacer(modifier = Modifier.height(4.dp))
}
index += rowSize
}
}
is ChatView.Message.Attachment.Image -> {
Box(
modifier = Modifier
.padding(
start = 4.dp,
end = 4.dp,
bottom = 4.dp,
top = 0.dp
)
.size(300.dp)
.padding(horizontal = 4.dp)
.size(292.dp)
.background(
color = colorResource(R.color.shape_tertiary),
shape = RoundedCornerShape(16.dp)
shape = RoundedCornerShape(12.dp)
)
) {
CircularProgressIndicator(
modifier = Modifier
.align(alignment = Alignment.Center)
.size(64.dp),
.size(48.dp),
color = colorResource(R.color.glyph_active),
trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f),
strokeWidth = 8.dp
strokeWidth = 4.dp
)
GlideImage(
model = attachment.url,
contentDescription = "Attachment image",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(300.dp)
.clip(shape = RoundedCornerShape(16.dp))
.size(292.dp)
.clip(shape = RoundedCornerShape(12.dp))
.clickable {
onAttachmentClicked(attachment)
}

View file

@ -1,9 +1,7 @@
package com.anytypeio.anytype.feature_chats.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
@ -15,7 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
@ -65,7 +63,6 @@ import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Caption2Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.fontIBM
import com.anytypeio.anytype.core_utils.const.DateConst.TIME_H24
import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis
@ -136,69 +133,12 @@ fun Bubble(
.width(IntrinsicSize.Max)
) {
if (reply != null) {
Text(
text = reply.author,
modifier = Modifier
.padding(
start = 16.dp,
top = 8.dp,
end = 12.dp
)
.alpha(0.5f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_primary),
style = Caption1Medium
ChatBubbleReply(
reply = reply,
onScrollToReplyClicked = onScrollToReplyClicked
)
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clickable {
onScrollToReplyClicked(reply)
}
.alpha(0.5f)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(4.dp)
.background(
color = colorResource(R.color.shape_transparent_primary),
shape = RoundedCornerShape(4.dp)
)
)
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = colorResource(R.color.shape_transparent_secondary),
shape = RoundedCornerShape(16.dp)
)
.clip(RoundedCornerShape(16.dp))
.clickable {
onScrollToReplyClicked(reply)
}
.alpha(0.5f)
) {
Text(
modifier = Modifier.padding(
horizontal = 12.dp,
vertical = 8.dp
),
text = reply.text,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_primary),
style = Caption1Regular
)
}
}
Spacer(modifier = Modifier.height(4.dp))
}
// Username section
// Bubble username section
if (!isUserAuthor) {
Text(
text = name,
@ -214,12 +154,12 @@ fun Bubble(
)
Spacer(modifier = Modifier.height(4.dp))
}
// Text with attachments
// Rendering text with attachments
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = if (isUserAuthor)
color = if (!isUserAuthor)
colorResource(R.color.background_primary)
else
colorResource(R.color.shape_transparent_secondary),
@ -245,6 +185,7 @@ fun Bubble(
bottom = 4.dp
)
) {
// Rendering text body message
Text(
modifier = Modifier,
text = buildAnnotatedString {
@ -327,6 +268,7 @@ fun Bubble(
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
)
// Rendering message timestamp
Text(
modifier = Modifier
.align(Alignment.BottomEnd),
@ -446,6 +388,73 @@ fun Bubble(
}
}
@Composable
private fun ChatBubbleReply(
reply: ChatView.Message.Reply,
onScrollToReplyClicked: (ChatView.Message.Reply) -> Unit
) {
Text(
text = reply.author,
modifier = Modifier
.padding(
start = 16.dp,
top = 8.dp,
end = 12.dp
)
.alpha(0.5f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_primary),
style = Caption1Medium
)
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier
.wrapContentWidth()
.height(IntrinsicSize.Min)
.clickable {
onScrollToReplyClicked(reply)
}
.alpha(0.5f)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(4.dp)
.background(
color = colorResource(R.color.shape_transparent_primary),
shape = RoundedCornerShape(4.dp)
)
)
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.background(
color = colorResource(R.color.shape_transparent_secondary),
shape = RoundedCornerShape(16.dp)
)
.clip(RoundedCornerShape(16.dp))
.clickable {
onScrollToReplyClicked(reply)
}
.alpha(0.5f)
) {
Text(
modifier = Modifier.padding(
horizontal = 12.dp,
vertical = 8.dp
),
text = reply.text,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_primary),
style = Caption1Regular
)
}
}
Spacer(modifier = Modifier.height(4.dp))
}
@Composable
fun ChatUserAvatar(
msg: ChatView.Message,

View file

@ -0,0 +1,72 @@
package com.anytypeio.anytype.feature_chats.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.feature_chats.R
import com.anytypeio.anytype.feature_chats.presentation.ChatView
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun BubbleGalleryRowLayout(
images: List<ChatView.Message.Attachment.Image>,
onAttachmentClicked: (ChatView.Message.Attachment) -> Unit,
) {
Row(
modifier = Modifier
.width(300.dp)
.padding(horizontal = 4.dp)
,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
images.forEach { image ->
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.background(
color = colorResource(R.color.shape_tertiary),
shape = RoundedCornerShape(12.dp)
)
) {
CircularProgressIndicator(
modifier = Modifier
.align(alignment = Alignment.Center)
.size(64.dp),
color = colorResource(R.color.glyph_active),
trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f),
strokeWidth = 8.dp
)
GlideImage(
model = image.url,
contentDescription = "Attachment image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(shape = RoundedCornerShape(12.dp))
.clickable {
onAttachmentClicked(image)
}
)
}
}
}
}

View file

@ -44,9 +44,9 @@ fun ReactionList(
) {
FlowRow(
modifier = Modifier
.padding(start = 0.dp, end = 0.dp, bottom = 0.dp, top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
reactions.forEach { reaction ->
Row(
@ -108,9 +108,11 @@ fun ReactionList(
if (!isMaxReactionCountReached) {
Box(
modifier = Modifier
.padding(end = 8.dp)
.size(28.dp)
.clip(CircleShape)
.background(
color = colorResource(R.color.shape_transparent_secondary)
)
.clickable {
onAddNewReaction()
}