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:
parent
191b675fc9
commit
1d8501f7a0
20 changed files with 282 additions and 15 deletions
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,7 +200,8 @@ fun ChatScreenPreview() {
|
|||
onScrollToReplyClicked = {},
|
||||
onClearIntent = {},
|
||||
onScrollToBottomClicked = {},
|
||||
onVisibleRangeChanged = { _, _ -> }
|
||||
onVisibleRangeChanged = { _, _ -> },
|
||||
onUrlInserted = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue