Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.net.Uri
import android.text.Editable
import android.text.InputType
import android.text.Spanned
import android.text.TextWatcher
import android.text.style.ReplacementSpan
import android.view.Gravity
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.view.ContentInfoCompat
import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
import java.io.File
import java.util.UUID
import org.json.JSONObject
import kotlin.math.max

Expand Down Expand Up @@ -451,11 +459,28 @@ private class SelectionAwareEditText(context: Context) : EditText(context) {
var selectionListener: ((Int, Int) -> Unit)? = null
var pasteImagesListener: ((List<String>) -> Unit)? = null

init {
ViewCompat.setOnReceiveContentListener(this, SUPPORTED_IMAGE_MIME_TYPES) { _, payload ->
val cachedImageUris = cacheReceivedImages(payload)
if (cachedImageUris.isEmpty()) {
payload
} else {
pasteImagesListener?.invoke(cachedImageUris)
null
}
}
Comment on lines +463 to +471

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium t3composereditor/T3ComposerEditorView.kt:463

The receive-content listener returns null as soon as it caches at least one image, marking the entire payload as consumed. When a payload contains an image alongside text, unsupported items, or additional images that failed to cache, those remaining items are silently discarded instead of being returned to the platform for default handling. Consider partitioning the payload via payload.partition(...) and returning the unconsumed portion instead of null.

-      val cachedImageUris = cacheReceivedImages(payload)
-      if (cachedImageUris.isEmpty()) {
-        payload
-      } else {
-        pasteImagesListener?.invoke(cachedImageUris)
-        null
-      }
+      val (imagePayload, remainder) = payload.partition { item ->
+        val uri = item.uri
+        if (uri == null) return@partition false
+        val mimeType = context.contentResolver.getType(uri)?.lowercase()
+        mimeType != null && fileExtensionForImageMimeType(mimeType) != null
+      }
+      val cachedImageUris = cacheReceivedImages(imagePayload)
+      if (cachedImageUris.isNotEmpty()) {
+        pasteImagesListener?.invoke(cachedImageUris)
+      }
+      remainder
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt around lines 463-471:

The receive-content listener returns `null` as soon as it caches at least one image, marking the entire payload as consumed. When a payload contains an image alongside text, unsupported items, or additional images that failed to cache, those remaining items are silently discarded instead of being returned to the platform for default handling. Consider partitioning the payload via `payload.partition(...)` and returning the unconsumed portion instead of `null`.

}

override fun onSelectionChanged(selStart: Int, selEnd: Int) {
super.onSelectionChanged(selStart, selEnd)
selectionListener?.invoke(selStart, selEnd)
}

override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val inputConnection = super.onCreateInputConnection(outAttrs) ?: return null
return InputConnectionCompat.createWrapper(this, inputConnection, outAttrs)
}

override fun onTextContextMenuItem(id: Int): Boolean {
if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
Expand All @@ -477,4 +502,56 @@ private class SelectionAwareEditText(context: Context) : EditText(context) {
}
return super.onTextContextMenuItem(id)
}

private fun cacheReceivedImages(payload: ContentInfoCompat): List<String> = buildList {
for (index in 0 until payload.clip.itemCount) {
val uri = payload.clip.getItemAt(index).uri ?: continue
val mimeType = context.contentResolver.getType(uri)?.lowercase() ?: continue
val extension = fileExtensionForImageMimeType(mimeType) ?: continue
cacheReceivedImage(uri, extension)?.let(::add)
}
}

private fun cacheReceivedImage(uri: Uri, extension: String): String? {
val pasteDirectory = File(context.cacheDir, OWNED_PASTED_IMAGE_DIRECTORY)
if (!pasteDirectory.exists() && !pasteDirectory.mkdirs()) {
return null
}

val destination = File(pasteDirectory, "${UUID.randomUUID()}.$extension")
return try {
val input = context.contentResolver.openInputStream(uri) ?: return null
input.use { source ->
destination.outputStream().use { output ->
source.copyTo(output)
}
}
Uri.fromFile(destination).toString()
} catch (_: Exception) {
destination.delete()
null
}
}

private companion object {
const val OWNED_PASTED_IMAGE_DIRECTORY = "t3-composer-paste"

val SUPPORTED_IMAGE_MIME_TYPES = arrayOf(
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/heic",
"image/heif",
)

fun fileExtensionForImageMimeType(mimeType: String): String? = when (mimeType) {
"image/png" -> "png"
"image/jpeg", "image/jpg" -> "jpg"
"image/gif" -> "gif"
"image/webp" -> "webp"
"image/heic", "image/heif" -> "heic"
else -> null
}
}
}
Loading