diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index f4f748ef0f..41349654be 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import com.google.accompanist.insets.navigationBarsWithImePadding actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() @@ -14,3 +15,11 @@ actual fun ProvideWindowInsets( ) { com.google.accompanist.insets.ProvideWindowInsets(content = content) } + +@Composable +actual fun Modifier.desktopOnExternalDrag( + enabled: Boolean, + onFiles: (List) -> Unit, + onImage: (Painter) -> Unit, + onText: (String) -> Unit +): Modifier = this diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index a5c455d989..543444d0eb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -2,6 +2,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter expect fun Modifier.navigationBarsWithImePadding(): Modifier @@ -11,3 +12,11 @@ expect fun ProvideWindowInsets( windowInsetsAnimationsEnabled: Boolean = true, content: @Composable () -> Unit ) + +@Composable +expect fun Modifier.desktopOnExternalDrag( + enabled: Boolean = true, + onFiles: (List) -> Unit = {}, + onImage: (Painter) -> Unit = {}, + onText: (String) -> Unit = {} +): Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 0f7dd1a081..ce7765bcd1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -11,8 +11,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* @@ -410,6 +409,31 @@ fun ChatLayout( Box( Modifier .fillMaxWidth() + .desktopOnExternalDrag( + enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value, + onFiles = { paths -> + val uris = paths.map { URI.create(it) } + val groups = uris.groupBy { isImage(it) } + val images = groups[true] ?: emptyList() + val files = groups[false] ?: emptyList() + if (images.isNotEmpty()) { + composeState.processPickedMedia(images, null) + } else if (files.isNotEmpty()) { + composeState.processPickedFile(uris.first(), null) + } + }, + onImage = { + val tmpFile = File.createTempFile("image", ".bmp", tmpDir) + tmpFile.deleteOnExit() + chatModel.filesToDelete.add(tmpFile) + val uri = tmpFile.toURI() + composeState.processPickedMedia(listOf(uri), null) + }, + onText = { + // Need to parse HTML in order to correctly display the content + //composeState.value = composeState.value.copy(message = composeState.value.message + it) + }, + ) ) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index d4c84624af..677465349f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -124,6 +124,8 @@ data class ComposeState( } } +private val maxFileSize = getMaxFileSize(FileProtocol.XFTP) + sealed class RecordingState { object NotStarted: RecordingState() class Started(val filePath: String, val progressMs: Int = 0): RecordingState() @@ -155,6 +157,66 @@ expect fun AttachmentSelection( processPickedMedia: (List, String?) -> Unit ) +fun MutableState.processPickedFile(uri: URI?, text: String?) { + if (uri != null) { + val fileSize = getFileSize(uri) + if (fileSize != null && fileSize <= maxFileSize) { + val fileName = getFileName(uri) + if (fileName != null) { + value = value.copy(message = text ?: value.message, preview = ComposePreview.FilePreview(fileName, uri)) + } + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) + ) + } + } +} + +fun MutableState.processPickedMedia(uris: List, text: String?) { + val content = ArrayList() + val imagesPreview = ArrayList() + uris.forEach { uri -> + var bitmap: ImageBitmap? + when { + isImage(uri) -> { + // Image + val drawable = getDrawableFromUri(uri) + bitmap = getBitmapFromUri(uri) + if (isAnimImage(uri, drawable)) { + // It's a gif or webp + val fileSize = getFileSize(uri) + if (fileSize != null && fileSize <= maxFileSize) { + content.add(UploadContent.AnimatedImage(uri)) + } else { + bitmap = null + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) + ) + } + } else { + content.add(UploadContent.SimpleImage(uri)) + } + } + else -> { + // Video + val res = getBitmapFromVideo(uri) + bitmap = res.preview + val durationMs = res.duration + content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) + } + } + if (bitmap != null) { + imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) + } + } + if (imagesPreview.isNotEmpty()) { + value = value.copy(message = text ?: value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) + } +} + @Composable fun ComposeView( chatModel: ChatModel, @@ -168,70 +230,11 @@ fun ComposeView( val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } val cancelledLinks = rememberSaveable { mutableSetOf() } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val maxFileSize = getMaxFileSize(FileProtocol.XFTP) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } - val processPickedMedia = { uris: List, text: String? -> - val content = ArrayList() - val imagesPreview = ArrayList() - uris.forEach { uri -> - var bitmap: ImageBitmap? - when { - isImage(uri) -> { - // Image - val drawable = getDrawableFromUri(uri) - bitmap = getBitmapFromUri(uri) - if (isAnimImage(uri, drawable)) { - // It's a gif or webp - val fileSize = getFileSize(uri) - if (fileSize != null && fileSize <= maxFileSize) { - content.add(UploadContent.AnimatedImage(uri)) - } else { - bitmap = null - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) - ) - } - } else { - content.add(UploadContent.SimpleImage(uri)) - } - } - else -> { - // Video - val res = getBitmapFromVideo(uri) - bitmap = res.preview - val durationMs = res.duration - content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) - } - } - if (bitmap != null) { - imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) - } - } - if (imagesPreview.isNotEmpty()) { - composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) - } - } - val processPickedFile = { uri: URI?, text: String? -> - if (uri != null) { - val fileSize = getFileSize(uri) - if (fileSize != null && fileSize <= maxFileSize) { - val fileName = getFileName(uri) - if (fileName != null) { - composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri)) - } - } else { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) - ) - } - } - } val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } - AttachmentSelection(composeState, attachmentOption, processPickedFile, processPickedMedia) + AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile, composeState::processPickedMedia) fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) @@ -620,8 +623,8 @@ fun ComposeView( when (val shared = chatModel.sharedContent.value) { is SharedContent.Text -> onMessageChange(shared.text) - is SharedContent.Media -> processPickedMedia(shared.uris, shared.text) - is SharedContent.File -> processPickedFile(shared.uri, shared.text) + is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text) + is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text) null -> {} } chatModel.sharedContent.value = null diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 72e163c2ed..0185a50fcf 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -1,7 +1,8 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.ui.* +import androidx.compose.ui.graphics.painter.Painter actual fun Modifier.navigationBarsWithImePadding(): Modifier = this @@ -13,3 +14,18 @@ actual fun ProvideWindowInsets( ) { content() } + +@Composable +actual fun Modifier.desktopOnExternalDrag( + enabled: Boolean, + onFiles: (List) -> Unit, + onImage: (Painter) -> Unit, + onText: (String) -> Unit +): Modifier = +onExternalDrag(enabled) { + when(val data = it.dragData) { + is DragData.FilesList -> onFiles(data.readFiles()) + is DragData.Image -> onImage(data.readImage()) + is DragData.Text -> onText(data.readText()) + } +}