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

DROID-3444 Chats | Enhancement | Bookmark flow updates (#2403)

This commit is contained in:
Evgenii Kozlov 2025-05-14 23:23:29 +02:00 committed by GitHub
parent 1d8501f7a0
commit 48b11cb886
Signed by: github
GPG key ID: B5690EEEBB952194
5 changed files with 159 additions and 37 deletions

View file

@ -95,6 +95,14 @@ sealed interface ChatView {
val icon: ObjectIcon = ObjectIcon.None,
val typeName: String
): Attachment()
data class Bookmark(
val id: Id,
val url: String,
val title: String,
val description: String,
val imageUrl: String?
) : Attachment()
}
sealed class ChatBoxAttachment {

View file

@ -6,7 +6,9 @@ import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.LinkPreview
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_models.chats.Chat
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
@ -262,27 +264,44 @@ class ChatViewModel @Inject constructor(
}
else -> {
val wrapper = dependencies[attachment.target]
if (wrapper?.layout == ObjectType.Layout.IMAGE) {
ChatView.Message.Attachment.Image(
target = attachment.target,
url = urlBuilder.large(path = attachment.target),
name = wrapper.name.orEmpty(),
ext = wrapper.fileExt.orEmpty()
)
} else {
val type = wrapper?.type?.firstOrNull()
ChatView.Message.Attachment.Link(
target = attachment.target,
wrapper = wrapper,
icon = wrapper?.objectIcon(
builder = urlBuilder,
objType = storeOfObjectTypes.getTypeOfObject(wrapper)
) ?: ObjectIcon.None,
typeName = if (type != null)
storeOfObjectTypes.get(type)?.name.orEmpty()
else
""
)
when (wrapper?.layout) {
ObjectType.Layout.IMAGE -> {
ChatView.Message.Attachment.Image(
target = attachment.target,
url = urlBuilder.large(path = attachment.target),
name = wrapper.name.orEmpty(),
ext = wrapper.fileExt.orEmpty()
)
}
ObjectType.Layout.BOOKMARK -> {
ChatView.Message.Attachment.Bookmark(
id = wrapper.id,
url = wrapper.getSingleValue<String>(Relations.SOURCE).orEmpty(),
title = wrapper.name.orEmpty(),
description = wrapper.description.orEmpty(),
imageUrl = wrapper.getSingleValue<String?>(Relations.PICTURE).let { hash ->
if (!hash.isNullOrEmpty())
urlBuilder.medium(hash)
else
null
}
)
}
else -> {
val type = wrapper?.type?.firstOrNull()
ChatView.Message.Attachment.Link(
target = attachment.target,
wrapper = wrapper,
icon = wrapper?.objectIcon(
builder = urlBuilder,
objType = storeOfObjectTypes.getTypeOfObject(wrapper)
) ?: ObjectIcon.None,
typeName = if (type != null)
storeOfObjectTypes.get(type)?.name.orEmpty()
else
""
)
}
}
}
}
@ -605,10 +624,11 @@ class ChatViewModel @Inject constructor(
uXCommands.emit(UXCommand.JumpToBottom)
chatBoxAttachments.value = emptyList()
}.onFailure {
Timber.e(it, "Error while adding message")
Timber.e(it, "Error while editing message")
}.onSuccess {
chatBoxMode.value = ChatBoxMode.Default()
Timber.d("Message edited with success")
}
chatBoxMode.value = ChatBoxMode.Default()
}
is ChatBoxMode.Reply -> {
addChatMessage.async(
@ -649,6 +669,16 @@ class ChatViewModel @Inject constructor(
)
)
}
is ChatView.Message.Attachment.Bookmark -> {
add(
ChatView.Message.ChatBoxAttachment.Existing.Link(
target = a.id,
name = a.title,
icon = ObjectIcon.None,
typeName = storeOfObjectTypes.get(ObjectTypeUniqueKeys.BOOKMARK)?.name.orEmpty()
)
)
}
is ChatView.Message.Attachment.Gallery -> {
a.images.forEach { image ->
add(
@ -687,7 +717,7 @@ class ChatViewModel @Inject constructor(
}
fun onAttachObject(obj: GlobalSearchItemView) {
chatBoxAttachments.value = chatBoxAttachments.value + listOf(
chatBoxAttachments.value += listOf(
ChatView.Message.ChatBoxAttachment.Link(
target = obj.id,
wrapper = obj
@ -734,8 +764,7 @@ class ChatViewModel @Inject constructor(
text = msg.content.msg.ifEmpty {
// Fallback to attachment name if empty
if (msg.attachments.isNotEmpty()) {
val attachment = msg.attachments.last()
when(attachment) {
when(val attachment = msg.attachments.last()) {
is ChatView.Message.Attachment.Image -> {
if (attachment.ext.isNotEmpty()) {
"${attachment.name}.${attachment.ext}"
@ -758,6 +787,9 @@ class ChatViewModel @Inject constructor(
is ChatView.Message.Attachment.Link -> {
attachment.wrapper?.name.orEmpty()
}
is ChatView.Message.Attachment.Bookmark -> {
attachment.url
}
}
} else {
""
@ -797,6 +829,9 @@ class ChatViewModel @Inject constructor(
is ChatView.Message.Attachment.Gallery -> {
// TODO
}
is ChatView.Message.Attachment.Bookmark -> {
// TODO
}
is ChatView.Message.Attachment.Link -> {
val wrapper = attachment.wrapper
if (wrapper != null) {
@ -822,7 +857,7 @@ class ChatViewModel @Inject constructor(
fun onChatBoxMediaPicked(uris: List<String>) {
Timber.d("onChatBoxMediaPicked: $uris")
chatBoxAttachments.value = chatBoxAttachments.value + uris.map {
chatBoxAttachments.value += uris.map {
ChatView.Message.ChatBoxAttachment.Media(
uri = it
)
@ -831,7 +866,7 @@ class ChatViewModel @Inject constructor(
fun onChatBoxFilePicked(infos: List<DefaultFileInfo>) {
Timber.d("onChatBoxFilePicked: $infos")
chatBoxAttachments.value = chatBoxAttachments.value + infos.map { info ->
chatBoxAttachments.value += infos.map { info ->
ChatView.Message.ChatBoxAttachment.File(
uri = info.uri,
name = info.name,
@ -920,14 +955,14 @@ class ChatViewModel @Inject constructor(
}
}
fun isMentionTriggered(text: String, selectionStart: Int): Boolean {
private fun isMentionTriggered(text: String, selectionStart: Int): Boolean {
if (selectionStart <= 0 || selectionStart > text.length) return false
val previousChar = text[selectionStart - 1]
return previousChar == '@'
&& (selectionStart == 1 || !text[selectionStart - 2].isLetterOrDigit())
}
fun shouldHideMention(text: String, selectionStart: Int): Boolean {
private fun shouldHideMention(text: String, selectionStart: Int): Boolean {
if (selectionStart > text.length) return false
// Check if the current character is a space
val currentChar = if (selectionStart > 0) text[selectionStart - 1] else null
@ -936,7 +971,7 @@ class ChatViewModel @Inject constructor(
return currentChar == ' ' || !atCharExists
}
fun resolveMentionQuery(text: String, selectionStart: Int): MentionPanelState.Query? {
private fun resolveMentionQuery(text: String, selectionStart: Int): MentionPanelState.Query? {
val atIndex = text.lastIndexOf('@', selectionStart - 1)
if (atIndex == -1 || (atIndex > 0 && text[atIndex - 1].isLetterOrDigit())) return null
val endIndex = text.indexOf(' ', atIndex).takeIf { it != -1 } ?: text.length

View file

@ -1,31 +1,41 @@
package com.anytypeio.anytype.feature_chats.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.feature_chats.R
import com.anytypeio.anytype.feature_chats.presentation.ChatView
@ -35,7 +45,7 @@ import com.bumptech.glide.integration.compose.GlideImage
@Composable
@OptIn(ExperimentalGlideComposeApi::class)
fun ColumnScope.BubbleAttachments(
fun BubbleAttachments(
attachments: List<ChatView.Message.Attachment>,
onAttachmentClicked: (ChatView.Message.Attachment) -> Unit,
isUserAuthor: Boolean
@ -106,6 +116,14 @@ fun ColumnScope.BubbleAttachments(
}
)
}
is ChatView.Message.Attachment.Bookmark -> {
Bookmark(
url = attachment.url,
title = attachment.title,
description = attachment.description,
imageUrl = attachment.imageUrl
)
}
}
}
}
@ -181,8 +199,68 @@ fun AttachedObject(
}
}
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
@Composable
fun Bookmark(
url: String,
title: String,
description: String,
imageUrl: String?
) {
Card(
modifier = Modifier.fillMaxWidth().padding(4.dp),
colors = CardDefaults.cardColors(
containerColor = colorResource(R.color.shape_transparent_secondary)
),
shape = RoundedCornerShape(12.dp),
) {
if (!imageUrl.isNullOrEmpty()) {
Image(
painter = rememberAsyncImagePainter(imageUrl),
modifier = Modifier
.fillMaxWidth()
.background(color = Color.Red)
.aspectRatio(1.91f),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = url,
modifier = Modifier.padding(horizontal = 12.dp),
style = Relations3,
color = colorResource(R.color.transparent_active)
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = title,
modifier = Modifier.padding(horizontal = 12.dp),
style = Title2,
color = colorResource(R.color.text_primary)
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = description,
modifier = Modifier.padding(horizontal = 12.dp),
style = Relations3,
color = colorResource(R.color.transparent_active)
)
Spacer(modifier = Modifier.height(8.dp))
}
}
@DefaultPreviews
@Composable
fun BookmarkPreview() {
Bookmark(
url = "algo.tv",
title = "Algo - Video Automation",
description = "Algo is a data-visualization studio specializing in video automation.",
imageUrl = null
)
}
@DefaultPreviews
@Composable
fun AttachmentPreview() {
AttachedObject(

View file

@ -191,7 +191,7 @@ fun Bubble(
modifier = Modifier,
text = buildAnnotatedString {
content.parts.forEach { part ->
if (part.link != null && part.link.param != null) {
if (part.link?.param != null) {
withLink(
LinkAnnotation.Clickable(
tag = DEFAULT_MENTION_LINK_TAG,
@ -208,7 +208,7 @@ fun Bubble(
) {
append(part.part)
}
} else if (part.mention != null && part.mention.param != null) {
} else if (part.mention?.param != null) {
withLink(
LinkAnnotation.Clickable(
tag = DEFAULT_MENTION_SPAN_TAG,
@ -225,7 +225,7 @@ fun Bubble(
) {
append(part.part)
}
} else if (part.emoji != null && part.emoji.param != null) {
} else if (part.emoji?.param != null) {
append(part.emoji.param)
} else {
withStyle(