add chat list (miss image, attach product in chat and date chat)

This commit is contained in:
shaulascr
2025-05-09 02:59:07 +07:00
parent b031e9cc6d
commit 3157122293
10 changed files with 543 additions and 183 deletions

View File

@ -0,0 +1,42 @@
package com.alya.ecommerce_serang.data.api.response.chat
import com.google.gson.annotations.SerializedName
data class ChatListResponse(
@field:SerializedName("chat")
val chat: List<ChatItemList>,
@field:SerializedName("message")
val message: String
)
data class ChatItemList(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("user_image")
val userImage: String? = null,
@field:SerializedName("user_name")
val userName: String,
@field:SerializedName("chat_room_id")
val chatRoomId: Int,
@field:SerializedName("latest_message_time")
val latestMessageTime: String,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("message")
val message: String,
@field:SerializedName("store_image")
val storeImage: String? = null
)

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
@ -12,24 +13,17 @@ import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import okhttp3.MultipartBody
import okhttp3.RequestBody
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
@ -45,21 +39,22 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.StoreRespons
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.HeaderMap
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
@ -250,11 +245,10 @@ interface ApiService {
suspend fun sendChatLine(
@Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody,
@Part("product_id") productId: RequestBody?,
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@PUT("chatstatus")
suspend fun updateChatStatus(
@Body request: UpdateChatRequest
@ -264,4 +258,8 @@ interface ApiService {
suspend fun getChatDetail(
@Path("chatRoomId") chatRoomId: Int
): Response<ChatHistoryResponse>
@GET("chat")
suspend fun getChatList(
): Response<ChatListResponse>
}

View File

@ -4,12 +4,14 @@ import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import javax.inject.Inject
@ -36,55 +38,56 @@ class ChatRepository @Inject constructor(
suspend fun sendChatMessage(
storeId: Int,
message: String,
productId: Int,
imageFile: File? = null
productId: Int? = null,
imageFile: File? = null,
chatRoomId: Int? = null // Not used in the actual API call but kept for compatibility
): Result<SendChatResponse> {
return try {
// Create request bodies for text fields
val storeIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), storeId.toString())
val messageBody = RequestBody.create("text/plain".toMediaTypeOrNull(), message)
val productIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), productId.toString())
// Create multipart request parts
val storeIdPart = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val messagePart = message.toRequestBody("text/plain".toMediaTypeOrNull())
// Create multipart body for the image file
val imageMultipart = if (imageFile != null && imageFile.exists()) {
// Log detailed file information
Log.d(TAG, "Image file: ${imageFile.absolutePath}")
Log.d(TAG, "Image file size: ${imageFile.length()} bytes")
Log.d(TAG, "Image file exists: ${imageFile.exists()}")
Log.d(TAG, "Image file can read: ${imageFile.canRead()}")
val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), imageFile)
MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile)
// Add product ID part if provided
val productIdPart = if (productId != null && productId > 0) {
productId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
} else {
// Pass null when no image is provided
null
}
// Log request info
Log.d(TAG, "Sending message to store ID: $storeId, product ID: $productId")
Log.d(TAG, "Message content: $message")
Log.d(TAG, "Has image: ${imageFile != null && imageFile.exists()}")
// Create image part if file is provided
val imagePart = if (imageFile != null && imageFile.exists()) {
val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile)
} else {
null
}
// Make the API call
// Debug log the request parameters
Log.d("ChatRepository", "Sending chat with: storeId=$storeId, productId=$productId, " +
"message length=${message.length}, hasImage=${imageFile != null}")
// Make API call using your actual endpoint and parameter names
val response = apiService.sendChatLine(
storeId = storeIdBody,
message = messageBody,
productId = productIdBody,
chatimg = imageMultipart
storeId = storeIdPart,
message = messagePart,
productId = productIdPart,
chatimg = imagePart
)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Send chat response is empty"))
val body = response.body()
if (body != null) {
Result.Success(body)
} else {
Result.Error(Exception("Empty response body"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "HTTP Error: ${response.code()}, Body: $errorBody")
val errorBody = response.errorBody()?.string() ?: "{}"
Log.e("ChatRepository", "API Error: ${response.code()} - $errorBody")
Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
}
} catch (e: Exception) {
Log.e(TAG, "Exception sending message", e)
e.printStackTrace()
Log.e("ChatRepository", "Exception sending message", e)
Result.Error(e)
}
}
@ -128,4 +131,19 @@ class ChatRepository @Inject constructor(
Result.Error(e)
}
}
suspend fun getListChat(): Result<List<ChatItemList>> {
return try {
val response = apiService.getChatList()
if (response.isSuccessful){
val chat = response.body()?.chat ?: emptyList()
Result.Success(chat)
} else {
Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}"))
}
} catch (e: Exception){
Result.Error(e)
}
}
}

View File

@ -100,7 +100,6 @@ class ChatActivity : AppCompatActivity() {
apiService = ApiConfig.getApiService(sessionManager)
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
// Log.d("ChatActivity", "User ID in storage: '${sessionManager.getUserId()}'")
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
@ -125,7 +124,7 @@ class ChatActivity : AppCompatActivity() {
val productImage = intent.getStringExtra(Constants.EXTRA_PRODUCT_IMAGE) ?: ""
val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
// Check if user is logged in
val token = sessionManager.getToken()
@ -148,13 +147,18 @@ class ChatActivity : AppCompatActivity() {
productRating = productRating,
storeName = storeName
)
// Setup UI components
setupRecyclerView()
setupListeners()
setupTypingIndicator()
observeViewModel()
// If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) {
// Directly set the chatRoomId and load chat history
viewModel._chatRoomId.value = chatRoomId
}
}
private fun setupRecyclerView() {
@ -234,22 +238,31 @@ class ChatActivity : AppCompatActivity() {
}
// Update product info
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
if (!state.productName.isNullOrEmpty()) {
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
// Load product image
if (state.productImageUrl.isNotEmpty()) {
Glide.with(this@ChatActivity)
.load(BASE_URL + state.productImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
// Load product image
if (!state.productImageUrl.isNullOrEmpty()) {
Glide.with(this@ChatActivity)
.load(BASE_URL + state.productImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
// Make sure the product section is visible
binding.productContainer.visibility = View.VISIBLE
} else {
// Hide the product section if info is missing
binding.productContainer.visibility = View.GONE
}
// Update attachment hint
if (state.hasAttachment) {
binding.editTextMessage.hint = getString(R.string.image_attached)
@ -352,17 +365,70 @@ class ChatActivity : AppCompatActivity() {
}
private fun handleSelectedImage(uri: Uri) {
// Get the file from Uri
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
val cursor = contentResolver.query(uri, filePathColumn, null, null, null)
cursor?.moveToFirst()
val columnIndex = cursor?.getColumnIndex(filePathColumn[0])
val filePath = cursor?.getString(columnIndex ?: 0)
cursor?.close()
try {
Log.d(TAG, "Processing selected image: $uri")
if (filePath != null) {
viewModel.setSelectedImageFile(File(filePath))
Toast.makeText(this, R.string.image_selected, Toast.LENGTH_SHORT).show()
// First try the direct approach to get the file path
var filePath: String? = null
// For newer Android versions, we need to handle content URIs properly
if (uri.scheme == "content") {
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndex(MediaStore.Images.Media.DATA)
if (columnIndex != -1) {
filePath = it.getString(columnIndex)
Log.d(TAG, "Found file path from cursor: $filePath")
}
}
}
// If we couldn't get the path directly, create a copy in our cache directory
if (filePath == null) {
contentResolver.openInputStream(uri)?.use { inputStream ->
val fileName = "img_${System.currentTimeMillis()}.jpg"
val outputFile = File(cacheDir, fileName)
outputFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
filePath = outputFile.absolutePath
Log.d(TAG, "Created temp file from input stream: $filePath")
}
}
} else if (uri.scheme == "file") {
// Direct file URI
filePath = uri.path
Log.d(TAG, "Got file path directly from URI: $filePath")
}
// Process the file path
if (filePath != null) {
val file = File(filePath)
if (file.exists()) {
// Check file size (limit to 5MB)
if (file.length() > 5 * 1024 * 1024) {
Toast.makeText(this, "Image too large (max 5MB), please select a smaller image", Toast.LENGTH_SHORT).show()
return
}
// Set the file to the ViewModel
viewModel.setSelectedImageFile(file)
Toast.makeText(this, R.string.image_selected, Toast.LENGTH_SHORT).show()
Log.d(TAG, "Successfully set image file: ${file.absolutePath}, size: ${file.length()} bytes")
} else {
Log.e(TAG, "File does not exist: $filePath")
Toast.makeText(this, "Could not access the selected image", Toast.LENGTH_SHORT).show()
}
} else {
Log.e(TAG, "Could not get file path from URI: $uri")
Toast.makeText(this, "Could not process the selected image", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e(TAG, "Error handling selected image", e)
Toast.makeText(this, "Error processing image: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
@ -395,21 +461,32 @@ class ChatActivity : AppCompatActivity() {
fun createIntent(
context: Activity,
storeId: Int,
productId: Int,
productName: String?,
productPrice: String,
productImage: String?,
productRating: String?,
storeName: String?,
productId: Int = 0,
productName: String? = null,
productPrice: String = "",
productImage: String? = null,
productRating: String? = null,
storeName: String? = null,
chatRoomId: Int = 0
){
val intent = Intent(context, ChatActivity::class.java).apply {
) {
val intent = Intent(context, ChatActivity::class.java).apply {
putExtra(Constants.EXTRA_STORE_ID, storeId)
putExtra(Constants.EXTRA_PRODUCT_ID, productId)
putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
putExtra(Constants.EXTRA_PRODUCT_RATING, productRating)
// Convert productRating string to float if provided
if (productRating != null) {
try {
putExtra(Constants.EXTRA_PRODUCT_RATING, productRating.toFloat())
} catch (e: NumberFormatException) {
putExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
}
} else {
putExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
}
putExtra(Constants.EXTRA_STORE_NAME, storeName)
if (chatRoomId > 0) {

View File

@ -0,0 +1,69 @@
package com.alya.ecommerce_serang.ui.chat
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
import com.alya.ecommerce_serang.databinding.ItemChatBinding
import com.bumptech.glide.Glide
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
class ChatListAdapter(
private val chatList: List<ChatItemList>,
private val onClick: (ChatItemList) -> Unit
) : RecyclerView.Adapter<ChatListAdapter.ChatViewHolder>() {
inner class ChatViewHolder(private val binding: ItemChatBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(chat: ChatItemList) {
binding.txtStoreName.text = chat.storeName
binding.txtMessage.text = chat.message
binding.txtTime.text = formatTime(chat.latestMessageTime)
// Process image URL properly
val imageUrl = chat.storeImage?.let {
if (it.startsWith("/")) BASE_URL + it else it
}
Glide.with(binding.imgStore.context)
.load(imageUrl)
.placeholder(R.drawable.ic_person)
.error(R.drawable.placeholder_image)
.into(binding.imgStore)
// Handle click event
binding.root.setOnClickListener {
onClick(chat)
}
}
private fun formatTime(isoTime: String): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val date = inputFormat.parse(isoTime)
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
outputFormat.format(date ?: Date())
} catch (e: Exception) {
""
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
val binding = ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ChatViewHolder(binding)
}
override fun getItemCount(): Int = chatList.size
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
holder.bind(chatList[position])
}
}

View File

@ -1,14 +1,15 @@
package com.alya.ecommerce_serang.ui.chat
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.FragmentChatListBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -30,6 +31,7 @@ class ChatListFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
socketService = SocketIOService(sessionManager)
}
@ -44,13 +46,43 @@ class ChatListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
viewModel.getChatList()
observeChatList()
}
private fun setupView(){
binding.btnTrial.setOnClickListener{
val intent = Intent(requireContext(), ChatActivity::class.java)
startActivity(intent)
private fun observeChatList() {
viewModel.chatList.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
val adapter = ChatListAdapter(result.data) { chatItem ->
// Use the ChatActivity.createIntent factory method for proper navigation
ChatActivity.createIntent(
context = requireActivity(),
storeId = chatItem.storeId,
productId = 0, // Default value since we don't have it in ChatListItem
productName = null, // Null is acceptable as per ChatActivity
productPrice = "",
productImage = null,
productRating = null,
storeName = chatItem.storeName,
chatRoomId = chatItem.chatRoomId
)
}
binding.chatListRecyclerView.adapter = adapter
}
is Result.Error -> {
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
}
Result.Loading -> {
// Optional: show progress bar
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.chat.ChatItem
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
import com.alya.ecommerce_serang.data.api.response.chat.ChatLine
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -31,12 +32,15 @@ class ChatViewModel @Inject constructor(
private val _state = MutableLiveData(ChatUiState())
val state: LiveData<ChatUiState> = _state
private val _chatRoomId = MutableLiveData<Int>(0)
val _chatRoomId = MutableLiveData<Int>(0)
val chatRoomId: LiveData<Int> = _chatRoomId
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
// Store and product parameters
private var storeId: Int = 0
private var productId: Int = 0
private var productId: Int? = 0
private var currentUserId: Int? = null
private var defaultUserId: Int = 0
@ -83,27 +87,27 @@ class ChatViewModel @Inject constructor(
*/
fun setChatParameters(
storeId: Int,
productId: Int,
productName: String,
productPrice: String,
productImage: String,
productRating: Float,
productId: Int? = 0,
productName: String? = null,
productPrice: String? = null,
productImage: String? = null,
productRating: Float? = 0f,
storeName: String
) {
this.storeId = storeId
this.productId = productId
this.productName = productName
this.productPrice = productPrice
this.productImage = productImage
this.productRating = productRating
this.productId = productId!!
this.productName = productName.toString()
this.productPrice = productPrice.toString()
this.productImage = productImage.toString()
this.productRating = productRating!!
this.storeName = storeName
// Update state with product info
updateState {
it.copy(
productName = productName,
productPrice = productPrice,
productImageUrl = productImage,
productName = productName.toString(),
productPrice = productPrice.toString(),
productImageUrl = productImage.toString(),
productRating = productRating,
storeName = storeName
)
@ -237,78 +241,113 @@ class ChatViewModel @Inject constructor(
* Sends a chat message
*/
fun sendMessage(message: String) {
if (message.isBlank()) return
if (storeId == 0 || productId == 0) {
Log.e(TAG, "Cannot send message: Store ID or Product ID is 0")
updateState { it.copy(error = "Cannot send message. Invalid parameters.") }
if (message.isBlank() && selectedImageFile == null) {
Log.e(TAG, "Cannot send message: Both message and image are empty")
return
}
// Check if we have the necessary parameters
if (storeId <= 0) {
Log.e(TAG, "Cannot send message: Store ID is invalid")
updateState { it.copy(error = "Cannot send message. Invalid store ID.") }
return
}
// Get the existing chatRoomId (not used in API but may be needed for Socket.IO)
val existingChatRoomId = _chatRoomId.value ?: 0
// Log debug information
Log.d(TAG, "Sending message with params: storeId=$storeId, productId=$productId")
Log.d(TAG, "Current user ID: $currentUserId")
Log.d(TAG, "Has attachment: ${selectedImageFile != null}")
// Check image file size if present
selectedImageFile?.let { file ->
if (file.exists() && file.length() > 5 * 1024 * 1024) { // 5MB limit
updateState { it.copy(error = "Image file is too large. Please select a smaller image.") }
return
}
}
viewModelScope.launch {
updateState { it.copy(isSending = true) }
when (val result = chatRepository.sendChatMessage(
storeId = storeId,
message = message,
productId = productId,
imageFile = selectedImageFile
)) {
is Result.Success -> {
// Add new message to the list
val chatLine = result.data.chatLine
val newMessage = convertChatLineToUiMessage(chatLine)
try {
// Send the message using the repository
// Note: We keep the chatRoomId parameter for compatibility with the repository method signature,
// but it's not actually used in the API call
val result = chatRepository.sendChatMessage(
storeId = storeId,
message = message,
productId = productId,
imageFile = selectedImageFile,
chatRoomId = existingChatRoomId
)
val currentMessages = _state.value?.messages ?: listOf()
val updatedMessages = currentMessages.toMutableList().apply {
add(newMessage)
when (result) {
is Result.Success -> {
// Add new message to the list
val chatLine = result.data.chatLine
val newMessage = convertChatLineToUiMessage(chatLine)
val currentMessages = _state.value?.messages ?: listOf()
val updatedMessages = currentMessages.toMutableList().apply {
add(newMessage)
}
updateState {
it.copy(
messages = updatedMessages,
isSending = false,
hasAttachment = false,
error = null
)
}
Log.d(TAG, "Message sent successfully: ${chatLine.id}")
// Update the chat room ID if it's the first message
val newChatRoomId = chatLine.chatRoomId
if (existingChatRoomId == 0 && newChatRoomId > 0) {
Log.d(TAG, "Chat room created: $newChatRoomId")
_chatRoomId.value = newChatRoomId
// Now that we have a chat room ID, we can join the Socket.IO room
joinSocketRoom(newChatRoomId)
}
// Emit the message via Socket.IO for real-time updates
socketService.sendMessage(chatLine)
// Clear the image attachment
selectedImageFile = null
}
is Result.Error -> {
val errorMsg = if (result.exception.message.isNullOrEmpty() || result.exception.message == "{}") {
"Failed to send message. Please try again."
} else {
result.exception.message
}
updateState {
it.copy(
messages = updatedMessages,
isSending = false,
hasAttachment = false,
error = null
)
updateState {
it.copy(
isSending = false,
error = errorMsg
)
}
Log.e(TAG, "Error sending message: ${result.exception.message}")
}
Log.d(TAG, "Message sent successfully: ${chatLine.id}")
// Update the chat room ID if it's the first message
// This is the key part - we get the chat room ID from the response
val newChatRoomId = chatLine.chatRoomId
if ((_chatRoomId.value ?: 0) == 0 && newChatRoomId > 0) {
Log.d(TAG, "Chat room created: $newChatRoomId")
_chatRoomId.value = newChatRoomId
// Now that we have a chat room ID, we can join the Socket.IO room
joinSocketRoom(newChatRoomId)
is Result.Loading -> {
updateState { it.copy(isSending = true) }
}
// Emit the message via Socket.IO for real-time updates
socketService.sendMessage(chatLine)
// Clear the image attachment
selectedImageFile = null
}
is Result.Error -> {
val errorMsg = if (result.exception.message.isNullOrEmpty() || result.exception.message == "{}") {
"Failed to send message. Please try again."
} else {
result.exception.message
}
updateState {
it.copy(
isSending = false,
error = errorMsg
)
}
Log.e(TAG, "Error sending message: ${result.exception.message}")
}
is Result.Loading -> {
updateState { it.copy(isSending = true) }
} catch (e: Exception) {
Log.e(TAG, "Exception in sendMessage", e)
updateState {
it.copy(
isSending = false,
error = "An unexpected error occurred: ${e.message}"
)
}
}
}
@ -428,6 +467,13 @@ class ChatViewModel @Inject constructor(
socketService.disconnect()
Log.d(TAG, "ViewModel cleared, Socket.IO disconnected")
}
fun getChatList() {
viewModelScope.launch {
_chatList.value = com.alya.ecommerce_serang.data.repository.Result.Loading
_chatList.value = chatRepository.getListChat()
}
}
}
/**

View File

@ -92,14 +92,15 @@
app:layout_constraintTop_toBottomOf="@+id/chatToolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/product_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/imgProduct"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image"
app:layout_constraintDimensionRatio="1:1"

View File

@ -1,19 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:context=".ui.chat.ChatListFragment">
<TextView
android:id="@+id/chatHeaderTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pesan"
android:textSize="24sp"
android:padding="16dp"
android:layout_marginHorizontal="8dp"
android:fontFamily="@font/dmsans_bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
app:dividerColor="@color/black_100"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chatListRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello" />
android:padding="8dp"
android:clipToPadding="false"
tools:listitem="@layout/item_chat"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<Button
android:id="@+id/btn_trial"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="trial button"/>
</FrameLayout>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:elevation="2dp"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:id="@+id/imgStore"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/circle_background"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_person" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/txtStoreName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store Name"
android:textStyle="bold" />
<TextView
android:id="@+id/txtMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last message"
android:textColor="#666" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:id="@+id/txtTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09.30"
android:textSize="12sp"
android:textColor="#999" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>