mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
add chat list (miss image, attach product in chat and date chat)
This commit is contained in:
@ -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
|
||||
)
|
@ -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>
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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])
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
|
@ -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>
|
60
app/src/main/res/layout/item_chat.xml
Normal file
60
app/src/main/res/layout/item_chat.xml
Normal 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>
|
Reference in New Issue
Block a user