android wip

This commit is contained in:
JRoberts
2022-04-13 22:07:57 +04:00
parent 1a869460b8
commit 1a4fbec895
16 changed files with 329 additions and 90 deletions
@@ -517,7 +517,8 @@ data class ChatItem (
val meta: CIMeta,
val content: CIContent,
val formattedText: List<FormattedText>? = null,
val quotedItem: CIQuote? = null
val quotedItem: CIQuote? = null,
val file: CIFile? = null
) {
val id: Long get() = meta.itemId
val timestampText: String get() = meta.timestampText
@@ -549,6 +550,7 @@ data class ChatItem (
text: String = "hello\nthere",
status: CIStatus = CIStatus.SndNew(),
quotedItem: CIQuote? = null,
file: CIFile? = null,
itemDeleted: Boolean = false,
itemEdited: Boolean = false,
editable: Boolean = true
@@ -557,7 +559,8 @@ data class ChatItem (
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem
quotedItem = quotedItem,
file = file
)
fun getDeletedContentSampleData(
@@ -569,9 +572,10 @@ data class ChatItem (
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, false, false, false),
meta = CIMeta.getSample(id, ts, text, status, itemDeleted = false, itemEdited = false, editable = false),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null
quotedItem = null,
file = null
)
}
}
@@ -699,18 +703,6 @@ sealed class CIContent: ItemContent {
override val text get() = "deleted"
override val msgContent get() = null
}
@Serializable @SerialName("sndFileInvitation")
class SndFileInvitation(val fileId: Long, val filePath: String): CIContent() {
override val text get() = "sending files is not supported yet"
override val msgContent get() = null
}
@Serializable @SerialName("rcvFileInvitation")
class RcvFileInvitation(val rcvFileTransfer: RcvFileTransfer): CIContent() {
override val text get() = "receiving files is not supported yet"
override val msgContent get() = null
}
}
@Serializable
@@ -738,6 +730,37 @@ class CIQuote (
}
}
@Serializable
class CIFile(
val fileId: Long,
val fileName: String,
val fileSize: Long,
val filePath: String? = null,
val fileStatus: CIFileStatus
) {
val stored: Boolean = when (fileStatus) {
CIFileStatus.SndStored -> true
CIFileStatus.SndCancelled -> true
CIFileStatus.RcvComplete -> true
else -> false
}
companion object {
fun getSample(fileId: Long, fileName: String, fileSize: Long, filePath: String?, fileStatus: CIFileStatus = CIFileStatus.SndStored): CIFile =
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus)
}
}
@Serializable
enum class CIFileStatus {
@SerialName("snd_stored") SndStored,
@SerialName("snd_cancelled") SndCancelled,
@SerialName("rcv_invitation") RcvInvitation,
@SerialName("rcv_transfer") RcvTransfer,
@SerialName("rcv_complete") RcvComplete,
@SerialName("rcv_cancelled") RcvCancelled;
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(with = MsgContentSerializer::class)
sealed class MsgContent {
@@ -749,12 +772,16 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class)
class MCLink(override val text: String, val preview: LinkPreview): MsgContent()
@Serializable(with = MsgContentSerializer::class)
class MCImage(override val text: String, val image: String): MsgContent()
@Serializable(with = MsgContentSerializer::class)
class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
val cmdString: String get() = when (this) {
is MCText -> "text $text"
is MCLink -> "json ${json.encodeToString(this)}"
is MCImage -> "json ${json.encodeToString(this)}"
is MCUnknown -> "json $json"
}
}
@@ -769,6 +796,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
element<String>("text")
element<String>("preview")
})
element("MCImage", buildClassSerialDescriptor("MCImage") {
element<String>("text")
element<String>("image")
})
element("MCUnknown", buildClassSerialDescriptor("MCUnknown"))
}
@@ -785,6 +816,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
val preview = Json.decodeFromString<LinkPreview>(json["preview"].toString())
MsgContent.MCLink(text, preview)
}
"image" -> {
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
MsgContent.MCImage(text, image)
}
else -> MsgContent.MCUnknown(t, text, json)
}
} else {
@@ -809,6 +844,12 @@ object MsgContentSerializer : KSerializer<MsgContent> {
put("text", value.text)
put("preview", json.encodeToJsonElement(value.preview))
}
is MsgContent.MCImage ->
buildJsonObject {
put("type", "image")
put("text", value.text)
put("image", value.image)
}
is MsgContent.MCUnknown -> value.json
}
encoder.encodeJsonElement(json)
@@ -876,6 +917,3 @@ enum class FormatColor(val color: String) {
white -> MaterialTheme.colors.onBackground
}
}
@Serializable
class RcvFileTransfer
@@ -141,9 +141,8 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
return null
}
suspend fun apiSendMessage(type: ChatType, id: Long, quotedItemId: Long? = null, mc: MsgContent): AChatItem? {
val cmd = if (quotedItemId == null) CC.ApiSendMessage(type, id, mc)
else CC.ApiSendMessageQuote(type, id, quotedItemId, mc)
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc)
val r = sendCmd(cmd)
if (r is CR.NewChatItem ) return r.chatItem
Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}")
@@ -479,8 +478,7 @@ sealed class CC {
class StartChat: CC()
class ApiGetChats: CC()
class ApiGetChat(val type: ChatType, val id: Long): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC()
class ApiSendMessageQuote(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class GetUserSMPServers(): CC()
@@ -504,8 +502,13 @@ sealed class CC {
is StartChat -> "/_start"
is ApiGetChats -> "/_get chats"
is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100"
is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}"
is ApiSendMessageQuote -> "/_send_quote ${chatRef(type, id)} $itemId ${mc.cmdString}"
is ApiSendMessage -> when {
file == null && quotedItemId == null -> "/_send ${chatRef(type, id)} ${mc.cmdString}"
file != null && quotedItemId == null -> "/_send ${chatRef(type, id)} file $file ${mc.cmdString}"
file == null && quotedItemId != null -> "/_send ${chatRef(type, id)} quoted $quotedItemId ${mc.cmdString}"
file != null && quotedItemId != null -> "/_send ${chatRef(type, id)} file $file quoted $quotedItemId ${mc.cmdString}"
else -> throw Exception()
}
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is GetUserSMPServers -> "/smp_servers"
@@ -531,7 +534,6 @@ sealed class CC {
is ApiGetChats -> "apiGetChats"
is ApiGetChat -> "apiGetChat"
is ApiSendMessage -> "apiSendMessage"
is ApiSendMessageQuote -> "apiSendMessageQuote"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
is GetUserSMPServers -> "getUserSMPServers"
@@ -3,8 +3,7 @@ package chat.simplex.app.views
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
@@ -44,13 +43,15 @@ fun TerminalLayout(terminalItems: List<TerminalItem>, close: () -> Unit, sendCom
Scaffold(
topBar = { CloseSheetBar(close) },
bottomBar = {
SendMsgView(
msg = remember { mutableStateOf("") },
linkPreview = remember { mutableStateOf(null) },
cancelledLinks = remember { mutableSetOf() },
parseMarkdown = { null },
sendMessage = sendCommand
)
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(
msg = remember { mutableStateOf("") },
linkPreview = remember { mutableStateOf(null) },
cancelledLinks = remember { mutableSetOf() },
parseMarkdown = { null },
sendMessage = sendCommand
)
}
},
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
@@ -1,12 +1,15 @@
package chat.simplex.app.views.chat
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBackIos
@@ -16,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
@@ -33,6 +37,8 @@ import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
import java.io.File
import java.io.FileOutputStream
@Composable
fun ChatView(chatModel: ChatModel) {
@@ -41,9 +47,12 @@ fun ChatView(chatModel: ChatModel) {
if (chat == null || user == null) {
chatModel.chatId.value = null
} else {
val context = LocalContext.current
val quotedItem = remember { mutableStateOf<ChatItem?>(null) }
val editingItem = remember { mutableStateOf<ChatItem?>(null) }
val linkPreview = remember { mutableStateOf<LinkPreview?>(null) }
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val imagePreview = remember { mutableStateOf<String?>(null) }
var msg = remember { mutableStateOf("") }
BackHandler { chatModel.chatId.value = null }
@@ -62,7 +71,7 @@ fun ChatView(chatModel: ChatModel) {
}
}
}
ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem, linkPreview,
ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem, linkPreview, chosenImage, imagePreview,
back = { chatModel.chatId.value = null },
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
openDirectChat = { contactId ->
@@ -85,12 +94,28 @@ fun ChatView(chatModel: ChatModel) {
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
} else {
var file: String? = null
val imagePreviewData = imagePreview.value
val chosenImageData = chosenImage.value
val linkPreviewData = linkPreview.value
val mc = when {
imagePreviewData != null && chosenImageData != null -> {
file = saveImage(context, chosenImageData)
MsgContent.MCImage(msg, imagePreviewData)
}
linkPreviewData != null -> {
MsgContent.MCLink(msg, linkPreviewData)
}
else -> {
MsgContent.MCText(msg)
}
}
val newItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quotedItem.value?.meta?.itemId,
mc = if (linkPreviewData != null) MsgContent.MCLink(msg, linkPreviewData) else MsgContent.MCText(msg)
mc = mc
)
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
}
@@ -113,11 +138,23 @@ fun ChatView(chatModel: ChatModel) {
if (toItem != null) chatModel.removeChatItem(cInfo, toItem.chatItem)
}
},
parseMarkdown = { text -> runBlocking { chatModel.controller.apiParseMarkdown(text) } }
parseMarkdown = { text -> runBlocking { chatModel.controller.apiParseMarkdown(text) } },
onImageChange = { bitmap -> imagePreview.value = resizeImageToDataSize(bitmap, maxDataSize = 12500) }
)
}
}
fun saveImage(context: Context, image: Bitmap): String {
val imageResized = base64ToBitmap(resizeImageToDataSize(image, 160000))
val imageName = "image_${System.currentTimeMillis()}.jpg"
val file = File(context.filesDir, imageName)
val output = FileOutputStream(file)
imageResized.compress(Bitmap.CompressFormat.JPEG, 100, output)
output.flush()
output.close()
return file.absolutePath
}
@Composable
fun ChatLayout(
user: User,
@@ -127,27 +164,53 @@ fun ChatLayout(
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
linkPreview: MutableState<LinkPreview?>,
chosenImage: MutableState<Bitmap?>,
imagePreview: MutableState<String?>,
back: () -> Unit,
info: () -> Unit,
openDirectChat: (Long) -> Unit,
sendMessage: (String) -> Unit,
resetMessage: () -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
parseMarkdown: (String) -> List<FormattedText>?
parseMarkdown: (String) -> List<FormattedText>?,
onImageChange: (Bitmap) -> Unit
) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
Surface(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(
topBar = { ChatInfoToolbar(chat, back, info) },
bottomBar = { ComposeView(msg, quotedItem, editingItem, linkPreview, sendMessage, resetMessage, parseMarkdown) },
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, chatItems, msg, quotedItem, editingItem, openDirectChat, deleteMessage)
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetContent = {
GetImageBottomSheet(
chosenImage,
onImageChange = onImageChange,
hideBottomSheet = {
scope.launch { bottomSheetModalState.hide() }
})
},
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
Scaffold(
topBar = { ChatInfoToolbar(chat, back, info) },
bottomBar = {
ComposeView(
msg, quotedItem, editingItem, linkPreview, imagePreview, sendMessage, resetMessage, parseMarkdown,
showBottomSheet = { scope.launch { bottomSheetModalState.show() } }
)
},
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, chatItems, msg, quotedItem, editingItem, openDirectChat, deleteMessage)
}
}
}
}
@@ -341,13 +404,16 @@ fun PreviewChatLayout() {
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
linkPreview = remember { mutableStateOf(null) },
chosenImage = remember { mutableStateOf(null) },
imagePreview = remember { mutableStateOf(null) },
back = {},
info = {},
openDirectChat = {},
sendMessage = {},
resetMessage = {},
deleteMessage = { _, _ -> },
parseMarkdown = { null }
parseMarkdown = { null },
onImageChange = {}
)
}
}
@@ -386,13 +452,16 @@ fun PreviewGroupChatLayout() {
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
linkPreview = remember { mutableStateOf(null) },
chosenImage = remember { mutableStateOf(null) },
imagePreview = remember { mutableStateOf(null) },
back = {},
info = {},
openDirectChat = {},
sendMessage = {},
resetMessage = {},
deleteMessage = { _, _ -> },
parseMarkdown = { null }
parseMarkdown = { null },
onImageChange = {}
)
}
}
@@ -0,0 +1,31 @@
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
import chat.simplex.app.views.chat.item.SentColorLight
import chat.simplex.app.views.helpers.base64ToBitmap
@Composable
fun ComposeImageView(image: String) {
Row(
Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.background(SentColorLight),
verticalAlignment = Alignment.CenterVertically
) {
val imageBitmap = base64ToBitmap(image).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier
.width(80.dp)
.height(60.dp)
.padding(end = 8.dp)
)
}
}
@@ -1,7 +1,20 @@
package chat.simplex.app.views.chat
import ComposeImageView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddCircleOutline
import androidx.compose.material.icons.outlined.AttachFile
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.ComposeLinkView
@@ -13,9 +26,11 @@ fun ComposeView(
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
linkPreview: MutableState<LinkPreview?>,
imagePreview: MutableState<String?>,
sendMessage: (String) -> Unit,
resetMessage: () -> Unit,
parseMarkdown: (String) -> List<FormattedText>?
parseMarkdown: (String) -> List<FormattedText>?,
showBottomSheet: () -> Unit
) {
val cancelledLinks = remember { mutableSetOf<String>() }
@@ -28,8 +43,13 @@ fun ComposeView(
}
Column {
val lp = linkPreview.value
if (lp != null) ComposeLinkView(lp, ::cancelPreview)
val ip = imagePreview.value
if (ip != null) {
ComposeImageView(ip)
} else {
val lp = linkPreview.value
if (lp != null) ComposeLinkView(lp, ::cancelPreview)
}
when {
quotedItem.value != null -> {
ContextItemView(quotedItem)
@@ -39,6 +59,26 @@ fun ComposeView(
}
else -> {}
}
SendMsgView(msg, linkPreview, cancelledLinks, parseMarkdown, sendMessage, editing = editingItem.value != null)
Row(
modifier = Modifier.padding(start = 2.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Icon(
Icons.Outlined.AddCircleOutline,
contentDescription = "Add image",
tint = if (editingItem.value == null) MaterialTheme.colors.primary else Color.Gray,
modifier = Modifier
.size(40.dp)
.padding(vertical = 4.dp)
.clip(CircleShape)
.clickable {
if (editingItem.value == null) {
showBottomSheet()
}
}
)
SendMsgView(msg, linkPreview, cancelledLinks, parseMarkdown, sendMessage, editing = editingItem.value != null)
}
}
}
@@ -106,7 +106,7 @@ fun SendMsgView(
capitalization = KeyboardCapitalization.Sentences,
autoCorrect = true
),
modifier = Modifier.padding(8.dp),
modifier = Modifier.padding(vertical = 8.dp),
cursorBrush = SolidColor(HighOrLowlight),
decorationBox = { innerTextField ->
Surface(
@@ -0,0 +1,52 @@
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider
import chat.simplex.app.BuildConfig
import chat.simplex.app.model.CIFile
import chat.simplex.app.views.helpers.base64ToBitmap
import java.io.*
@Composable
fun ChatItemImageView(image: String, file: CIFile?) {
Column {
val imageBitmap = if (
file?.filePath != null &&
File(file.filePath).exists() &&
file.stored // TODO more advanced approach would be to send progressive jpeg and only check for filepath
) {
val context = LocalContext.current
try {
getBitmapFromUri(context, file.filePath)
} catch (e: Exception) {
base64ToBitmap(image)
}
} else {
base64ToBitmap(image)
}
Image(
imageBitmap.asImageBitmap(),
contentDescription = "image",
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth,
)
}
}
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
@Throws(IOException::class)
private fun getBitmapFromUri(context: Context, file: String): Bitmap {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(file))
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = BitmapFactory.decodeFileDescriptor(fileDescriptor)
parcelFileDescriptor?.close()
return image
}
@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat.item
import ChatItemImageView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -61,7 +62,9 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null, sho
} else {
Column(Modifier.fillMaxWidth()) {
val mc = ci.content.msgContent
if (mc is MsgContent.MCLink) {
if (mc is MsgContent.MCImage) {
ChatItemImageView(image = mc.image, file = ci.file)
} else if (mc is MsgContent.MCLink) {
ChatItemLinkView(mc.preview)
}
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
@@ -37,7 +37,7 @@ import kotlin.math.sqrt
// Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery
private fun cropToSquare(image: Bitmap): Bitmap {
fun cropToSquare(image: Bitmap): Bitmap {
var xOffset = 0
var yOffset = 0
val side = min(image.height, image.width)
@@ -124,7 +124,8 @@ fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLaun
@Composable
fun GetImageBottomSheet(
profileImageStr: MutableState<String?>,
imageBitmap: MutableState<Bitmap?>,
onImageChange: (Bitmap) -> Unit,
hideBottomSheet: () -> Unit
) {
val context = LocalContext.current
@@ -134,12 +135,16 @@ fun GetImageBottomSheet(
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
profileImageStr.value = resizeImageToDataSize(cropToSquare(bitmap), maxDataSize = 12500)
imageBitmap.value = bitmap
onImageChange(bitmap)
}
}
val cameraLauncher = rememberCameraLauncher { bitmap: Bitmap? ->
if (bitmap != null) profileImageStr.value = resizeImageToDataSize(cropToSquare(bitmap), maxDataSize = 12500)
if (bitmap != null) {
imageBitmap.value = bitmap
onImageChange(bitmap)
}
}
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
@@ -1,6 +1,7 @@
package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import android.graphics.Bitmap
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -63,6 +64,7 @@ fun UserProfileLayout(
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = remember { mutableStateOf(profile.displayName) }
val fullName = remember { mutableStateOf(profile.fullName) }
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val profileImage = remember { mutableStateOf(profile.image) }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
@@ -74,9 +76,12 @@ fun UserProfileLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetContent = {
GetImageBottomSheet(profileImage, hideBottomSheet = {
scope.launch { bottomSheetModalState.hide() }
})
GetImageBottomSheet(
chosenImage,
onImageChange = { bitmap -> profileImage.value = resizeImageToDataSize(cropToSquare(bitmap), maxDataSize = 12500) },
hideBottomSheet = {
scope.launch { bottomSheetModalState.hide() }
})
},
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
+11
View File
@@ -773,6 +773,17 @@ struct CIFile: Decodable {
static func getSample(_ fileId: Int64, _ fileName: String, _ fileSize: Int64, filePath: String?, fileStatus: CIFileStatus = .sndStored) -> CIFile {
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
}
var stored: Bool {
get {
switch self.fileStatus {
case .sndStored: return true
case .sndCancelled: return true
case .rcvComplete: return true
default: return false
}
}
}
}
enum CIFileStatus: String, Decodable {
@@ -52,7 +52,7 @@ struct FramedItemView: View {
.padding(.bottom, 2)
} else {
if case let .image(_, image) = chatItem.content.msgContent {
ChatItemImageView(image: image, file: chatItem.file?.filePath)
ChatItemImageView(image: image, file: chatItem.file)
} else if case let .link(_, preview) = chatItem.content.msgContent {
ChatItemLinkView(linkPreview: preview)
}
+3 -2
View File
@@ -260,8 +260,9 @@ struct ChatView: View {
if let imageResized = resizeImageToDataSize(uiImage, maxDataSize: 160000),
let dataResized = Data(base64Encoded: dropImagePrefix(imageResized)),
let jpegData = UIImage(data: dataResized)?.jpegData(compressionQuality: 1) {
let imgName = "IMG_\(Date().timeIntervalSince1970).jpg"
let filename = getDocumentsDirectory().appendingPathComponent(imgName)
let millisecondsSince1970 = Int64((Date().timeIntervalSince1970 * 1000.0).rounded())
let imageName = "image_\(millisecondsSince1970).jpg"
let filename = getDocumentsDirectory().appendingPathComponent(imageName)
do {
try jpegData.write(to: filename)
return filename.path
@@ -124,6 +124,8 @@ struct ComposeView: View {
}
.onChange(of: chosenImage) { image in
if let image = image {
// TODO
// imagePreview = resizeImageToDataSize(image, maxDataSize: 12500)
imagePreview = resizeImageToDataSize(cropToSquare(image), maxDataSize: 12500)
} else {
imagePreview = nil
File diff suppressed because one or more lines are too long