diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatListResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatListResponse.kt new file mode 100644 index 0000000..ef55a4f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatListResponse.kt @@ -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, + + @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 +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index e0e5352..5419b1e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -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 - @PUT("chatstatus") suspend fun updateChatStatus( @Body request: UpdateChatRequest @@ -264,4 +258,8 @@ interface ApiService { suspend fun getChatDetail( @Path("chatRoomId") chatRoomId: Int ): Response + + @GET("chat") + suspend fun getChatList( + ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt index 751f93b..f2253f5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt @@ -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 { 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> { + 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt index 619185e..907d4bb 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt @@ -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) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListAdapter.kt new file mode 100644 index 0000000..27b1fcd --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListAdapter.kt @@ -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, + private val onClick: (ChatItemList) -> Unit +) : RecyclerView.Adapter() { + + 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]) + } +} diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt index 38979d2..4d395a2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt index 1ebaca8..b5db71a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -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 = _state - private val _chatRoomId = MutableLiveData(0) + val _chatRoomId = MutableLiveData(0) val chatRoomId: LiveData = _chatRoomId + private val _chatList = MutableLiveData>>() + val chatList: LiveData>> = _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() + } + } } /** diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index f6e2b69..902de35 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -92,14 +92,15 @@ app:layout_constraintTop_toBottomOf="@+id/chatToolbar"> - + + + + + android:padding="8dp" + android:clipToPadding="false" + tools:listitem="@layout/item_chat" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> -