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:
parent
1d8501f7a0
commit
48b11cb886
5 changed files with 159 additions and 37 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue