dm-170
This commit is contained in:
parent
6f8f42b51a
commit
5ae72801d1
3 changed files with 230 additions and 52 deletions
|
|
@ -4,7 +4,9 @@ import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.datastore.core.use
|
||||||
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.Message
|
import com.example.login_v3.data.api.api_class.Message
|
||||||
|
|
@ -16,15 +18,24 @@ import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity
|
import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
|
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.io.use
|
||||||
|
import kotlin.use
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ChatRoomsRepository @Inject constructor(
|
class ChatRoomsRepository @Inject constructor(
|
||||||
|
|
@ -215,40 +226,6 @@ class ChatRoomsRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//send message
|
|
||||||
suspend fun sendMessage(
|
|
||||||
roomId: String,
|
|
||||||
content: String,
|
|
||||||
replyToId: String? = null
|
|
||||||
): Result<Message> {
|
|
||||||
return try {
|
|
||||||
// 打包 Request Body
|
|
||||||
val requestBody = SendMessageRequest(
|
|
||||||
content = content,
|
|
||||||
replyToId = replyToId
|
|
||||||
)
|
|
||||||
|
|
||||||
// 呼叫 API
|
|
||||||
val response = api.sendMessage(roomId, requestBody)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
val body = response.body()
|
|
||||||
if (body != null) {
|
|
||||||
Result.success(body)
|
|
||||||
} else {
|
|
||||||
Result.failure(Exception("發送成功但回應身體為空 (Empty response body)"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 抓取後端傳回的錯誤訊息
|
|
||||||
val errorMsg = response.errorBody()?.string() ?: "未知錯誤"
|
|
||||||
Result.failure(Exception("發送訊息失敗,錯誤碼: ${response.code()}, 訊息: $errorMsg"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 捕捉網路斷線或解析 JSON 失敗等狀況
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//upload file
|
//upload file
|
||||||
suspend fun uploadAttachment(roomId: String, fileUri: Uri): Result<Message> {
|
suspend fun uploadAttachment(roomId: String, fileUri: Uri): Result<Message> {
|
||||||
|
|
@ -452,4 +429,69 @@ class ChatRoomsRepository @Inject constructor(
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//download file
|
||||||
|
private val httpClient = OkHttpClient()
|
||||||
|
fun downloadFileFromUrlFlow(fileUrl: String, fileName: String): Flow<Result<Float>> = flow {
|
||||||
|
try {
|
||||||
|
// 1. 建立 OkHttp Request,直接 GET 該網址
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(fileUrl)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = httpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
emit(Result.failure(Exception("下載失敗,HTTP 錯誤碼: ${response.code}")))
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body
|
||||||
|
if (body == null) {
|
||||||
|
emit(Result.failure(Exception("下載失敗,回應身體為空")))
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 定義手機本機儲存路徑
|
||||||
|
val targetDirectory = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
?: context.cacheDir
|
||||||
|
val targetFile = File(targetDirectory, fileName)
|
||||||
|
|
||||||
|
// 3. 讀取網路串流並寫入檔案
|
||||||
|
val totalBytes = body.contentLength()
|
||||||
|
var bytesCopied = 0L
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
|
||||||
|
body.byteStream().use { inputStream ->
|
||||||
|
FileOutputStream(targetFile).use { outputStream ->
|
||||||
|
var bytesRead = inputStream.read(buffer)
|
||||||
|
|
||||||
|
while (bytesRead != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead)
|
||||||
|
bytesCopied += bytesRead
|
||||||
|
|
||||||
|
// 4. 計算並發送進度
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
val progress = bytesCopied.toFloat() / totalBytes.toFloat()
|
||||||
|
emit(Result.success(progress))
|
||||||
|
} else {
|
||||||
|
// 如果後端沒給 Content-Length,傳 -1f 走無限轉圈模式
|
||||||
|
emit(Result.success(-1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead = inputStream.read(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 下載成功,發送 100%
|
||||||
|
emit(Result.success(1.0f))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emit(Result.failure(e))
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO) // 確保硬碟與網路 I/O 不卡 UI 執行緒
|
||||||
}
|
}
|
||||||
|
|
@ -80,8 +80,11 @@ import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||||
import androidx.compose.foundation.layout.statusBars // 如果要使用 statusBars 也需要這個
|
import androidx.compose.foundation.layout.statusBars // 如果要使用 statusBars 也需要這個
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
import androidx.compose.material.icons.filled.Call
|
import androidx.compose.material.icons.filled.Call
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
|
@ -98,6 +101,7 @@ import com.example.login_v3.home.Message.UI.Detail.Message_Component.ImageLightb
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.MessageAttachmentBar
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.MessageAttachmentBar
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserStatusDot
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserStatusDot
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.VideoLightbox
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.VideoLightbox
|
||||||
|
import com.example.login_v3.home.Message.ViewModel.Detail.DownloadStatus
|
||||||
|
|
||||||
|
|
||||||
//bottom bar state
|
//bottom bar state
|
||||||
|
|
@ -952,7 +956,11 @@ fun MessageRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//file message
|
||||||
isFile -> {
|
isFile -> {
|
||||||
|
// 1. 🎯 從 ViewModel 取得當前訊息的下載狀態
|
||||||
|
val downloadStatus = viewModel.getDownloadStatus(message.id)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(max = 260.dp)
|
.widthIn(max = 260.dp)
|
||||||
|
|
@ -965,7 +973,15 @@ fun MessageRow(
|
||||||
)
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { onReplyClick(message) },
|
onLongClick = { onReplyClick(message) },
|
||||||
onClick = { onRowClick(message) } // 🎯 點擊時可以在此觸發下載或開啟檔案
|
onClick = {
|
||||||
|
// 點擊整個氣泡:如果是已完成就直接開啟,否則可維持你原本的行為
|
||||||
|
if (downloadStatus is DownloadStatus.Completed) {
|
||||||
|
onRowClick(message)
|
||||||
|
} else {
|
||||||
|
// 也可以在這裡同樣觸發下載
|
||||||
|
viewModel.downloadFile(message, displayFileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.background(color = finalBubbleColor)
|
.background(color = finalBubbleColor)
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp)
|
.padding(horizontal = 12.dp, vertical = 10.dp)
|
||||||
|
|
@ -975,30 +991,89 @@ fun MessageRow(
|
||||||
RepliedMessagePreview(replied, isMe, partnerDisplayName, currentUserId)
|
RepliedMessagePreview(replied, isMe, partnerDisplayName, currentUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 檔案的 UI 佈局 (左邊圖示,右邊文字)
|
// 檔案的 UI 佈局 (左邊按鈕,右邊文字)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// 根據副檔名決定圖示,這裡示範用內建圖示,你也可以換成自己的 zip 圖片
|
// 檔案下載按鈕 / 進度顯示
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.background(Color.White.copy(alpha = 0.6f), CircleShape),
|
.background(Color.White.copy(alpha = 0.6f), CircleShape)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
// 2. 🎯 點擊按鈕觸發下載或開啟邏輯
|
||||||
|
when (downloadStatus) {
|
||||||
|
is DownloadStatus.NotStarted, is DownloadStatus.Error -> {
|
||||||
|
viewModel.downloadFile(message, displayFileName)
|
||||||
|
}
|
||||||
|
is DownloadStatus.Completed -> {
|
||||||
|
onRowClick(message) // 觸發下載完成後的開啟檔案
|
||||||
|
}
|
||||||
|
is DownloadStatus.Downloading -> {
|
||||||
|
// 下載中可以留空,或者未來實作取消下載
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
// 3. 🎯 根據密封介面 (Sealed Interface) 狀態切換 Icon 與進度條
|
||||||
|
when (downloadStatus) {
|
||||||
|
is DownloadStatus.NotStarted -> {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (displayFileName.endsWith(".zip") || displayFileName.endsWith(".rar")) {
|
imageVector = androidx.compose.material.icons.Icons.Default.ArrowDownward,
|
||||||
androidx.compose.material.icons.Icons.Default.Build // 或者是常規的 Folder / Img
|
contentDescription = "下載檔案",
|
||||||
} else {
|
|
||||||
androidx.compose.material.icons.Icons.Default.Info
|
|
||||||
},
|
|
||||||
contentDescription = "檔案",
|
|
||||||
tint = Color.Gray,
|
tint = Color.Gray,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is DownloadStatus.Downloading -> {
|
||||||
|
// 💡 這裡的 progress 是從 downloadStatus.progress 拿出來的
|
||||||
|
val progress = downloadStatus.progress
|
||||||
|
|
||||||
|
if (progress >= 0f) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(2.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 如果後端沒給 Content-Length (progress = -1f),改跑無限轉圈
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(2.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = androidx.compose.material.icons.Icons.Default.Close,
|
||||||
|
contentDescription = "下載中",
|
||||||
|
tint = Color.Gray.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is DownloadStatus.Completed -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = androidx.compose.material.icons.Icons.Default.Check,
|
||||||
|
contentDescription = "下載完成",
|
||||||
|
tint = Color.Green,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is DownloadStatus.Error -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = androidx.compose.material.icons.Icons.Default.Warning, // 或者是 Refresh 圖示
|
||||||
|
contentDescription = "下載失敗,點擊重試",
|
||||||
|
tint = Color.Red,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右側檔名與資訊
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = displayFileName,
|
text = displayFileName,
|
||||||
|
|
@ -1007,7 +1082,15 @@ fun MessageRow(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "檔案", // 如果後端有提供大小(例如 12.5 MB),放這裡最適合
|
text = when (downloadStatus) {
|
||||||
|
is DownloadStatus.Downloading -> {
|
||||||
|
val progress = downloadStatus.progress
|
||||||
|
if (progress >= 0f) "下載中... ${(progress * 100).toInt()}%" else "下載中..."
|
||||||
|
}
|
||||||
|
is DownloadStatus.Completed -> "已下載"
|
||||||
|
is DownloadStatus.Error -> "下載失敗,點擊重試"
|
||||||
|
else -> "檔案"
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp, color = Color.Gray)
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp, color = Color.Gray)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package com.example.login_v3.home.Message.ViewModel.Detail
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
|
@ -73,6 +74,14 @@ data class AudioPlaybackState(
|
||||||
val duration: Long = 0L // 總時長 (毫秒)
|
val duration: Long = 0L // 總時長 (毫秒)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🎯 檔案下載狀態管理
|
||||||
|
sealed interface DownloadStatus {
|
||||||
|
object NotStarted : DownloadStatus
|
||||||
|
data class Downloading(val progress: Float) : DownloadStatus // 儲存 0.0 ~ 1.0f 的進度
|
||||||
|
object Completed : DownloadStatus
|
||||||
|
data class Error(val message: String) : DownloadStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ChatViewModel @Inject constructor(
|
class ChatViewModel @Inject constructor(
|
||||||
|
|
@ -381,15 +390,59 @@ class ChatViewModel @Inject constructor(
|
||||||
progressLogJob = null
|
progressLogJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// 🌟 生命週期釋放:ViewModel 銷毀時,必須徹底關閉 Player
|
|
||||||
// =====================================================================
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
stopProgressTracker()
|
stopProgressTracker()
|
||||||
exoPlayer?.release()
|
exoPlayer?.release()
|
||||||
exoPlayer = null
|
exoPlayer = null
|
||||||
}
|
}
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
//file download
|
||||||
|
val fileDownloadStates = mutableStateMapOf<String, DownloadStatus>()
|
||||||
|
|
||||||
|
// 取得特定訊息的下載狀態,若無則預設為 NotStarted
|
||||||
|
fun getDownloadStatus(messageId: String): DownloadStatus {
|
||||||
|
return fileDownloadStates[messageId] ?: DownloadStatus.NotStarted
|
||||||
|
}
|
||||||
|
fun downloadFile(message: Message, displayFileName: String) {
|
||||||
|
val messageId = message.id
|
||||||
|
|
||||||
|
// 🎯 取得後端給的直接 URL。請根據你實際的 Message 資料結構調整欄位名稱
|
||||||
|
val fileUrl = message.content // 假設你把 URL 存放在 content 裡,或是 message.fileUrl
|
||||||
|
|
||||||
|
if (fileUrl.isNullOrBlank()) {
|
||||||
|
fileDownloadStates[messageId] = DownloadStatus.Error("檔案下載連結無效")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileDownloadStates[messageId] is DownloadStatus.Downloading) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
fileDownloadStates[messageId] = DownloadStatus.Downloading(0f)
|
||||||
|
|
||||||
|
// 呼叫剛剛改好的 URL 版 Flow
|
||||||
|
repository.downloadFileFromUrlFlow(fileUrl, displayFileName)
|
||||||
|
.collect { progressResult ->
|
||||||
|
progressResult.onSuccess { progress ->
|
||||||
|
if (progress >= 1.0f) {
|
||||||
|
fileDownloadStates[messageId] = DownloadStatus.Completed
|
||||||
|
} else {
|
||||||
|
fileDownloadStates[messageId] = DownloadStatus.Downloading(progress)
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.e("ChatViewModel", "下載失敗: ${error.message}")
|
||||||
|
fileDownloadStates[messageId] = DownloadStatus.Error(error.message ?: "下載失敗")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (選填)如果想要清除特定訊息的狀態(例如錯誤後想重試)
|
||||||
|
fun resetDownloadStatus(messageId: String) {
|
||||||
|
fileDownloadStates.remove(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
fun resetDeleteMessageState() { _deleteMessageState.value = DeleteMessageState.Idle }
|
fun resetDeleteMessageState() { _deleteMessageState.value = DeleteMessageState.Idle }
|
||||||
fun resetSendMessageState() { _sendMessageState.value = SendMessageState.Idle }
|
fun resetSendMessageState() { _sendMessageState.value = SendMessageState.Idle }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue