add delta sync for dm-v18
This commit is contained in:
parent
d84ba852c6
commit
3f0bbd8008
7 changed files with 190 additions and 5 deletions
|
|
@ -167,4 +167,12 @@ interface TecnologiaApi {
|
||||||
@Path("room_id") roomId: String,
|
@Path("room_id") roomId: String,
|
||||||
@Path("msg_id") msgId: String
|
@Path("msg_id") msgId: String
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
//edit message
|
||||||
|
@PATCH("api/dm/{room_id}/messages/{msg_id}")
|
||||||
|
suspend fun editMessage(
|
||||||
|
@Path("room_id") roomId: String,
|
||||||
|
@Path("msg_id") msgId: String,
|
||||||
|
@Body request: EditMessageRequest
|
||||||
|
): Response<EditMessageResponse>
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +218,17 @@ data class MessageReactionUsersResponse(
|
||||||
@Json(name = "users") val users: List<String>
|
@Json(name = "users") val users: List<String>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//edit message
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EditMessageRequest(
|
||||||
|
@Json(name = "content") val content: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EditMessageResponse(
|
||||||
|
@Json(name = "ok") val ok: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.example.login_v3.data.api.TecnologiaApi
|
import com.example.login_v3.data.api.TecnologiaApi
|
||||||
import com.example.login_v3.data.api.api_class.ChatRoom
|
import com.example.login_v3.data.api.api_class.ChatRoom
|
||||||
|
import com.example.login_v3.data.api.api_class.EditMessageRequest
|
||||||
import com.example.login_v3.data.api.api_class.Message
|
import com.example.login_v3.data.api.api_class.Message
|
||||||
import com.example.login_v3.data.api.api_class.MessageResponse
|
import com.example.login_v3.data.api.api_class.MessageResponse
|
||||||
import com.example.login_v3.data.api.api_class.Reaction
|
import com.example.login_v3.data.api.api_class.Reaction
|
||||||
|
|
@ -569,6 +570,45 @@ class ChatRoomsRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 編輯訊息(樂觀更新版本)
|
||||||
|
suspend fun editMessage(roomId: String, messageId: String, content: String): Result<Unit> {
|
||||||
|
// 1. 先從本地撈出原本的訊息作為備份
|
||||||
|
val backupEntity = try {
|
||||||
|
messageDao.getMessageById(messageId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 樂觀更新:立刻在本地修改內容
|
||||||
|
if (backupEntity != null) {
|
||||||
|
try {
|
||||||
|
val updatedEntity = backupEntity.copy(
|
||||||
|
content = content,
|
||||||
|
isEdited = true
|
||||||
|
)
|
||||||
|
messageDao.insertOrUpdate(updatedEntity)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 呼叫 API
|
||||||
|
return try {
|
||||||
|
val response = api.editMessage(roomId, messageId, EditMessageRequest(content))
|
||||||
|
if (response.isSuccessful && response.body()?.ok == true) {
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
// 失敗則回滾
|
||||||
|
backupEntity?.let { messageDao.insertOrUpdate(it) }
|
||||||
|
Result.failure(Exception("編輯失敗"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 異常則回滾
|
||||||
|
backupEntity?.let { messageDao.insertOrUpdate(it) }
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//download file
|
//download file
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Reply
|
import androidx.compose.material.icons.filled.Reply
|
||||||
|
import androidx.compose.material.icons.filled.Edit // 🎯 新增
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -41,9 +42,11 @@ fun MessageActionMenuRow(
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onReplyClick: (MessageUiModel) -> Unit, // 👈 修正:這裡只需要定義型態
|
onReplyClick: (MessageUiModel) -> Unit, // 👈 修正:這裡只需要定義型態
|
||||||
onDeleteClick: (MessageUiModel) -> Unit,
|
onDeleteClick: (MessageUiModel) -> Unit,
|
||||||
|
onEditClick: (MessageUiModel) -> Unit, // 🎯 新增
|
||||||
isOwnMessage: Boolean,
|
isOwnMessage: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
// ... (rest of the function)
|
||||||
// 💡 取得系統剪貼簿,用來實作「複製文字」功能
|
// 💡 取得系統剪貼簿,用來實作「複製文字」功能
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
|
@ -116,7 +119,16 @@ fun MessageActionMenuRow(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. 刪除:只有在自己的訊息時才顯示 👈
|
// 3. 編輯:只有在自己的文字訊息時顯示 🎯
|
||||||
|
if (isOwnMessage && !isImage) {
|
||||||
|
ChatMenuButton(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
label = "編輯",
|
||||||
|
onClick = { onEditClick(message) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 刪除:只有在自己的訊息時才顯示 👈
|
||||||
if (isOwnMessage) {
|
if (isOwnMessage) {
|
||||||
ChatMenuButton(
|
ChatMenuButton(
|
||||||
imageVector = Icons.Default.Delete,
|
imageVector = Icons.Default.Delete,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AddCircle
|
import androidx.compose.material.icons.filled.AddCircle
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Photo
|
import androidx.compose.material.icons.filled.Photo
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
|
|
@ -28,6 +29,7 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect // 🎯 新增
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -44,12 +46,25 @@ import com.example.login_v3.home.Message.UI.Detail.MessageUiModel
|
||||||
fun MessageInputBar(
|
fun MessageInputBar(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
replyingMessage: MessageUiModel?,
|
replyingMessage: MessageUiModel?,
|
||||||
|
editingMessage: MessageUiModel? = null, // 🎯 新增
|
||||||
onCancelReply: () -> Unit,
|
onCancelReply: () -> Unit,
|
||||||
|
onCancelEdit: () -> Unit = {}, // 🎯 新增
|
||||||
onSendClick: (String) -> Unit,
|
onSendClick: (String) -> Unit,
|
||||||
|
onEditSaveClick: (String) -> Unit = {}, // 🎯 新增
|
||||||
onAttachmentClick: () -> Unit
|
onAttachmentClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
var textState by remember { mutableStateOf("") }
|
var textState by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// 🎯 當進入編輯模式時,自動填入原本的內容
|
||||||
|
LaunchedEffect(editingMessage) {
|
||||||
|
if (editingMessage != null) {
|
||||||
|
textState = editingMessage.content
|
||||||
|
} else {
|
||||||
|
// 如果從編輯模式退出且不是因為發送成功,可能需要清空?
|
||||||
|
// 但通常 clearEditingMessage 會觸發這裡
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 💡 外層包裹一層 Box,並加上 padding 與 navigationBarsPadding,確保懸浮且不被系統列擋住
|
// 💡 外層包裹一層 Box,並加上 padding 與 navigationBarsPadding,確保懸浮且不被系統列擋住
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -103,6 +118,39 @@ fun MessageInputBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 1.5 編輯預覽列
|
||||||
|
AnimatedVisibility(visible = editingMessage != null) {
|
||||||
|
if (editingMessage != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFFFFF3E0)) // 編輯模式用淡橘色區分
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "編輯訊息",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = Color(0xFFE65100),
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
onCancelEdit()
|
||||||
|
textState = "" // 取消編輯時清空
|
||||||
|
}, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "取消編輯",
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 實際的輸入工具列
|
// 2. 實際的輸入工具列
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -129,7 +177,10 @@ fun MessageInputBar(
|
||||||
textStyle = MaterialTheme.typography.bodyLarge,
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
if (textState.isEmpty()) {
|
if (textState.isEmpty()) {
|
||||||
Text("請輸入訊息...", color = Color.LightGray)
|
Text(
|
||||||
|
text = if (editingMessage != null) "編輯訊息..." else "請輸入訊息...",
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
}
|
}
|
||||||
innerTextField()
|
innerTextField()
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +190,11 @@ fun MessageInputBar(
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (textState.isNotBlank() && !isLoading) {
|
if (textState.isNotBlank() && !isLoading) {
|
||||||
onSendClick(textState)
|
if (editingMessage != null) {
|
||||||
|
onEditSaveClick(textState)
|
||||||
|
} else {
|
||||||
|
onSendClick(textState)
|
||||||
|
}
|
||||||
textState = ""
|
textState = ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -150,9 +205,10 @@ fun MessageInputBar(
|
||||||
} else {
|
} else {
|
||||||
// 加上小背景讓送出按鈕更有重點
|
// 加上小背景讓送出按鈕更有重點
|
||||||
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
|
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
|
||||||
|
val icon = if (editingMessage != null) Icons.Default.Check else Icons.Default.Send
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Send,
|
imageVector = icon,
|
||||||
contentDescription = "送出",
|
contentDescription = if (editingMessage != null) "儲存編輯" else "送出",
|
||||||
tint = iconColor
|
tint = iconColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ sealed interface BottomBarState {
|
||||||
data class ActionMenu(val message: MessageUiModel) : BottomBarState // 長按:動作選單
|
data class ActionMenu(val message: MessageUiModel) : BottomBarState // 長按:動作選單
|
||||||
data class EmojiMenu(val message: MessageUiModel) : BottomBarState // 單擊:Emoji 工具列
|
data class EmojiMenu(val message: MessageUiModel) : BottomBarState // 單擊:Emoji 工具列
|
||||||
object AttachmentMenu : BottomBarState // 🎯 新增:附件選單
|
object AttachmentMenu : BottomBarState // 🎯 新增:附件選單
|
||||||
|
data class Edit(val message: MessageUiModel) : BottomBarState // 🎯 新增:編輯模式
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 新增:Top Bar 狀態機
|
// 🎯 新增:Top Bar 狀態機
|
||||||
|
|
@ -155,6 +156,8 @@ fun MessageMessaging(
|
||||||
// 🎯 新增:Top Bar 的本地狀態
|
// 🎯 新增:Top Bar 的本地狀態
|
||||||
var topBarState by remember { mutableStateOf<TopBarState>(TopBarState.Regular) }
|
var topBarState by remember { mutableStateOf<TopBarState>(TopBarState.Regular) }
|
||||||
|
|
||||||
|
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// 🌟 核心:使用 derivedStateOf 統一控管狀態權限(長按優先權通常大於單擊)
|
// 🌟 核心:使用 derivedStateOf 統一控管狀態權限(長按優先權通常大於單擊)
|
||||||
// 🎯 新增:用來控管非訊息觸發的底部狀態(例如:AttachmentMenu)
|
// 🎯 新增:用來控管非訊息觸發的底部狀態(例如:AttachmentMenu)
|
||||||
var localBottomBarState by remember { mutableStateOf<BottomBarState?>(null) }
|
var localBottomBarState by remember { mutableStateOf<BottomBarState?>(null) }
|
||||||
|
|
@ -163,6 +166,7 @@ fun MessageMessaging(
|
||||||
when {
|
when {
|
||||||
actionMessage != null -> BottomBarState.ActionMenu(actionMessage!!)
|
actionMessage != null -> BottomBarState.ActionMenu(actionMessage!!)
|
||||||
emojiTargetMessage != null -> BottomBarState.EmojiMenu(emojiTargetMessage!!)
|
emojiTargetMessage != null -> BottomBarState.EmojiMenu(emojiTargetMessage!!)
|
||||||
|
editingMessage != null -> BottomBarState.Edit(editingMessage!!) // 🎯 編輯模式優先
|
||||||
localBottomBarState != null -> localBottomBarState!! // 🎯 如果有本地狀態,則採用它
|
localBottomBarState != null -> localBottomBarState!! // 🎯 如果有本地狀態,則採用它
|
||||||
else -> BottomBarState.Input
|
else -> BottomBarState.Input
|
||||||
}
|
}
|
||||||
|
|
@ -519,6 +523,21 @@ fun MessageMessaging(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is BottomBarState.Edit -> {
|
||||||
|
MessageInputBar(
|
||||||
|
isLoading = sendStatus is SendMessageState.Loading,
|
||||||
|
replyingMessage = null, // 編輯時不顯示回覆
|
||||||
|
editingMessage = currentState.message,
|
||||||
|
onCancelReply = {},
|
||||||
|
onCancelEdit = { viewModel.clearEditingMessage() },
|
||||||
|
onSendClick = {},
|
||||||
|
onEditSaveClick = { content ->
|
||||||
|
viewModel.editMessage(roomId, currentState.message.id, content)
|
||||||
|
},
|
||||||
|
onAttachmentClick = {} // 編輯模式不給上傳附件
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is BottomBarState.ActionMenu -> {
|
is BottomBarState.ActionMenu -> {
|
||||||
val currentActionMessage = currentState.message
|
val currentActionMessage = currentState.message
|
||||||
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
|
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
|
||||||
|
|
@ -532,6 +551,10 @@ fun MessageMessaging(
|
||||||
viewModel.setReplyingMessage(msg)
|
viewModel.setReplyingMessage(msg)
|
||||||
viewModel.clearActionMessage()
|
viewModel.clearActionMessage()
|
||||||
},
|
},
|
||||||
|
onEditClick = { msg ->
|
||||||
|
viewModel.setEditingMessage(msg)
|
||||||
|
viewModel.clearActionMessage()
|
||||||
|
},
|
||||||
onDeleteClick = { msg ->
|
onDeleteClick = { msg ->
|
||||||
// 🌟 核心防呆:如果訊息還在發送中(SENDING),禁止點擊刪除,避免 tempId 送給後端造成 404
|
// 🌟 核心防呆:如果訊息還在發送中(SENDING),禁止點擊刪除,避免 tempId 送給後端造成 404
|
||||||
if (msg.status == MessageStatus.SENDING) {
|
if (msg.status == MessageStatus.SENDING) {
|
||||||
|
|
@ -1331,6 +1354,17 @@ private fun TextMessageContent(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)
|
style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (message.isEdited) {
|
||||||
|
Text(
|
||||||
|
text = "(已編輯)",
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color.Gray.copy(alpha = 0.7f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.align(Alignment.End).padding(top = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,11 @@ class ChatViewModel @Inject constructor(
|
||||||
fun setActionMessage(message: MessageUiModel?) { _actionMessage.value = message }
|
fun setActionMessage(message: MessageUiModel?) { _actionMessage.value = message }
|
||||||
fun clearActionMessage() { _actionMessage.value = null }
|
fun clearActionMessage() { _actionMessage.value = null }
|
||||||
|
|
||||||
|
private val _editingMessage = MutableStateFlow<MessageUiModel?>(null)
|
||||||
|
val editingMessage: StateFlow<MessageUiModel?> = _editingMessage.asStateFlow()
|
||||||
|
fun setEditingMessage(message: MessageUiModel?) { _editingMessage.value = message }
|
||||||
|
fun clearEditingMessage() { _editingMessage.value = null }
|
||||||
|
|
||||||
private val _replyingMessage = MutableStateFlow<MessageUiModel?>(null)
|
private val _replyingMessage = MutableStateFlow<MessageUiModel?>(null)
|
||||||
val replyingMessage: StateFlow<MessageUiModel?> = _replyingMessage.asStateFlow()
|
val replyingMessage: StateFlow<MessageUiModel?> = _replyingMessage.asStateFlow()
|
||||||
fun setReplyingMessage(message: MessageUiModel?) { _replyingMessage.value = message }
|
fun setReplyingMessage(message: MessageUiModel?) { _replyingMessage.value = message }
|
||||||
|
|
@ -323,6 +328,25 @@ class ChatViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun editMessage(roomId: String, messageId: String, content: String) {
|
||||||
|
if (content.trim().isBlank()) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_sendMessageState.value = SendMessageState.Loading
|
||||||
|
|
||||||
|
val result = repository.editMessage(roomId, messageId, content)
|
||||||
|
|
||||||
|
_editingMessage.value = null
|
||||||
|
|
||||||
|
result.onSuccess {
|
||||||
|
_sendMessageState.value = SendMessageState.Success
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.e("ChatViewModel", "Edit message failed", error)
|
||||||
|
_sendMessageState.value = SendMessageState.Error(error.message ?: "編輯訊息失敗")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 其餘 uploadAttachment、Emoji 功能保持原樣 ---
|
// --- 其餘 uploadAttachment、Emoji 功能保持原樣 ---
|
||||||
fun uploadAttachment(roomId: String, fileUri: Uri) {
|
fun uploadAttachment(roomId: String, fileUri: Uri) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue