1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-2966 Chats | Enhancement | Displaying video thumbnail + Request playing video by OS media player (MVP) (#2505)

This commit is contained in:
Evgenii Kozlov 2025-06-05 19:59:30 +02:00 committed by GitHub
parent 8b1e379785
commit 7968718ecb
Signed by: github
GPG key ID: B5690EEEBB952194
11 changed files with 207 additions and 15 deletions

View file

@ -1,7 +1,6 @@
package com.anytypeio.anytype.ui.chats
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
@ -41,8 +39,7 @@ import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.intents.SystemAction
import com.anytypeio.anytype.core_utils.intents.SystemAction.*
import com.anytypeio.anytype.core_utils.intents.SystemAction.OpenUrl
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
@ -113,7 +110,7 @@ class ChatFragment : BaseComposeFragment() {
modifier = Modifier.weight(1f),
vm = vm,
onAttachObjectClicked = { showGlobalSearchBottomSheet = true },
onMarkupLinkClicked = { proceedWithAction(SystemAction.OpenUrl(it)) },
onMarkupLinkClicked = { proceedWithAction(OpenUrl(it)) },
onRequestOpenFullScreenImage = { url -> vm.onMediaPreview(url) },
onSelectChatReaction = vm::onSelectChatReaction,
onViewChatReaction = { msg, emoji ->

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_utils.ext
import android.content.Context
import android.net.Uri
import java.io.IOException
fun Context.getJsonDataFromAsset(fileName: String): String? = try {
@ -11,4 +12,18 @@ fun Context.getJsonDataFromAsset(fileName: String): String? = try {
} catch (e: IOException) {
e.printStackTrace()
null
}
fun getMimeType(context: Context, uri: Uri): String? {
return context.contentResolver.getType(uri)
}
fun isImage(uri: Uri, context: Context): Boolean {
val mimeType = getMimeType(context, uri)
return mimeType?.startsWith("image/") == true
}
fun isVideo(uri: Uri, context: Context): Boolean {
val mimeType = getMimeType(context, uri)
return mimeType?.startsWith("video/") == true
}

View file

@ -40,6 +40,7 @@ dependencies {
implementation libs.composeMaterial
implementation libs.coilCompose
implementation libs.coilVideo
annotationProcessor libs.glideCompiler
implementation libs.glideCompose

View file

@ -91,6 +91,13 @@ sealed interface ChatView {
val ext: String
): Attachment()
data class Video(
val target: Id,
val url: String,
val name: String,
val ext: String
): Attachment()
data class Link(
val target: Id,
val wrapper: ObjectWrapper.Basic?,
@ -111,7 +118,8 @@ sealed interface ChatView {
data class Media(
val uri: String,
val state: State = State.Idle
val state: State = State.Idle,
val isVideo: Boolean = false
): ChatBoxAttachment()
data class File(
@ -127,6 +135,11 @@ sealed interface ChatView {
val url: Url
) : Existing()
data class Video(
val target: Id,
val url: Url
) : Existing()
data class Link(
val target: Id,
val name: String,

View file

@ -301,7 +301,7 @@ class ChatViewModel @Inject constructor(
val wrapper = dependencies[attachment.target]
ChatView.Message.Attachment.Image(
target = attachment.target,
url = urlBuilder.medium(path = attachment.target),
url = urlBuilder.large(path = attachment.target),
name = wrapper?.name.orEmpty(),
ext = wrapper?.fileExt.orEmpty()
)
@ -317,6 +317,14 @@ class ChatViewModel @Inject constructor(
ext = wrapper.fileExt.orEmpty()
)
}
ObjectType.Layout.VIDEO -> {
ChatView.Message.Attachment.Video(
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,
@ -525,6 +533,14 @@ class ChatViewModel @Inject constructor(
)
)
}
is ChatView.Message.ChatBoxAttachment.Existing.Video -> {
add(
Chat.Message.Attachment(
target = attachment.target,
type = Chat.Message.Attachment.Type.File
)
)
}
is ChatView.Message.ChatBoxAttachment.Media -> {
chatBoxAttachments.value = currAttachments.toMutableList().apply {
set(
@ -538,13 +554,19 @@ class ChatViewModel @Inject constructor(
UploadFile.Params(
space = vmParams.space,
path = attachment.uri,
type = Block.Content.File.Type.IMAGE
type = if (attachment.isVideo)
Block.Content.File.Type.VIDEO
else
Block.Content.File.Type.IMAGE
)
).onSuccess { file ->
add(
Chat.Message.Attachment(
target = file.id,
type = Chat.Message.Attachment.Type.Image
type = if (attachment.isVideo)
Chat.Message.Attachment.Type.File
else
Chat.Message.Attachment.Type.Image
)
)
chatBoxAttachments.value = currAttachments.toMutableList().apply {
@ -736,6 +758,14 @@ class ChatViewModel @Inject constructor(
)
)
}
is ChatView.Message.Attachment.Video -> {
add(
ChatView.Message.ChatBoxAttachment.Existing.Video(
target = a.target,
url = a.url
)
)
}
is ChatView.Message.Attachment.Bookmark -> {
add(
ChatView.Message.ChatBoxAttachment.Existing.Link(
@ -839,6 +869,13 @@ class ChatViewModel @Inject constructor(
attachment.name
}
}
is ChatView.Message.Attachment.Video -> {
if (attachment.ext.isNotEmpty()) {
"${attachment.name}.${attachment.ext}"
} else {
attachment.name
}
}
is ChatView.Message.Attachment.Gallery -> {
val first = attachment.images.firstOrNull()
if (first != null) {
@ -893,9 +930,12 @@ class ChatViewModel @Inject constructor(
)
)
}
is ChatView.Message.Attachment.Gallery -> {
is ChatView.Message.Attachment.Video -> {
// TODO
}
is ChatView.Message.Attachment.Gallery -> {
// Do nothing.
}
is ChatView.Message.Attachment.Bookmark -> {
commands.emit(ViewModelCommand.Browse(attachment.url))
}
@ -922,11 +962,12 @@ class ChatViewModel @Inject constructor(
}
}
fun onChatBoxMediaPicked(uris: List<String>) {
fun onChatBoxMediaPicked(uris: List<ChatBoxMediaUri>) {
Timber.d("onChatBoxMediaPicked: $uris")
chatBoxAttachments.value += uris.map {
chatBoxAttachments.value += uris.map { uri ->
ChatView.Message.ChatBoxAttachment.Media(
uri = it
uri = uri.uri,
isVideo = uri.isVideo
)
}
}
@ -1193,6 +1234,11 @@ class ChatViewModel @Inject constructor(
}
}
data class ChatBoxMediaUri(
val uri: String,
val isVideo: Boolean = false
)
sealed class ViewModelCommand {
data object Exit : ViewModelCommand()
data object OpenWidgets : ViewModelCommand()

View file

@ -1,5 +1,8 @@
package com.anytypeio.anytype.feature_chats.ui
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -22,11 +25,17 @@ 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.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.video.videoFrameMillis
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
@ -39,6 +48,7 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import timber.log.Timber
@Composable
@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class)
@ -48,6 +58,8 @@ fun BubbleAttachments(
onAttachmentLongClicked: (ChatView.Message.Attachment) -> Unit,
isUserAuthor: Boolean
) {
Timber.d("Binding attachments: $attachments")
val context = LocalContext.current
attachments.forEachIndexed { idx, attachment ->
when (attachment) {
is ChatView.Message.Attachment.Gallery -> {
@ -64,6 +76,52 @@ fun BubbleAttachments(
index += rowSize
}
}
is ChatView.Message.Attachment.Video -> {
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(292.dp)
.background(
color = colorResource(R.color.shape_tertiary),
shape = RoundedCornerShape(12.dp)
)
.combinedClickable(
onClick = {
requestPlayingVideoByOS(attachment, context)
},
onLongClick = {
onAttachmentLongClicked(attachment)
}
)
) {
CircularProgressIndicator(
modifier = Modifier
.align(alignment = Alignment.Center)
.size(48.dp),
color = colorResource(R.color.glyph_active),
trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f),
strokeWidth = 4.dp
)
AsyncImage(
modifier = Modifier
.size(292.dp)
.clip(RoundedCornerShape(12.dp)),
model = ImageRequest.Builder(context)
.data(attachment.url)
.videoFrameMillis(0)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop
)
Image(
modifier = Modifier.align(Alignment.Center),
painter = painterResource(id = R.drawable.ic_chat_attachment_play),
contentDescription = "Play button"
)
}
}
is ChatView.Message.Attachment.Image -> {
Box(
modifier = Modifier
@ -146,6 +204,17 @@ fun BubbleAttachments(
}
}
private fun requestPlayingVideoByOS(
attachment: ChatView.Message.Attachment.Video,
context: Context
) {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(Uri.parse(attachment.url), "video/*")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AttachedObject(

View file

@ -270,7 +270,7 @@ fun ChatBox(
onClick = {
showDropdownMenu = false
uploadMediaLauncher.launch(
PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly)
PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageAndVideo)
)
}
)

View file

@ -171,6 +171,35 @@ internal fun ChatBoxAttachments(
}
}
}
is ChatView.Message.ChatBoxAttachment.Existing.Video -> {
item {
Box(modifier = Modifier.padding()) {
Image(
painter = rememberAsyncImagePainter(attachment.url),
contentDescription = null,
modifier = Modifier
.padding(
top = 12.dp,
end = 4.dp
)
.size(72.dp)
.clip(RoundedCornerShape(8.dp))
,
contentScale = ContentScale.Crop
)
Image(
painter = painterResource(R.drawable.ic_clear_chatbox_attachment),
contentDescription = "Clear attachment icon",
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 6.dp)
.noRippleClickable {
onClearAttachmentClicked(attachment)
}
)
}
}
}
is ChatView.Message.ChatBoxAttachment.File -> {
item {
Box {

View file

@ -83,6 +83,7 @@ import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.core_utils.common.DefaultFileInfo
import com.anytypeio.anytype.core_utils.ext.isVideo
import com.anytypeio.anytype.core_utils.ext.parseImagePath
import com.anytypeio.anytype.domain.chats.ChatContainer
import com.anytypeio.anytype.feature_chats.R
@ -191,7 +192,17 @@ fun ChatScreenWrapper(
onReplyMessage = vm::onReplyMessage,
onClearReplyClicked = vm::onClearReplyClicked,
onChatBoxMediaPicked = { uris ->
vm.onChatBoxMediaPicked(uris.map { it.parseImagePath(context = context) })
vm.onChatBoxMediaPicked(
uris.map {
ChatViewModel.ChatBoxMediaUri(
uri = it.parseImagePath(context = context),
isVideo = isVideo(
uri = it,
context = context
)
)
}
)
},
onChatBoxFilePicked = { uris ->
val infos = uris.mapNotNull { uri ->

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="34dp"
android:viewportWidth="30"
android:viewportHeight="34">
<path
android:pathData="M0,31.58V2.42C0,0.89 1.647,-0.073 2.981,0.677L28.901,15.257C30.26,16.021 30.26,17.979 28.901,18.743L2.981,33.324C1.647,34.073 0,33.11 0,31.58Z"
android:fillColor="@color/glyph_white"/>
</vector>

View file

@ -61,6 +61,7 @@ roomVersion = '2.7.0'
dataStoreVersion = '1.1.4'
amplitudeVersion = '3.35.1'
coilComposeVersion = '3.1.0'
coilVideoVersion = '3.2.0'
sentryVersion = '7.13.0'
firebaseBomVersion = "33.13.0"
@ -98,6 +99,7 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersio
glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glideVersion" }
glideCompose = { module = "com.github.bumptech.glide:compose", version.ref = "glideComposeVersion" }
coilCompose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilComposeVersion" }
coilVideo = { module = "io.coil-kt.coil3:coil-video", version.ref = "coilVideoVersion" }
coilNetwork = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilComposeVersion" }
mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlinVersion" }
junit = { module = "junit:junit", version.ref = "junitVersion" }