fixed chat (hide attach + adjust attach product)

This commit is contained in:
shaulascr
2025-05-02 09:28:33 +07:00
parent 3a06f65e96
commit d29e9072f8
11 changed files with 276 additions and 135 deletions

View File

@ -1,7 +1,6 @@
package com.alya.ecommerce_serang.data.api.response.chat
import com.google.gson.annotations.SerializedName
import java.io.File
data class ChatHistoryResponse(
@ -15,7 +14,7 @@ data class ChatHistoryResponse(
data class ChatItem(
@field:SerializedName("attachment")
val attachment: File? = null,
val attachment: String? = null,
@field:SerializedName("product_id")
val productId: Int,

View File

@ -14,7 +14,7 @@ data class SendChatResponse(
data class ChatLine(
@field:SerializedName("attachment")
val attachment: String,
val attachment: String? = null,
@field:SerializedName("product_id")
val productId: Int,

View File

@ -14,7 +14,7 @@ data class UpdateChatResponse(
data class Address(
@field:SerializedName("attachment")
val attachment: Any,
val attachment: String? = null,
@field:SerializedName("product_id")
val productId: Int,

View File

@ -223,12 +223,13 @@ interface ApiService {
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@PUT("chatstatus")
suspend fun updateChatStatus(
@Body request: UpdateChatRequest
): Response<UpdateChatResponse>
@GET("chatdetail/{chatRoomId}")
@GET("chat/{chatRoomId}")
suspend fun getChatDetail(
@Path("chatRoomId") chatRoomId: Int
): Response<ChatHistoryResponse>

View File

@ -0,0 +1,131 @@
package com.alya.ecommerce_serang.data.repository
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.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 java.io.File
import javax.inject.Inject
class ChatRepository @Inject constructor(
private val apiService: ApiService
) {
private val TAG = "ChatRepository"
suspend fun fetchUserProfile(): Result<UserProfile?> {
return try {
val response = apiService.getUserProfile()
if (response.isSuccessful) {
response.body()?.user?.let {
Result.Success(it) // ✅ Returning only UserProfile
} ?: Result.Error(Exception("User data not found"))
} else {
Result.Error(Exception("Error fetching profile: ${response.code()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun sendChatMessage(
storeId: Int,
message: String,
productId: Int,
imageFile: File? = null
): 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 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)
} 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()}")
// Make the API call
val response = apiService.sendChatLine(
storeId = storeIdBody,
message = messageBody,
productId = productIdBody,
chatimg = imageMultipart
)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Send chat response is empty"))
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "HTTP Error: ${response.code()}, Body: $errorBody")
Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
}
} catch (e: Exception) {
Log.e(TAG, "Exception sending message", e)
e.printStackTrace()
Result.Error(e)
}
}
suspend fun updateMessageStatus(
messageId: Int,
status: String
): Result<UpdateChatResponse> {
return try {
val requestBody = UpdateChatRequest(
id = messageId,
status = status
)
val response = apiService.updateChatStatus(requestBody)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Update status response is empty"))
} else {
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun getChatHistory(chatRoomId: Int): Result<ChatHistoryResponse> {
return try {
val response = apiService.getChatDetail(chatRoomId)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Chat history response is empty"))
} else {
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
}

View File

@ -3,19 +3,10 @@ package com.alya.ecommerce_serang.data.repository
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
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.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
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.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.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserRepository(private val apiService: ApiService) {
@ -65,100 +56,101 @@ class UserRepository(private val apiService: ApiService) {
}
}
suspend fun sendChatMessage(
storeId: Int,
message: String,
productId: Int,
imageFile: File? = null
): Result<SendChatResponse> {
return try {
// Create request bodies for text fields
val storeIdBody = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val messageBody = message.toRequestBody("text/plain".toMediaTypeOrNull())
val productIdBody = productId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
// Create multipart body for the image file
val imageMultipart = if (imageFile != null && imageFile.exists()) {
val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile)
} else {
// Create an empty part if no image is provided
val emptyRequest = "".toRequestBody("text/plain".toMediaTypeOrNull())
MultipartBody.Part.createFormData("chatimg", "", emptyRequest)
}
// Make the API call
val response = apiService.sendChatLine(
storeId = storeIdBody,
message = messageBody,
productId = productIdBody,
chatimg = imageMultipart
)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Send chat response is empty"))
} else {
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Updates the status of a message (sent, delivered, read)
*
* @param messageId The ID of the message to update
* @param status The new status to set
* @return Result containing the updated message details or error
*/
suspend fun updateMessageStatus(
messageId: Int,
status: String
): Result<UpdateChatResponse> {
return try {
val requestBody = UpdateChatRequest(
id = messageId,
status = status
)
val response = apiService.updateChatStatus(requestBody)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Update status response is empty"))
} else {
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Gets the chat history for a specific chat room
*
* @param chatRoomId The ID of the chat room
* @return Result containing the list of chat messages or error
*/
suspend fun getChatHistory(chatRoomId: Int): Result<ChatHistoryResponse> {
return try {
val response = apiService.getChatDetail(chatRoomId)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Chat history response is empty"))
} else {
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
// suspend fun sendChatMessage(
// storeId: Int,
// message: String,
// productId: Int,
// imageFile: File? = null
// ): Result<SendChatResponse> {
// return try {
// // Create multipart request builder
// val requestBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
//
// // Add text fields
// requestBodyBuilder.addFormDataPart("store_id", storeId.toString())
// requestBodyBuilder.addFormDataPart("message", message)
// requestBodyBuilder.addFormDataPart("product_id", productId.toString())
//
// // Add image if it exists
// if (imageFile != null && imageFile.exists()) {
// val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
// requestBodyBuilder.addFormDataPart("chatimg", imageFile.name, requestFile)
// }
//
// // Build the final request body
// val requestBody = requestBodyBuilder.build()
//
// // Make the API call using a custom endpoint that takes a plain MultipartBody
// val response = apiService.sendChatLineWithBody(requestBody)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Send chat response is empty"))
// } else {
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
// Log.e("ChatRepository", "HTTP Error: ${response.code()}, Body: $errorBody")
// Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
// }
// } catch (e: Exception) {
// Log.e("ChatRepository", "Exception sending message", e)
// e.printStackTrace()
// Result.Error(e)
// }
// }
//
// /**
// * Updates the status of a message (sent, delivered, read)
// *
// * @param messageId The ID of the message to update
// * @param status The new status to set
// * @return Result containing the updated message details or error
// */
// suspend fun updateMessageStatus(
// messageId: Int,
// status: String
// ): Result<UpdateChatResponse> {
// return try {
// val requestBody = UpdateChatRequest(
// id = messageId,
// status = status
// )
//
// val response = apiService.updateChatStatus(requestBody)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Update status response is empty"))
// } else {
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
// }
// } catch (e: Exception) {
// Result.Error(e)
// }
// }
//
// /**
// * Gets the chat history for a specific chat room
// *
// * @param chatRoomId The ID of the chat room
// * @return Result containing the list of chat messages or error
// */
// suspend fun getChatHistory(chatRoomId: Int): Result<ChatHistoryResponse> {
// return try {
// val response = apiService.getChatDetail(chatRoomId)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Chat history response is empty"))
// } else {
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
// }
// } catch (e: Exception) {
// Result.Error(e)
// }
// }
}

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.di
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.ui.chat.SocketIOService
import com.alya.ecommerce_serang.utils.SessionManager
@ -16,7 +17,13 @@ object ChatModule {
@Provides
@Singleton
fun provideChatRepository(apiService: ApiService): UserRepository {
fun provideChatRepository(apiService: ApiService): ChatRepository {
return ChatRepository(apiService)
}
@Provides
@Singleton
fun provideUserRepository(apiService: ApiService): UserRepository {
return UserRepository(apiService)
}

View File

@ -27,6 +27,8 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.databinding.ActivityChatBinding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.utils.Constants
@ -47,6 +49,9 @@ class ChatActivity : AppCompatActivity() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
lateinit var apiService: ApiService
private lateinit var chatAdapter: ChatAdapter
private val viewModel: ChatViewModel by viewModels()
@ -92,8 +97,10 @@ class ChatActivity : AppCompatActivity() {
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
Log.d("ChatActivity", "User ID in storage: '${sessionManager.getUserId()}'")
// Log.d("ChatActivity", "User ID in storage: '${sessionManager.getUserId()}'")
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
@ -121,7 +128,6 @@ class ChatActivity : AppCompatActivity() {
// Check if user is logged in
val userId = sessionManager.getUserId()
val token = sessionManager.getToken()
if (token.isEmpty()) {

View File

@ -76,7 +76,7 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMess
binding.imgStatus.setImageResource(statusIcon)
// Handle attachment if exists
if (message.attachment.isNotEmpty()) {
if (message.attachment?.isNotEmpty() == true) {
binding.imgAttachment.visibility = View.VISIBLE
Glide.with(binding.root.context)
.load(BASE_URL + message.attachment)
@ -101,7 +101,7 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMess
binding.tvTimestamp.text = message.time
// Handle attachment if exists
if (message.attachment.isNotEmpty()) {
if (message.attachment?.isNotEmpty() == true) {
binding.imgAttachment.visibility = View.VISIBLE
Glide.with(binding.root.context)
.load(BASE_URL + message.attachment)

View File

@ -8,7 +8,7 @@ import android.view.ViewGroup
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.UserRepository
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.databinding.FragmentChatListBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -23,8 +23,8 @@ class ChatListFragment : Fragment() {
private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ChatViewModel(userRepository, socketService, sessionManager)
val chatRepository = ChatRepository(apiService)
ChatViewModel(chatRepository, socketService, sessionManager)
}
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -7,8 +7,8 @@ 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.ChatLine
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.Constants
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
@ -20,7 +20,7 @@ import javax.inject.Inject
@HiltViewModel
class ChatViewModel @Inject constructor(
private val chatRepository: UserRepository,
private val chatRepository: ChatRepository,
private val socketService: SocketIOService,
private val sessionManager: SessionManager
) : ViewModel() {
@ -37,10 +37,9 @@ class ChatViewModel @Inject constructor(
// Store and product parameters
private var storeId: Int = 0
private var productId: Int = 0
private var currentUserId: Int? = 0
private var currentUserId: Int? = null
private var defaultUserId: Int = 0
// Product details for display
private var productName: String = ""
private var productPrice: String = ""
@ -52,7 +51,7 @@ class ChatViewModel @Inject constructor(
private var selectedImageFile: File? = null
init {
// Try to get current user ID from SessionManager
// Try to get current user ID from the repository
viewModelScope.launch {
when (val result = chatRepository.fetchUserProfile()) {
is Result.Success -> {
@ -60,7 +59,7 @@ class ChatViewModel @Inject constructor(
Log.e(TAG, "User ID: $currentUserId")
// Move the validation and subsequent logic inside the coroutine
if (currentUserId == 0) {
if (currentUserId == null || currentUserId == 0) {
Log.e(TAG, "Error: User ID is not set or invalid")
updateState { it.copy(error = "User authentication error. Please login again.") }
} else {
@ -188,7 +187,7 @@ class ChatViewModel @Inject constructor(
/**
* Loads chat history
*/
fun loadChatHistory(chatRoomId : Int) {
fun loadChatHistory(chatRoomId: Int) {
if (chatRoomId <= 0) {
Log.e(TAG, "Cannot load chat history: Chat room ID is 0")
return
@ -198,7 +197,7 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(isLoading = true) }
when (val result = chatRepository.getChatHistory(chatRoomId)) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
is Result.Success -> {
val messages = result.data.chat.map { chatLine ->
convertChatLineToUiMessageHistory(chatLine)
}
@ -218,7 +217,7 @@ class ChatViewModel @Inject constructor(
.filter { it.senderId != currentUserId && it.status != Constants.STATUS_READ }
.forEach { updateMessageStatus(it.id, Constants.STATUS_READ) }
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
is Result.Error -> {
updateState {
it.copy(
isLoading = false,
@ -238,7 +237,7 @@ class ChatViewModel @Inject constructor(
* Sends a chat message
*/
fun sendMessage(message: String) {
if (message.isBlank() && selectedImageFile == null) return
if (message.isBlank()) return
if (storeId == 0 || productId == 0) {
Log.e(TAG, "Cannot send message: Store ID or Product ID is 0")
@ -255,7 +254,7 @@ class ChatViewModel @Inject constructor(
productId = productId,
imageFile = selectedImageFile
)) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
is Result.Success -> {
// Add new message to the list
val chatLine = result.data.chatLine
val newMessage = convertChatLineToUiMessage(chatLine)
@ -293,16 +292,22 @@ class ChatViewModel @Inject constructor(
// Clear the image attachment
selectedImageFile = null
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
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 = result.exception.message
error = errorMsg
)
}
Log.e(TAG, "Error sending message: ${result.exception.message}")
}
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
is Result.Loading -> {
updateState { it.copy(isSending = true) }
}
}
@ -317,7 +322,7 @@ class ChatViewModel @Inject constructor(
try {
val result = chatRepository.updateMessageStatus(messageId, status)
if (result is com.alya.ecommerce_serang.data.repository.Result.Success) {
if (result is Result.Success) {
// Update local message status
val currentMessages = _state.value?.messages ?: listOf()
val updatedMessages = currentMessages.map { message ->
@ -330,7 +335,7 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(messages = updatedMessages) }
Log.d(TAG, "Message status updated: $messageId -> $status")
} else if (result is com.alya.ecommerce_serang.data.repository.Result.Error) {
} else if (result is Result.Error) {
Log.e(TAG, "Error updating message status: ${result.exception.message}")
}
} catch (e: Exception) {
@ -386,7 +391,7 @@ class ChatViewModel @Inject constructor(
return ChatUiMessage(
id = chatLine.id,
message = chatLine.message,
attachment = chatLine.attachment,
attachment = chatLine.attachment ?: "", // Handle null attachment
status = chatLine.status,
time = formattedTime,
isSentByMe = chatLine.senderId == currentUserId
@ -408,7 +413,7 @@ class ChatViewModel @Inject constructor(
}
return ChatUiMessage(
attachment = "",
attachment = chatItem.attachment, // Handle null attachment
id = chatItem.id,
message = chatItem.message,
status = chatItem.status,
@ -451,7 +456,7 @@ data class ChatUiState(
data class ChatUiMessage(
val id: Int,
val message: String,
val attachment: String,
val attachment: String?,
val status: String,
val time: String,
val isSentByMe: Boolean