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 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.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest 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.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.SearchRequest 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.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 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.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse 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.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.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse 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.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.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse 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.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse 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.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.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse 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.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.HeaderMap
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
@ -250,11 +245,10 @@ interface ApiService {
suspend fun sendChatLine( suspend fun sendChatLine(
@Part("store_id") storeId: RequestBody, @Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody, @Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody, @Part("product_id") productId: RequestBody?,
@Part chatimg: MultipartBody.Part? @Part chatimg: MultipartBody.Part?
): Response<SendChatResponse> ): Response<SendChatResponse>
@PUT("chatstatus") @PUT("chatstatus")
suspend fun updateChatStatus( suspend fun updateChatStatus(
@Body request: UpdateChatRequest @Body request: UpdateChatRequest
@ -264,4 +258,8 @@ interface ApiService {
suspend fun getChatDetail( suspend fun getChatDetail(
@Path("chatRoomId") chatRoomId: Int @Path("chatRoomId") chatRoomId: Int
): Response<ChatHistoryResponse> ): 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.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile 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.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.SendChatResponse
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -36,55 +38,56 @@ class ChatRepository @Inject constructor(
suspend fun sendChatMessage( suspend fun sendChatMessage(
storeId: Int, storeId: Int,
message: String, message: String,
productId: Int, productId: Int? = null,
imageFile: File? = null imageFile: File? = null,
chatRoomId: Int? = null // Not used in the actual API call but kept for compatibility
): Result<SendChatResponse> { ): Result<SendChatResponse> {
return try { return try {
// Create request bodies for text fields // Create multipart request parts
val storeIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), storeId.toString()) val storeIdPart = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val messageBody = RequestBody.create("text/plain".toMediaTypeOrNull(), message) val messagePart = message.toRequestBody("text/plain".toMediaTypeOrNull())
val productIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), productId.toString())
// Create multipart body for the image file // Add product ID part if provided
val imageMultipart = if (imageFile != null && imageFile.exists()) { val productIdPart = if (productId != null && productId > 0) {
// Log detailed file information productId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
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)
} else { } else {
// Pass null when no image is provided
null null
} }
// Log request info // Create image part if file is provided
Log.d(TAG, "Sending message to store ID: $storeId, product ID: $productId") val imagePart = if (imageFile != null && imageFile.exists()) {
Log.d(TAG, "Message content: $message") val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
Log.d(TAG, "Has image: ${imageFile != null && imageFile.exists()}") 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( val response = apiService.sendChatLine(
storeId = storeIdBody, storeId = storeIdPart,
message = messageBody, message = messagePart,
productId = productIdBody, productId = productIdPart,
chatimg = imageMultipart chatimg = imagePart
) )
if (response.isSuccessful) { if (response.isSuccessful) {
response.body()?.let { val body = response.body()
Result.Success(it) if (body != null) {
} ?: Result.Error(Exception("Send chat response is empty")) Result.Success(body)
} else { } else {
val errorBody = response.errorBody()?.string() ?: "Unknown error" Result.Error(Exception("Empty response body"))
Log.e(TAG, "HTTP Error: ${response.code()}, Body: $errorBody") }
} else {
val errorBody = response.errorBody()?.string() ?: "{}"
Log.e("ChatRepository", "API Error: ${response.code()} - $errorBody")
Result.Error(Exception("API Error: ${response.code()} - $errorBody")) Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception sending message", e) Log.e("ChatRepository", "Exception sending message", e)
e.printStackTrace()
Result.Error(e) Result.Error(e)
} }
} }
@ -128,4 +131,19 @@ class ChatRepository @Inject constructor(
Result.Error(e) 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) apiService = ApiConfig.getApiService(sessionManager)
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'") Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
// Log.d("ChatActivity", "User ID in storage: '${sessionManager.getUserId()}'")
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge() enableEdgeToEdge()
@ -125,7 +124,7 @@ class ChatActivity : AppCompatActivity() {
val productImage = intent.getStringExtra(Constants.EXTRA_PRODUCT_IMAGE) ?: "" val productImage = intent.getStringExtra(Constants.EXTRA_PRODUCT_IMAGE) ?: ""
val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f) val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: "" val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
// Check if user is logged in // Check if user is logged in
val token = sessionManager.getToken() val token = sessionManager.getToken()
@ -148,13 +147,18 @@ class ChatActivity : AppCompatActivity() {
productRating = productRating, productRating = productRating,
storeName = storeName storeName = storeName
) )
// Setup UI components // Setup UI components
setupRecyclerView() setupRecyclerView()
setupListeners() setupListeners()
setupTypingIndicator() setupTypingIndicator()
observeViewModel() 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() { private fun setupRecyclerView() {
@ -234,6 +238,7 @@ class ChatActivity : AppCompatActivity() {
} }
// Update product info // Update product info
if (!state.productName.isNullOrEmpty()) {
binding.tvProductName.text = state.productName binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating binding.ratingBar.rating = state.productRating
@ -241,7 +246,7 @@ class ChatActivity : AppCompatActivity() {
binding.tvSellerName.text = state.storeName binding.tvSellerName.text = state.storeName
// Load product image // Load product image
if (state.productImageUrl.isNotEmpty()) { if (!state.productImageUrl.isNullOrEmpty()) {
Glide.with(this@ChatActivity) Glide.with(this@ChatActivity)
.load(BASE_URL + state.productImageUrl) .load(BASE_URL + state.productImageUrl)
.centerCrop() .centerCrop()
@ -250,6 +255,14 @@ class ChatActivity : AppCompatActivity() {
.into(binding.imgProduct) .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 // Update attachment hint
if (state.hasAttachment) { if (state.hasAttachment) {
binding.editTextMessage.hint = getString(R.string.image_attached) binding.editTextMessage.hint = getString(R.string.image_attached)
@ -352,17 +365,70 @@ class ChatActivity : AppCompatActivity() {
} }
private fun handleSelectedImage(uri: Uri) { private fun handleSelectedImage(uri: Uri) {
// Get the file from Uri try {
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) Log.d(TAG, "Processing selected image: $uri")
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()
// 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) { if (filePath != null) {
viewModel.setSelectedImageFile(File(filePath)) 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() 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,12 +461,12 @@ class ChatActivity : AppCompatActivity() {
fun createIntent( fun createIntent(
context: Activity, context: Activity,
storeId: Int, storeId: Int,
productId: Int, productId: Int = 0,
productName: String?, productName: String? = null,
productPrice: String, productPrice: String = "",
productImage: String?, productImage: String? = null,
productRating: String?, productRating: String? = null,
storeName: String?, storeName: String? = null,
chatRoomId: Int = 0 chatRoomId: Int = 0
) { ) {
val intent = Intent(context, ChatActivity::class.java).apply { val intent = Intent(context, ChatActivity::class.java).apply {
@ -409,7 +475,18 @@ class ChatActivity : AppCompatActivity() {
putExtra(Constants.EXTRA_PRODUCT_NAME, productName) putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice) putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage) 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) putExtra(Constants.EXTRA_STORE_NAME, storeName)
if (chatRoomId > 0) { 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 package com.alya.ecommerce_serang.ui.chat
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ChatRepository 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.databinding.FragmentChatListBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -30,6 +31,7 @@ class ChatListFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext()) sessionManager = SessionManager(requireContext())
socketService = SocketIOService(sessionManager)
} }
@ -44,13 +46,43 @@ class ChatListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupView() viewModel.getChatList()
observeChatList()
} }
private fun setupView(){ private fun observeChatList() {
binding.btnTrial.setOnClickListener{ viewModel.chatList.observe(viewLifecycleOwner) { result ->
val intent = Intent(requireContext(), ChatActivity::class.java) when (result) {
startActivity(intent) 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.chat.ChatItem 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.api.response.chat.ChatLine
import com.alya.ecommerce_serang.data.repository.ChatRepository import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
@ -31,12 +32,15 @@ class ChatViewModel @Inject constructor(
private val _state = MutableLiveData(ChatUiState()) private val _state = MutableLiveData(ChatUiState())
val state: LiveData<ChatUiState> = _state val state: LiveData<ChatUiState> = _state
private val _chatRoomId = MutableLiveData<Int>(0) val _chatRoomId = MutableLiveData<Int>(0)
val chatRoomId: LiveData<Int> = _chatRoomId val chatRoomId: LiveData<Int> = _chatRoomId
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
// Store and product parameters // Store and product parameters
private var storeId: Int = 0 private var storeId: Int = 0
private var productId: Int = 0 private var productId: Int? = 0
private var currentUserId: Int? = null private var currentUserId: Int? = null
private var defaultUserId: Int = 0 private var defaultUserId: Int = 0
@ -83,27 +87,27 @@ class ChatViewModel @Inject constructor(
*/ */
fun setChatParameters( fun setChatParameters(
storeId: Int, storeId: Int,
productId: Int, productId: Int? = 0,
productName: String, productName: String? = null,
productPrice: String, productPrice: String? = null,
productImage: String, productImage: String? = null,
productRating: Float, productRating: Float? = 0f,
storeName: String storeName: String
) { ) {
this.storeId = storeId this.storeId = storeId
this.productId = productId this.productId = productId!!
this.productName = productName this.productName = productName.toString()
this.productPrice = productPrice this.productPrice = productPrice.toString()
this.productImage = productImage this.productImage = productImage.toString()
this.productRating = productRating this.productRating = productRating!!
this.storeName = storeName this.storeName = storeName
// Update state with product info // Update state with product info
updateState { updateState {
it.copy( it.copy(
productName = productName, productName = productName.toString(),
productPrice = productPrice, productPrice = productPrice.toString(),
productImageUrl = productImage, productImageUrl = productImage.toString(),
productRating = productRating, productRating = productRating,
storeName = storeName storeName = storeName
) )
@ -237,23 +241,50 @@ class ChatViewModel @Inject constructor(
* Sends a chat message * Sends a chat message
*/ */
fun sendMessage(message: String) { fun sendMessage(message: String) {
if (message.isBlank()) return if (message.isBlank() && selectedImageFile == null) {
Log.e(TAG, "Cannot send message: Both message and image are empty")
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.") }
return 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 { viewModelScope.launch {
updateState { it.copy(isSending = true) } updateState { it.copy(isSending = true) }
when (val result = chatRepository.sendChatMessage( 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, storeId = storeId,
message = message, message = message,
productId = productId, productId = productId,
imageFile = selectedImageFile imageFile = selectedImageFile,
)) { chatRoomId = existingChatRoomId
)
when (result) {
is Result.Success -> { is Result.Success -> {
// Add new message to the list // Add new message to the list
val chatLine = result.data.chatLine val chatLine = result.data.chatLine
@ -276,9 +307,8 @@ class ChatViewModel @Inject constructor(
Log.d(TAG, "Message sent successfully: ${chatLine.id}") Log.d(TAG, "Message sent successfully: ${chatLine.id}")
// Update the chat room ID if it's the first message // 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 val newChatRoomId = chatLine.chatRoomId
if ((_chatRoomId.value ?: 0) == 0 && newChatRoomId > 0) { if (existingChatRoomId == 0 && newChatRoomId > 0) {
Log.d(TAG, "Chat room created: $newChatRoomId") Log.d(TAG, "Chat room created: $newChatRoomId")
_chatRoomId.value = newChatRoomId _chatRoomId.value = newChatRoomId
@ -311,6 +341,15 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(isSending = true) } 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() socketService.disconnect()
Log.d(TAG, "ViewModel cleared, Socket.IO disconnected") 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"> app:layout_constraintTop_toBottomOf="@+id/chatToolbar">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/product_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"> android:padding="16dp">
<ImageView <ImageView
android:id="@+id/imgProduct" android:id="@+id/imgProduct"
android:layout_width="0dp" android:layout_width="64dp"
android:layout_height="0dp" android:layout_height="64dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" android:src="@drawable/placeholder_image"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"

View File

@ -1,19 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" 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"> tools:context=".ui.chat.ChatListFragment">
<TextView <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_width="match_parent"
android:layout_height="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 </LinearLayout>
android:id="@+id/btn_trial"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="trial button"/>
</FrameLayout>

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>