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:
parent
a4e5d16e16
commit
e384832a9a
8 changed files with 159 additions and 29 deletions
|
@ -34,6 +34,7 @@ dependencies {
|
|||
|
||||
implementation libs.appcompat
|
||||
implementation libs.compose
|
||||
implementation libs.activityCompose
|
||||
implementation libs.composeFoundation
|
||||
implementation libs.composeToolingPreview
|
||||
implementation libs.composeMaterial3
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -205,7 +205,8 @@ fun ChatScreenPreview() {
|
|||
onVisibleRangeChanged = { _, _ -> },
|
||||
onUrlInserted = {},
|
||||
onGoToMentionClicked = {},
|
||||
onShareInviteClicked = {}
|
||||
onShareInviteClicked = {},
|
||||
onImageCaptured = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue