mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix image display, implement expressive file upload
This commit is contained in:
@@ -2,6 +2,8 @@ package ru.fromchat.api
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import java.io.File
|
||||||
|
import org.json.JSONObject
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
@@ -17,12 +19,18 @@ import com.pr0gramm3r101.utils.settings.settings
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import ru.fromchat.crypto.transport.TransportCiphertext
|
import ru.fromchat.crypto.transport.TransportCiphertext
|
||||||
import ru.fromchat.crypto.transport.TransportCrypto
|
import ru.fromchat.crypto.transport.TransportCrypto
|
||||||
|
|
||||||
private const val INLINE_UPLOAD_THRESHOLD_BYTES = 512 * 1024
|
private const val INLINE_UPLOAD_THRESHOLD_BYTES = 512 * 1024
|
||||||
private const val DEFAULT_CHUNK_SIZE = 262_144
|
private const val DEFAULT_CHUNK_SIZE = 262_144
|
||||||
|
|
||||||
|
private fun dmBlobCacheFile(context: Context, jobId: String): File =
|
||||||
|
File(context.cacheDir, "dm_resumable_$jobId.blob")
|
||||||
|
|
||||||
|
private fun dmTransportCipherPrefsKey(jobId: String): String = "dm_upload_transport_cipher_$jobId"
|
||||||
|
|
||||||
private object AttachmentUploadEvents {
|
private object AttachmentUploadEvents {
|
||||||
val flow = MutableSharedFlow<AttachmentUploadProgress>(extraBufferCapacity = 64)
|
val flow = MutableSharedFlow<AttachmentUploadProgress>(extraBufferCapacity = 64)
|
||||||
}
|
}
|
||||||
@@ -68,7 +76,12 @@ actual object AttachmentUploadQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun cancel(jobId: String) {
|
actual fun cancel(jobId: String) {
|
||||||
WorkManager.getInstance(UtilsLibrary.context).cancelUniqueWork(uniqueWorkName(jobId))
|
val ctx = UtilsLibrary.context
|
||||||
|
dmBlobCacheFile(ctx, jobId).delete()
|
||||||
|
runBlocking {
|
||||||
|
settings.putString(dmTransportCipherPrefsKey(jobId), "")
|
||||||
|
}
|
||||||
|
WorkManager.getInstance(ctx).cancelUniqueWork(uniqueWorkName(jobId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun uniqueWorkName(jobId: String): String = "dm-attachment-upload-$jobId"
|
private fun uniqueWorkName(jobId: String): String = "dm-attachment-upload-$jobId"
|
||||||
@@ -101,29 +114,49 @@ class DmAttachmentUploadWorker(
|
|||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
emitProgress(jobId, 0)
|
emitProgress(jobId, 0)
|
||||||
|
|
||||||
|
val prepared = loadPreparedTransportAndBlob(applicationContext, jobId)
|
||||||
|
val encryptedBlob: ByteArray
|
||||||
|
val msgCipher: TransportCiphertext
|
||||||
|
|
||||||
|
if (prepared != null) {
|
||||||
|
encryptedBlob = prepared.first
|
||||||
|
msgCipher = prepared.second
|
||||||
|
} else {
|
||||||
|
val staleUploadId = settings.getString(uploadIdKey(jobId), "").ifBlank { null }
|
||||||
|
if (staleUploadId != null) {
|
||||||
|
runCatching { ApiClient.abortDmUpload(staleUploadId) }
|
||||||
|
settings.putString(uploadIdKey(jobId), "")
|
||||||
|
}
|
||||||
|
|
||||||
val transportKey = ApiClient.getTransportPublicKey()
|
val transportKey = ApiClient.getTransportPublicKey()
|
||||||
val (msgCipher, ephemeralSecret) = TransportCrypto.encryptWithTransportKeyWithEphemeralSecret(
|
val (freshCipher, ephemeralSecret) = TransportCrypto.encryptWithTransportKeyWithEphemeralSecret(
|
||||||
plaintext = plaintext,
|
plaintext = plaintext,
|
||||||
transportPublicKeyB64 = transportKey.publicKeyB64
|
transportPublicKeyB64 = transportKey.publicKeyB64
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
val bytes = applicationContext.contentResolver.openInputStream(Uri.parse(fileUri))?.use { it.readBytes() }
|
val bytes = applicationContext.contentResolver.openInputStream(Uri.parse(fileUri))?.use { it.readBytes() }
|
||||||
?: error("Failed to read file from URI")
|
?: error("Failed to read file from URI")
|
||||||
val encryptedBlob = TransportCrypto.encryptFileForTransport(
|
val blob = TransportCrypto.encryptFileForTransport(
|
||||||
fileBytes = bytes,
|
fileBytes = bytes,
|
||||||
transportPublicKeyB64 = transportKey.publicKeyB64,
|
transportPublicKeyB64 = transportKey.publicKeyB64,
|
||||||
ephemeralSecretKey = ephemeralSecret
|
ephemeralSecretKey = ephemeralSecret
|
||||||
)
|
)
|
||||||
|
encryptedBlob = blob
|
||||||
|
msgCipher = freshCipher
|
||||||
|
savePreparedTransportAndBlob(applicationContext, jobId, encryptedBlob, msgCipher)
|
||||||
|
} finally {
|
||||||
|
ephemeralSecret.fill(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (encryptedBlob.size <= INLINE_UPLOAD_THRESHOLD_BYTES) {
|
if (encryptedBlob.size <= INLINE_UPLOAD_THRESHOLD_BYTES) {
|
||||||
sendInline(jobId, recipientId, plaintext, filename, encryptedBlob, msgCipher)
|
sendInline(jobId, recipientId, plaintext, filename, encryptedBlob, msgCipher)
|
||||||
} else {
|
} else {
|
||||||
sendResumable(jobId, recipientId, plaintext, filename, encryptedBlob, msgCipher)
|
sendResumable(jobId, recipientId, plaintext, filename, encryptedBlob, msgCipher)
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
ephemeralSecret.fill(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
clearPreparedTransportAndBlob(applicationContext, jobId)
|
||||||
clearResumableState(jobId)
|
clearResumableState(jobId)
|
||||||
AttachmentUploadEvents.flow.tryEmit(AttachmentUploadProgress.Success(jobId))
|
AttachmentUploadEvents.flow.tryEmit(AttachmentUploadProgress.Success(jobId))
|
||||||
Result.success()
|
Result.success()
|
||||||
@@ -216,5 +249,46 @@ class DmAttachmentUploadWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun uploadIdKey(jobId: String): String = "dm_upload_id_$jobId"
|
private fun uploadIdKey(jobId: String): String = "dm_upload_id_$jobId"
|
||||||
|
|
||||||
|
private suspend fun savePreparedTransportAndBlob(
|
||||||
|
context: Context,
|
||||||
|
jobId: String,
|
||||||
|
blob: ByteArray,
|
||||||
|
cipher: TransportCiphertext
|
||||||
|
) {
|
||||||
|
val f = dmBlobCacheFile(context, jobId)
|
||||||
|
f.outputStream().use { it.write(blob) }
|
||||||
|
val json = JSONObject().apply {
|
||||||
|
put("clientPublicKeyB64", cipher.clientPublicKeyB64)
|
||||||
|
put("nonceB64", cipher.nonceB64)
|
||||||
|
put("ciphertextB64", cipher.ciphertextB64)
|
||||||
|
}
|
||||||
|
settings.putString(dmTransportCipherPrefsKey(jobId), json.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPreparedTransportAndBlob(
|
||||||
|
context: Context,
|
||||||
|
jobId: String
|
||||||
|
): Pair<ByteArray, TransportCiphertext>? {
|
||||||
|
val f = dmBlobCacheFile(context, jobId)
|
||||||
|
val raw = settings.getString(dmTransportCipherPrefsKey(jobId), "").ifBlank { return null }
|
||||||
|
if (!f.isFile || f.length() == 0L) return null
|
||||||
|
return try {
|
||||||
|
val o = JSONObject(raw)
|
||||||
|
val cipher = TransportCiphertext(
|
||||||
|
clientPublicKeyB64 = o.getString("clientPublicKeyB64"),
|
||||||
|
nonceB64 = o.getString("nonceB64"),
|
||||||
|
ciphertextB64 = o.getString("ciphertextB64")
|
||||||
|
)
|
||||||
|
f.readBytes() to cipher
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun clearPreparedTransportAndBlob(context: Context, jobId: String) {
|
||||||
|
dmBlobCacheFile(context, jobId).delete()
|
||||||
|
settings.putString(dmTransportCipherPrefsKey(jobId), "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AttachFile
|
import androidx.compose.material.icons.filled.AttachFile
|
||||||
@@ -33,6 +34,7 @@ import androidx.compose.material3.CircularWavyProgressIndicator
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
import androidx.compose.material3.WavyProgressIndicatorDefaults
|
import androidx.compose.material3.WavyProgressIndicatorDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -47,14 +49,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.CompositingStrategy
|
|
||||||
import androidx.compose.ui.graphics.Paint
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.withSaveLayer
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
@@ -111,31 +108,24 @@ fun AttachmentPreview(
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
val isFile = file != null && !isImage
|
|
||||||
val isImageWithThumb = file != null && isImage && dmEnvelope != null && !fileThumbnail.isNullOrBlank()
|
val isImageWithThumb = file != null && isImage && dmEnvelope != null && !fileThumbnail.isNullOrBlank()
|
||||||
val isPendingImage = pendingFileUri != null && isImage
|
val isPendingImage = pendingFileUri != null && isImage
|
||||||
val isPendingFile = pendingFileUri != null && !isImage
|
val isPendingFile = pendingFileUri != null && !isImage
|
||||||
|
|
||||||
when {
|
when {
|
||||||
isFile -> {
|
(file != null && !isImage) || isPendingFile -> {
|
||||||
FileIconContent(
|
ExpressiveFileAttachmentRow(
|
||||||
filename = file.name,
|
filename = file?.name
|
||||||
|
?: pendingFilename?.takeIf { it.isNotBlank() }
|
||||||
|
?: pendingFileUri?.substringAfterLast("/")
|
||||||
|
?.substringBefore("?")
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: "File",
|
||||||
sizeBytes = fileSizeBytes,
|
sizeBytes = fileSizeBytes,
|
||||||
onClick = onFileClick,
|
onClick = if (file != null) onFileClick else null,
|
||||||
isAuthor = isAuthor,
|
isAuthor = isAuthor,
|
||||||
isUploading = false,
|
isUploading = isPendingFile && isUploading,
|
||||||
uploadProgress = null,
|
uploadProgress = if (isPendingFile) uploadProgress else null,
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isPendingFile -> {
|
|
||||||
FileIconContent(
|
|
||||||
filename = pendingFilename ?: "File",
|
|
||||||
sizeBytes = null,
|
|
||||||
onClick = null,
|
|
||||||
isAuthor = isAuthor,
|
|
||||||
isUploading = isUploading,
|
|
||||||
uploadProgress = uploadProgress,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -357,7 +347,9 @@ private fun UploadingImageOverlay(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ExpressiveUploadIndicator(
|
private fun ExpressiveUploadIndicator(
|
||||||
uploadProgress: Int?,
|
uploadProgress: Int?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
indicatorColor: Color? = null,
|
||||||
|
trackColorOverride: Color? = null
|
||||||
) {
|
) {
|
||||||
val clampedProgress = uploadProgress?.coerceIn(0, 100)
|
val clampedProgress = uploadProgress?.coerceIn(0, 100)
|
||||||
val indeterminate = clampedProgress == null || clampedProgress == 0
|
val indeterminate = clampedProgress == null || clampedProgress == 0
|
||||||
@@ -377,15 +369,15 @@ private fun ExpressiveUploadIndicator(
|
|||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
label = "uploadProgress"
|
label = "uploadProgress"
|
||||||
)
|
)
|
||||||
val primary = MaterialTheme.colorScheme.primary
|
val primary = indicatorColor ?: MaterialTheme.colorScheme.primary
|
||||||
val trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.28f)
|
val trackColor = trackColorOverride
|
||||||
|
?: MaterialTheme.colorScheme.onSurface.copy(alpha = 0.28f)
|
||||||
val defaultIndicatorAmplitude = WavyProgressIndicatorDefaults.indicatorAmplitude
|
val defaultIndicatorAmplitude = WavyProgressIndicatorDefaults.indicatorAmplitude
|
||||||
val ringModifier = Modifier.size(56.dp)
|
|
||||||
|
|
||||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
if (indeterminate) {
|
if (indeterminate) {
|
||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
modifier = ringModifier,
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = primary,
|
color = primary,
|
||||||
trackColor = trackColor,
|
trackColor = trackColor,
|
||||||
amplitude = animatedWaveStrength
|
amplitude = animatedWaveStrength
|
||||||
@@ -393,7 +385,7 @@ private fun ExpressiveUploadIndicator(
|
|||||||
} else {
|
} else {
|
||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
progress = { animatedProgress },
|
progress = { animatedProgress },
|
||||||
modifier = ringModifier,
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = primary,
|
color = primary,
|
||||||
trackColor = trackColor,
|
trackColor = trackColor,
|
||||||
amplitude = { p -> animatedWaveStrength * defaultIndicatorAmplitude(p) }
|
amplitude = { p -> animatedWaveStrength * defaultIndicatorAmplitude(p) }
|
||||||
@@ -658,101 +650,95 @@ private fun formatFileSize(bytes: Long): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FileIconContent(
|
private fun ExpressiveFileAttachmentRow(
|
||||||
filename: String,
|
filename: String,
|
||||||
sizeBytes: Long?,
|
sizeBytes: Long?,
|
||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
isAuthor: Boolean,
|
isAuthor: Boolean,
|
||||||
isUploading: Boolean = false,
|
isUploading: Boolean,
|
||||||
uploadProgress: Int? = null,
|
uploadProgress: Int?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val contentColor = if (isAuthor) Color.White else MaterialTheme.colorScheme.onSurface
|
val headlineColor = if (isAuthor) Color.White else MaterialTheme.colorScheme.onSurface
|
||||||
val circleBackground = if (isAuthor) Color.White else MaterialTheme.colorScheme.primary
|
val supportingColor = if (isAuthor) {
|
||||||
val iconTint = if (isAuthor) MaterialTheme.colorScheme.primary else Color.White
|
Color.White.copy(alpha = 0.78f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
val leadingSize = 48.dp
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.widthIn(max = 240.dp)
|
.widthIn(max = 268.dp)
|
||||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier),
|
.then(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
if (onClick != null && !isUploading) {
|
||||||
) {
|
Modifier.clickable(onClick = onClick)
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.size(40.dp).then(
|
|
||||||
if (isAuthor) {
|
|
||||||
Modifier
|
|
||||||
.graphicsLayer {
|
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
|
||||||
}
|
|
||||||
.drawWithContent {
|
|
||||||
drawCircle(
|
|
||||||
color = circleBackground,
|
|
||||||
radius = size.minDimension / 2f,
|
|
||||||
center = center
|
|
||||||
)
|
|
||||||
drawContext.canvas.withSaveLayer(
|
|
||||||
bounds = Rect(0f, 0f, size.width, size.height),
|
|
||||||
paint = Paint().apply { blendMode = BlendMode.DstOut }
|
|
||||||
) {
|
|
||||||
drawContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
.background(circleBackground, RoundedCornerShape(20.dp))
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(leadingSize),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isUploading) {
|
||||||
|
ExpressiveUploadIndicator(
|
||||||
|
uploadProgress = uploadProgress,
|
||||||
|
modifier = Modifier.size(leadingSize),
|
||||||
|
indicatorColor = if (isAuthor) Color.White else null,
|
||||||
|
trackColorOverride = if (isAuthor) {
|
||||||
|
Color.White.copy(alpha = 0.28f)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = if (isAuthor) {
|
||||||
|
Color.White.copy(alpha = 0.22f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(leadingSize)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Download,
|
imageVector = Icons.Rounded.Download,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(26.dp),
|
||||||
tint = if (isAuthor) Color.White else iconTint
|
tint = if (isAuthor) {
|
||||||
)
|
Color.White
|
||||||
}
|
|
||||||
if (isUploading) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.surface.copy(alpha = 0.6f),
|
|
||||||
RoundedCornerShape(20.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (uploadProgress != null) {
|
|
||||||
DeterminateCircularProgress(
|
|
||||||
progress = uploadProgress,
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
IndefiniteCircularProgress(modifier = Modifier.size(28.dp))
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(start = 12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = filename.take(70) + if (filename.length > 70) "…" else "",
|
text = filename.take(70) + if (filename.length > 70) "…" else "",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
color = contentColor,
|
color = headlineColor,
|
||||||
maxLines = 2
|
maxLines = 2
|
||||||
)
|
)
|
||||||
if (sizeBytes != null) {
|
if (sizeBytes != null) {
|
||||||
Text(
|
Text(
|
||||||
text = formatFileSize(sizeBytes),
|
text = formatFileSize(sizeBytes),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontSize = 12.sp,
|
color = supportingColor
|
||||||
color = contentColor.copy(alpha = 0.8f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,11 @@ fun MessageItem(
|
|||||||
} else {
|
} else {
|
||||||
message.pendingFileAspectRatio
|
message.pendingFileAspectRatio
|
||||||
},
|
},
|
||||||
fileSizeBytes = if (pendingImageFile != null) message.fileSizes?.firstOrNull() else null,
|
fileSizeBytes = when {
|
||||||
|
pendingImageFile != null -> message.fileSizes?.firstOrNull()
|
||||||
|
!isPendingImage -> message.fileSizes?.firstOrNull()
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
messageId = if (pendingImageFile != null && isPendingImage) message.id else null,
|
messageId = if (pendingImageFile != null && isPendingImage) message.id else null,
|
||||||
fileIndex = if (pendingImageFile != null && isPendingImage) 0 else null,
|
fileIndex = if (pendingImageFile != null && isPendingImage) 0 else null,
|
||||||
onFileClick = null,
|
onFileClick = null,
|
||||||
@@ -343,7 +347,7 @@ fun MessageItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
message.files?.forEachIndexed { index, file ->
|
message.files?.forEachIndexed { index, file ->
|
||||||
if (hasPendingServerImage && index == 0) return@forEachIndexed
|
if (message.pendingFileUri != null && index == 0) return@forEachIndexed
|
||||||
val isImage = isImageFilename(file.name)
|
val isImage = isImageFilename(file.name)
|
||||||
val imageKey = if (isImage) "img_${message.id}_$index" else null
|
val imageKey = if (isImage) "img_${message.id}_$index" else null
|
||||||
val isFirstImage = index == 0 && isImage
|
val isFirstImage = index == 0 && isImage
|
||||||
|
|||||||
Reference in New Issue
Block a user