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 (#2399)

This commit is contained in:
Evgenii Kozlov 2025-05-14 17:09:31 +02:00 committed by GitHub
parent 191b675fc9
commit 1d8501f7a0
Signed by: github
GPG key ID: B5690EEEBB952194
20 changed files with 282 additions and 15 deletions

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.feature_chats.presentation
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.LinkPreview
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.domain.chats.ChatContainer
@ -129,6 +130,10 @@ sealed interface ChatView {
val wrapper: GlobalSearchItemView
): ChatBoxAttachment()
data class Bookmark(
val preview: LinkPreview
) : ChatBoxAttachment()
sealed class State {
data object Idle : State()
data object Uploading : State()

View file

@ -4,8 +4,10 @@ import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Block
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.ObjectWrapper
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
import com.anytypeio.anytype.core_models.primitives.Space
@ -23,10 +25,12 @@ import com.anytypeio.anytype.domain.chats.DeleteChatMessage
import com.anytypeio.anytype.domain.chats.EditChatMessage
import com.anytypeio.anytype.domain.chats.ToggleChatMessageReaction
import com.anytypeio.anytype.domain.media.UploadFile
import com.anytypeio.anytype.domain.misc.GetLinkPreview
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer.Store
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.getTypeOfObject
import com.anytypeio.anytype.feature_chats.BuildConfig
@ -76,6 +80,8 @@ class ChatViewModel @Inject constructor(
private val storeOfObjectTypes: StoreOfObjectTypes,
private val copyFileToCacheDirectory: CopyFileToCacheDirectory,
private val exitToVaultDelegate: ExitToVaultDelegate,
private val getLinkPreview: GetLinkPreview,
private val createObjectFromUrl: CreateObjectFromUrl
) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate {
private val visibleRangeUpdates = MutableSharedFlow<Pair<Id, Id>>(
@ -489,6 +495,27 @@ class ChatViewModel @Inject constructor(
}
}
}
is ChatView.Message.ChatBoxAttachment.Bookmark -> {
createObjectFromUrl.async(
params = CreateObjectFromUrl.Params(
url = attachment.preview.url,
space = vmParams.space
)
).onSuccess { obj ->
if (obj.isValid) {
add(
Chat.Message.Attachment(
target = obj.id,
type = Chat.Message.Attachment.Type.Link
)
)
} else {
Timber.w("DROID-2966 Created object from URL is not valid")
}
}.onFailure {
Timber.e(it, "DROID-2966 Error while creating object from url")
}
}
is ChatView.Message.ChatBoxAttachment.File -> {
val path = withContext(dispatchers.io) {
copyFileToCacheDirectory.copy(attachment.uri)
@ -961,6 +988,25 @@ class ChatViewModel @Inject constructor(
visibleRangeUpdates.tryEmit(from to to)
}
fun onUrlPasted(url: Url) {
viewModelScope.launch {
getLinkPreview.async(
params = url
).onSuccess { preview ->
chatBoxAttachments.value = buildList {
addAll(chatBoxAttachments.value)
add(
ChatView.Message.ChatBoxAttachment.Bookmark(
preview = preview
)
)
}
}.onFailure {
Timber.e(it, "Failed to get link preview")
}
}
}
/**
* Used for testing. Will be deleted.
*/
@ -1021,7 +1067,7 @@ class ChatViewModel @Inject constructor(
): ChatBoxMode()
}
fun ChatBoxMode.updateIsSendingBlocked(isBlocked: Boolean): ChatBoxMode {
private fun ChatBoxMode.updateIsSendingBlocked(isBlocked: Boolean): ChatBoxMode {
return when (this) {
is ChatBoxMode.Default -> copy(isSendingMessageBlocked = isBlocked)
is ChatBoxMode.EditMessage -> copy(isSendingMessageBlocked = isBlocked)

View file

@ -10,11 +10,13 @@ import com.anytypeio.anytype.domain.chats.DeleteChatMessage
import com.anytypeio.anytype.domain.chats.EditChatMessage
import com.anytypeio.anytype.domain.chats.ToggleChatMessageReaction
import com.anytypeio.anytype.domain.media.UploadFile
import com.anytypeio.anytype.domain.misc.GetLinkPreview
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.`object`.OpenObject
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.vault.ExitToVaultDelegate
@ -35,7 +37,9 @@ class ChatViewModelFactory @Inject constructor(
private val uploadFile: UploadFile,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val copyFileToCacheDirectory: CopyFileToCacheDirectory,
private val exitToVaultDelegate: ExitToVaultDelegate
private val exitToVaultDelegate: ExitToVaultDelegate,
private val getLinkPreview: GetLinkPreview,
private val createObjectFromUrl: CreateObjectFromUrl
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = ChatViewModel(
@ -53,6 +57,8 @@ class ChatViewModelFactory @Inject constructor(
uploadFile = uploadFile,
storeOfObjectTypes = storeOfObjectTypes,
copyFileToCacheDirectory = copyFileToCacheDirectory,
exitToVaultDelegate = exitToVaultDelegate
exitToVaultDelegate = exitToVaultDelegate,
getLinkPreview = getLinkPreview,
createObjectFromUrl = createObjectFromUrl
) as T
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.feature_chats.ui
import android.net.Uri
import android.util.Patterns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@ -61,6 +62,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.common.DEFAULT_DISABLED_ALPHA
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.FULL_ALPHA
@ -94,7 +96,8 @@ fun ChatBox(
onChatBoxMediaPicked: (List<Uri>) -> Unit,
onChatBoxFilePicked: (List<Uri>) -> Unit,
onExitEditMessageMode: () -> Unit,
onValueChange: (TextFieldValue, List<ChatBoxSpan>) -> Unit
onValueChange: (TextFieldValue, List<ChatBoxSpan>) -> Unit,
onUrlInserted: (Url) -> Unit,
) {
val length = text.text.length
@ -309,7 +312,8 @@ fun ChatBox(
end = 4.dp,
top = 16.dp,
bottom = 16.dp
)
),
onUrlInserted = onUrlInserted
)
if (length >= ChatConfig.MAX_MESSAGE_CHARACTER_OFFSET_LIMIT) {
Box(
@ -478,15 +482,29 @@ private fun ChatBoxUserInput(
text: TextFieldValue,
spans: List<ChatBoxSpan>,
onValueChange: (TextFieldValue, List<ChatBoxSpan>) -> Unit,
onUrlInserted: (Url) -> Unit,
onFocusChanged: (Boolean) -> Unit
) {
BasicTextField(
value = text,
onValueChange = { newValue ->
val newText = newValue.text
val oldText = text.text // Keep a reference to the current text before updating
val textLengthDifference = newText.length - oldText.length
// URL insert detection
if (textLengthDifference > 0) {
val prefixLen = newText.commonPrefixWith(oldText).length
val inserted = newText.substring(prefixLen, prefixLen + textLengthDifference)
val urlMatcher = Patterns.WEB_URL.matcher(inserted)
if (urlMatcher.find()) {
val url = urlMatcher.group()
onUrlInserted(url)
}
}
// SPANS normalization
val updatedSpans = spans.mapNotNull { span ->
// Detect the common prefix length
val commonPrefixLength = newText.commonPrefixWith(oldText).length

View file

@ -58,7 +58,7 @@ internal fun ChatBoxAttachments(
type = attachment.typeName,
icon = attachment.icon,
onAttachmentClicked = {
// TODO
// Do nothing
}
)
Image(
@ -90,7 +90,7 @@ internal fun ChatBoxAttachments(
type = attachment.wrapper.type,
icon = attachment.wrapper.icon,
onAttachmentClicked = {
// TODO
// Do nothing
}
)
Image(
@ -191,7 +191,7 @@ internal fun ChatBoxAttachments(
fileName = null
),
onAttachmentClicked = {
// TODO
// Do nothing
}
)
Image(
@ -210,6 +210,38 @@ internal fun ChatBoxAttachments(
}
}
}
is ChatView.Message.ChatBoxAttachment.Bookmark -> {
item {
Box {
AttachedObject(
modifier = Modifier
.padding(
top = 12.dp,
end = 4.dp
)
.width(216.dp),
title = attachment.preview.title,
type = stringResource(R.string.bookmark),
icon = ObjectIcon.None,
onAttachmentClicked = {
// Do nothing
}
)
Image(
painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment),
contentDescription = "Close icon",
modifier = Modifier
.align(
Alignment.TopEnd
)
.padding(top = 6.dp)
.noRippleClickable {
onClearAttachmentClicked(attachment)
}
)
}
}
}
}
}
}

View file

@ -200,7 +200,8 @@ fun ChatScreenPreview() {
onScrollToReplyClicked = {},
onClearIntent = {},
onScrollToBottomClicked = {},
onVisibleRangeChanged = { _, _ -> }
onVisibleRangeChanged = { _, _ -> },
onUrlInserted = {}
)
}

View file

@ -70,6 +70,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
import com.anytypeio.anytype.core_ui.foundation.AlertIcon
import com.anytypeio.anytype.core_ui.foundation.Divider
@ -238,7 +239,8 @@ fun ChatScreenWrapper(
onScrollToReplyClicked = vm::onChatScrollToReply,
onClearIntent = vm::onClearChatViewStateIntent,
onScrollToBottomClicked = vm::onScrollToBottomClicked,
onVisibleRangeChanged = vm::onVisibleRangeChanged
onVisibleRangeChanged = vm::onVisibleRangeChanged,
onUrlInserted = vm::onUrlPasted
)
LaunchedEffect(Unit) {
vm.uXCommands.collect { command ->
@ -352,7 +354,8 @@ fun ChatScreen(
onScrollToReplyClicked: (Id) -> Unit,
onClearIntent: () -> Unit,
onScrollToBottomClicked: (Id?) -> Unit,
onVisibleRangeChanged: (Id, Id) -> Unit
onVisibleRangeChanged: (Id, Id) -> Unit,
onUrlInserted: (Url) -> Unit,
) {
Timber.d("DROID-2966 Render called with state, number of messages: ${messages.size}")
@ -688,7 +691,8 @@ fun ChatScreen(
onTextChanged(t)
},
text = text,
spans = spans
spans = spans,
onUrlInserted = onUrlInserted
)
}
}