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

DROID-3047 Chats | Enhancement | Allow capturing pictures by camera and sending it as attachments (#2510)

This commit is contained in:
Evgenii Kozlov 2025-06-06 16:13:13 +02:00 committed by GitHub
parent a4e5d16e16
commit e384832a9a
Signed by: github
GPG key ID: B5690EEEBB952194
8 changed files with 159 additions and 29 deletions

View file

@ -34,6 +34,7 @@ dependencies {
implementation libs.appcompat
implementation libs.compose
implementation libs.activityCompose
implementation libs.composeFoundation
implementation libs.composeToolingPreview
implementation libs.composeMaterial3

View file

@ -119,7 +119,8 @@ sealed interface ChatView {
data class Media(
val uri: String,
val state: State = State.Idle,
val isVideo: Boolean = false
val isVideo: Boolean = false,
val capturedByCamera: Boolean = false
): ChatBoxAttachment()
data class File(

View file

@ -76,7 +76,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel.ShareLinkViewState
class ChatViewModel @Inject constructor(
private val vmParams: Params.Default,
@ -129,11 +128,12 @@ class ChatViewModel @Inject constructor(
private val dateFormatter = SimpleDateFormat("d MMMM YYYY")
private val messageRateLimiter = MessageRateLimiter()
private var capturedImageUri: String? = null
private var account: Id = ""
init {
// generateDummyChatHistory()
Timber.d("DROID-2966 init")
viewModelScope.launch {
spacePermissionProvider
@ -593,10 +593,17 @@ class ChatViewModel @Inject constructor(
)
)
}
val path = if (attachment.capturedByCamera) {
withContext(dispatchers.io) {
copyFileToCacheDirectory.copy(attachment.uri)
}.orEmpty()
} else {
attachment.uri
}
uploadFile.async(
UploadFile.Params(
space = vmParams.space,
path = attachment.uri,
path = path,
type = if (attachment.isVideo)
Block.Content.File.Type.VIDEO
else
@ -621,6 +628,7 @@ class ChatViewModel @Inject constructor(
)
}
}.onFailure {
Timber.e(it, "DROID-2966 Error while uploading file as attachment")
chatBoxAttachments.value = currAttachments.toMutableList().apply {
set(
index = idx,
@ -1006,17 +1014,18 @@ class ChatViewModel @Inject constructor(
}
fun onChatBoxMediaPicked(uris: List<ChatBoxMediaUri>) {
Timber.d("onChatBoxMediaPicked: $uris")
Timber.d("DROID-2966 onChatBoxMediaPicked: $uris")
chatBoxAttachments.value += uris.map { uri ->
ChatView.Message.ChatBoxAttachment.Media(
uri = uri.uri,
isVideo = uri.isVideo
isVideo = uri.isVideo,
capturedByCamera = uri.capturedByCamera
)
}
}
fun onChatBoxFilePicked(infos: List<DefaultFileInfo>) {
Timber.d("onChatBoxFilePicked: $infos")
Timber.d("DROID-2966 onChatBoxFilePicked: $infos")
chatBoxAttachments.value += infos.map { info ->
ChatView.Message.ChatBoxAttachment.File(
uri = info.uri,
@ -1451,7 +1460,8 @@ class ChatViewModel @Inject constructor(
data class ChatBoxMediaUri(
val uri: String,
val isVideo: Boolean = false
val isVideo: Boolean = false,
val capturedByCamera: Boolean = false
)
sealed class ViewModelCommand {

View file

@ -0,0 +1,29 @@
package com.anytypeio.anytype.feature_chats.tools
import android.content.Context
import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.core.content.FileProvider
import java.io.File
fun launchCamera(
context: Context,
launcher: ManagedActivityResultLauncher<Uri, Boolean>,
onUriReceived: (Uri) -> Unit
) {
val photoFile = File.createTempFile("IMG_", ".jpg", context.cacheDir).apply {
createNewFile()
deleteOnExit()
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
photoFile
)
onUriReceived(uri)
launcher.launch(uri)
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.feature_chats.ui
import android.Manifest
import android.net.Uri
import android.util.Patterns
import androidx.activity.compose.rememberLauncherForActivityResult
@ -39,6 +40,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -48,7 +50,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -72,9 +74,11 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.ContentMiscChat
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.feature_chats.R
import com.anytypeio.anytype.feature_chats.presentation.ChatView
import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.ChatBoxMode
import com.anytypeio.anytype.feature_chats.tools.launchCamera
import com.anytypeio.anytype.presentation.confgs.ChatConfig
import kotlinx.coroutines.launch
import timber.log.Timber
@ -99,9 +103,12 @@ fun ChatBox(
onExitEditMessageMode: () -> Unit,
onValueChange: (TextFieldValue, List<ChatBoxSpan>) -> Unit,
onUrlInserted: (Url) -> Unit,
onImageCaptured: (Uri) -> Unit
) {
val length = text.text.length
val context = LocalContext.current
// LAUNCHERS
val uploadMediaLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(maxItems = ChatConfig.MAX_ATTACHMENT_COUNT)
@ -115,12 +122,41 @@ fun ChatBox(
onChatBoxFilePicked(uris.take(ChatConfig.MAX_ATTACHMENT_COUNT))
}
var capturedImageUri by rememberSaveable { mutableStateOf<String?>(null) }
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { isSuccess ->
if (isSuccess && capturedImageUri != null) {
onImageCaptured(Uri.parse(capturedImageUri))
capturedImageUri = null
} else {
Timber.w("DROID-2966 Failed to capture image")
}
}
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
launchCamera(
context = context,
launcher = cameraLauncher,
onUriReceived = { capturedImageUri = it.toString() }
)
} else {
context.toast(context.getString(R.string.chat_camera_permission_denied))
}
}
// END OF LAUNCHERS
val length = text.text.length
var showDropdownMenu by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val focus = LocalFocusManager.current
var isFocused by remember { mutableStateOf(false) }
var showMarkup by remember { mutableStateOf(false) }
@ -261,6 +297,22 @@ fun ChatBox(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.chat_box_camera),
color = colorResource(id = R.color.text_primary)
)
},
onClick = {
showDropdownMenu = false
permissionLauncher.launch(Manifest.permission.CAMERA)
}
)
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
text = {
Text(
@ -337,12 +389,14 @@ fun ChatBox(
) {
Text(
text = "${text.text.length} / ${ChatConfig.MAX_MESSAGE_CHARACTER_LIMIT}",
modifier = Modifier.padding(
horizontal = 8.dp,
vertical = 3.dp
).align(
Alignment.Center
),
modifier = Modifier
.padding(
horizontal = 8.dp,
vertical = 3.dp
)
.align(
Alignment.Center
),
style = Caption1Regular,
color = if (length > ChatConfig.MAX_MESSAGE_CHARACTER_LIMIT)
colorResource(R.color.palette_system_red)
@ -487,6 +541,9 @@ fun ChatBox(
uploadMediaLauncher.launch(
PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
onCameraCaptureClicked = {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
)
}
@ -779,13 +836,16 @@ fun ChatBoxEditPanel(
onStyleClicked: () -> Unit,
onMentionClicked: () -> Unit,
onUploadMediaClicked: () -> Unit,
onUploadFileClicked: () -> Unit
onUploadFileClicked: () -> Unit,
onCameraCaptureClicked: () -> Unit
) {
var showDropdownMenu by remember { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth().height(52.dp),
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
verticalAlignment = Alignment.CenterVertically
) {
@ -840,6 +900,22 @@ fun ChatBoxEditPanel(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.chat_box_camera),
color = colorResource(id = R.color.text_primary)
)
},
onClick = {
showDropdownMenu = false
onCameraCaptureClicked()
}
)
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
text = {
Text(
@ -958,6 +1034,7 @@ fun ChatBoxEditPanelPreview() {
onStyleClicked = {},
onAttachObjectClicked = {},
onUploadFileClicked = {},
onUploadMediaClicked = {}
onUploadMediaClicked = {},
onCameraCaptureClicked = {}
)
}

View file

@ -205,7 +205,8 @@ fun ChatScreenPreview() {
onVisibleRangeChanged = { _, _ -> },
onUrlInserted = {},
onGoToMentionClicked = {},
onShareInviteClicked = {}
onShareInviteClicked = {},
onImageCaptured = {}
)
}

View file

@ -3,7 +3,6 @@ package com.anytypeio.anytype.feature_chats.ui
import android.content.res.Configuration
import android.net.Uri
import android.provider.OpenableColumns
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -70,10 +69,8 @@ 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.BUTTON_SECONDARY
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED
import com.anytypeio.anytype.core_ui.foundation.GenericAlert
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
@ -252,7 +249,18 @@ fun ChatScreenWrapper(
canCreateInviteLink = vm.canCreateInviteLink.collectAsStateWithLifecycle().value,
isReadOnly = vm.chatBoxMode
.collectAsStateWithLifecycle()
.value is ChatBoxMode.ReadOnly
.value is ChatBoxMode.ReadOnly,
onImageCaptured = {
vm.onChatBoxMediaPicked(
uris = listOf(
ChatViewModel.ChatBoxMediaUri(
uri = it.toString(),
isVideo = false,
capturedByCamera = true
)
)
)
}
)
LaunchedEffect(Unit) {
vm.uXCommands.collect { command ->
@ -369,6 +377,7 @@ fun ChatScreen(
onMarkupLinkClicked: (String) -> Unit,
onAttachObjectClicked: () -> Unit,
onChatBoxMediaPicked: (List<Uri>) -> Unit,
onImageCaptured: (Uri) -> Unit,
onChatBoxFilePicked: (List<Uri>) -> Unit,
onAddReactionClicked: (String) -> Unit,
onViewChatReaction: (Id, String) -> Unit,
@ -803,13 +812,13 @@ fun ChatScreen(
},
text = text,
spans = spans,
onUrlInserted = onUrlInserted
onUrlInserted = onUrlInserted,
onImageCaptured = onImageCaptured
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Messages(
modifier: Modifier = Modifier,