mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-12 18:22:22 +00:00
add chat store
This commit is contained in:
@ -29,13 +29,16 @@
|
|||||||
android:theme="@style/Theme.Ecommerce_serang"
|
android:theme="@style/Theme.Ecommerce_serang"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.profile.mystore.chat.ChatListStoreActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.product.storeDetail.StoreDetailActivity"
|
android:name=".ui.product.storeDetail.StoreDetailActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.auth.RegisterStoreActivity"
|
android:name=".ui.auth.RegisterStoreActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.editprofile.EditProfileCustActivity"
|
android:name=".ui.profile.editprofile.EditProfileCustActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -50,8 +53,8 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.profile.EditStoreProfileActivity"
|
android:name=".ui.profile.mystore.profile.EditStoreProfileActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.sells.shipment.DetailShipmentActivity"
|
android:name=".ui.profile.mystore.sells.shipment.DetailShipmentActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -73,6 +76,10 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.profile.mystore.chat.ChatStoreActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
||||||
@ -97,8 +104,8 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.order.address.AddAddressActivity"
|
android:name=".ui.order.address.AddAddressActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.order.address.AddressActivity"
|
android:name=".ui.order.address.AddressActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -146,8 +153,8 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.auth.RegisterActivity"
|
android:name=".ui.auth.RegisterActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:exported="true"
|
||||||
android:exported="true">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
@ -443,6 +443,17 @@ interface ApiService {
|
|||||||
@Part chatimg: MultipartBody.Part?
|
@Part chatimg: MultipartBody.Part?
|
||||||
): Response<SendChatResponse>
|
): Response<SendChatResponse>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("store/sendchat")
|
||||||
|
suspend fun sendChatMessageStore(
|
||||||
|
@PartMap parts: Map<String, @JvmSuppressWildcards RequestBody>,
|
||||||
|
@Part chatimg: MultipartBody.Part? = null
|
||||||
|
): Response<SendChatResponse>
|
||||||
|
|
||||||
|
@GET("store/chat")
|
||||||
|
suspend fun getChatListStore(
|
||||||
|
): Response<ChatListResponse>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("sendchat")
|
@POST("sendchat")
|
||||||
suspend fun sendChatMessage(
|
suspend fun sendChatMessage(
|
||||||
|
@ -90,6 +90,60 @@ class ChatRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sendChatMessageStore(
|
||||||
|
storeId: Int,
|
||||||
|
message: String,
|
||||||
|
productId: Int?, // Nullable and optional
|
||||||
|
imageFile: File? = null // Nullable and optional
|
||||||
|
): Result<SendChatResponse> {
|
||||||
|
return try {
|
||||||
|
val parts = mutableMapOf<String, RequestBody>()
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
parts["store_id"] = storeId.toString().toRequestBody("text/plain".toMediaType())
|
||||||
|
parts["message"] = message.toRequestBody("text/plain".toMediaType())
|
||||||
|
|
||||||
|
// Optional: Only include if productId is valid
|
||||||
|
if (productId != null && productId > 0) {
|
||||||
|
parts["product_id"] = productId.toString().toRequestBody("text/plain".toMediaType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Only include if imageFile is valid
|
||||||
|
val imagePart = imageFile?.takeIf { it.exists() }?.let { file ->
|
||||||
|
// val requestFile = file.asRequestBody("image/*".toMediaType())
|
||||||
|
val mimeType = when {
|
||||||
|
file.name.endsWith(".png", ignoreCase = true) -> "image/png"
|
||||||
|
file.name.endsWith(".jpg", ignoreCase = true) || file.name.endsWith(".jpeg", ignoreCase = true) -> "image/jpeg"
|
||||||
|
else -> "image/jpeg" // fallback
|
||||||
|
}
|
||||||
|
val requestFile = file.asRequestBody(mimeType.toMediaType())
|
||||||
|
MultipartBody.Part.createFormData("chatimg", file.name, requestFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the parts map keys and values (string representations)
|
||||||
|
Log.d("ChatRepository", "Sending chat message with parts:")
|
||||||
|
parts.forEach { (key, body) ->
|
||||||
|
Log.d("ChatRepository", "Key: $key, Value (approx): ${bodyToString(body)}")
|
||||||
|
}
|
||||||
|
Log.d("ChatRepository", "Sending chat message with imagePart: ${imagePart != null}")
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
val response = apiService.sendChatMessageStore(parts, imagePart)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body()?.let { Result.Success(it) } ?: Result.Error(Exception("Empty response body"))
|
||||||
|
} else {
|
||||||
|
val errorMsg = response.errorBody()?.string().orEmpty()
|
||||||
|
Log.e("ChatRepository", "API Error: ${response.code()} - $errorMsg")
|
||||||
|
Result.Error(Exception("API Error: ${response.code()} - $errorMsg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ChatRepository", "Exception sending chat message", e)
|
||||||
|
Result.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get string content from RequestBody (best effort)
|
// Helper function to get string content from RequestBody (best effort)
|
||||||
private fun bodyToString(requestBody: RequestBody): String {
|
private fun bodyToString(requestBody: RequestBody): String {
|
||||||
return try {
|
return try {
|
||||||
@ -217,4 +271,26 @@ class ChatRepository @Inject constructor(
|
|||||||
Result.Error(e)
|
Result.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getListChatStore(): Result<List<ChatItemList>> {
|
||||||
|
return try {
|
||||||
|
Log.d("ChatRepository", "Calling getChatListStore() from ApiService")
|
||||||
|
|
||||||
|
val response = apiService.getChatListStore()
|
||||||
|
|
||||||
|
Log.d("ChatRepository", "Response received: isSuccessful=${response.isSuccessful}, code=${response.code()}")
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val chat = response.body()?.chat ?: emptyList()
|
||||||
|
Log.d("ChatRepository", "Chat list size: ${chat.size}")
|
||||||
|
Result.Success(chat)
|
||||||
|
} else {
|
||||||
|
Log.e("ChatRepository", "Failed response: ${response.errorBody()?.string()}")
|
||||||
|
Result.Error(Exception("Failed to fetch chat list. Code: ${response.code()}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ChatRepository", "Exception during getChatListStore", e)
|
||||||
|
Result.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -41,6 +41,9 @@ class ChatViewModel @Inject constructor(
|
|||||||
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
|
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
|
||||||
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
|
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
|
||||||
|
|
||||||
|
private val _chatListStore = MutableLiveData<Result<List<ChatItemList>>>()
|
||||||
|
val chatListStore: LiveData<Result<List<ChatItemList>>> = _chatListStore
|
||||||
|
|
||||||
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
|
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
|
||||||
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
|
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
|
||||||
|
|
||||||
@ -367,6 +370,126 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendMessageStore(message: String) {
|
||||||
|
Log.d(TAG, "=== SEND MESSAGE ===")
|
||||||
|
Log.d(TAG, "Message: '$message'")
|
||||||
|
Log.d(TAG, "Has attachment: ${selectedImageFile != null}")
|
||||||
|
Log.d(TAG, "Selected image file: ${selectedImageFile?.absolutePath}")
|
||||||
|
Log.d(TAG, "File exists: ${selectedImageFile?.exists()}")
|
||||||
|
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) }
|
||||||
|
|
||||||
|
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 safeProductId = if (productId == 0) null else productId
|
||||||
|
|
||||||
|
|
||||||
|
val result = chatRepository.sendChatMessageStore(
|
||||||
|
storeId = storeId,
|
||||||
|
message = message,
|
||||||
|
productId = safeProductId,
|
||||||
|
imageFile = selectedImageFile
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a message status (delivered, read)
|
* Updates a message status (delivered, read)
|
||||||
*/
|
*/
|
||||||
@ -488,6 +611,17 @@ class ChatViewModel @Inject constructor(
|
|||||||
_chatList.value = chatRepository.getListChat()
|
_chatList.value = chatRepository.getListChat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChatListStore() {
|
||||||
|
Log.d("ChatViewModel", "getChatListStore() called")
|
||||||
|
_chatListStore.value = Result.Loading
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = chatRepository.getListChatStore()
|
||||||
|
Log.d("ChatViewModel", "getChatListStore() result: $result")
|
||||||
|
_chatListStore.value = result
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,8 +14,8 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
|||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityMyStoreBinding
|
import com.alya.ecommerce_serang.databinding.ActivityMyStoreBinding
|
||||||
import com.alya.ecommerce_serang.ui.chat.ChatListFragment
|
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
|
||||||
|
import com.alya.ecommerce_serang.ui.profile.mystore.chat.ChatListStoreActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
|
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
|
||||||
@ -124,10 +124,8 @@ class MyStoreActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutInbox.setOnClickListener {
|
binding.layoutInbox.setOnClickListener {
|
||||||
supportFragmentManager.beginTransaction()
|
val intent = Intent(this, ChatListStoreActivity::class.java)
|
||||||
.replace(android.R.id.content, ChatListFragment())
|
startActivity(intent)
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.profile.mystore.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])
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.profile.mystore.chat
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
|
import com.alya.ecommerce_serang.data.repository.ChatRepository
|
||||||
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
|
import com.alya.ecommerce_serang.databinding.ActivityChatListStoreBinding
|
||||||
|
import com.alya.ecommerce_serang.ui.chat.ChatViewModel
|
||||||
|
import com.alya.ecommerce_serang.ui.chat.SocketIOService
|
||||||
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
|
||||||
|
class ChatListStoreActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityChatListStoreBinding
|
||||||
|
private lateinit var socketService: SocketIOService
|
||||||
|
private lateinit var apiService: ApiService
|
||||||
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
private val TAG = "ChatListStoreActivity"
|
||||||
|
|
||||||
|
private val viewModel: ChatViewModel by viewModels {
|
||||||
|
BaseViewModelFactory {
|
||||||
|
val apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
val chatRepository = ChatRepository(apiService)
|
||||||
|
ChatViewModel(chatRepository, socketService, sessionManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Initialize SessionManager and SocketService
|
||||||
|
sessionManager = SessionManager(this)
|
||||||
|
socketService = SocketIOService(sessionManager)
|
||||||
|
|
||||||
|
// Inflate the layout and set content view
|
||||||
|
binding = ActivityChatListStoreBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
setupToolbar()
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||||
|
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
view.setPadding(
|
||||||
|
systemBars.left,
|
||||||
|
systemBars.top,
|
||||||
|
systemBars.right,
|
||||||
|
systemBars.bottom
|
||||||
|
)
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Fetching chat list from ViewModel")
|
||||||
|
viewModel.getChatListStore()
|
||||||
|
observeChatList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbar(){
|
||||||
|
binding.header.headerLeftIcon.setOnClickListener{
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
binding.header.headerTitle.text = "Pesan"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeChatList() {
|
||||||
|
viewModel.chatListStore.observe(this) { result ->
|
||||||
|
Log.d(TAG, "Observer triggered with result: $result")
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is Result.Success -> {
|
||||||
|
Log.d(TAG, "Chat list fetch success. Data size: ${result.data.size}")
|
||||||
|
val adapter = ChatListAdapter(result.data) { chatItem ->
|
||||||
|
Log.d(TAG, "Chat item clicked: storeId=${chatItem.storeId}, chatRoomId=${chatItem.chatRoomId}")
|
||||||
|
val intent = ChatStoreActivity.createIntent(
|
||||||
|
context = this,
|
||||||
|
storeId = chatItem.storeId,
|
||||||
|
productId = 0,
|
||||||
|
productName = null,
|
||||||
|
productPrice = "",
|
||||||
|
productImage = null,
|
||||||
|
productRating = null,
|
||||||
|
storeName = chatItem.storeName,
|
||||||
|
chatRoomId = chatItem.chatRoomId,
|
||||||
|
storeImage = chatItem.storeImage
|
||||||
|
)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
binding.chatListRecyclerView.adapter = adapter
|
||||||
|
Log.d(TAG, "Adapter set successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e(TAG, "Failed to load chats: ${result.exception.message}")
|
||||||
|
Toast.makeText(this, "Failed to load chats", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.Loading -> {
|
||||||
|
Log.d(TAG, "Chat list is loading...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
//package com.alya.ecommerce_serang.ui.profile.mystore.chat
|
||||||
|
//
|
||||||
|
//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.ui.chat.ChatViewModel
|
||||||
|
//import com.alya.ecommerce_serang.ui.chat.SocketIOService
|
||||||
|
//import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
|
//import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
//
|
||||||
|
//class ChatListStoreFragment : Fragment() {
|
||||||
|
//
|
||||||
|
// private var _binding: FragmentChatListBinding? = null
|
||||||
|
//
|
||||||
|
// private val binding get() = _binding!!
|
||||||
|
// private lateinit var socketService: SocketIOService
|
||||||
|
// private lateinit var sessionManager: SessionManager
|
||||||
|
//
|
||||||
|
// private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
|
||||||
|
// BaseViewModelFactory {
|
||||||
|
// val apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
// val chatRepository = ChatRepository(apiService)
|
||||||
|
// ChatViewModel(chatRepository, socketService, sessionManager)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// super.onCreate(savedInstanceState)
|
||||||
|
// sessionManager = SessionManager(requireContext())
|
||||||
|
// socketService = SocketIOService(sessionManager)
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onCreateView(
|
||||||
|
// inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
// savedInstanceState: Bundle?
|
||||||
|
// ): View {
|
||||||
|
// _binding = FragmentChatListBinding.inflate(inflater, container, false)
|
||||||
|
// return _binding!!.root
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
// super.onViewCreated(view, savedInstanceState)
|
||||||
|
//
|
||||||
|
// viewModel.getChatListStore()
|
||||||
|
// observeChatList()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun observeChatList() {
|
||||||
|
// viewModel.chatListStore.observe(viewLifecycleOwner) { result ->
|
||||||
|
// when (result) {
|
||||||
|
// is Result.Success -> {
|
||||||
|
// val adapter = ChatListAdapter(result.data) { chatItem ->
|
||||||
|
// // Use the ChatActivity.createIntent factory method for proper navigation
|
||||||
|
// ChatStoreActivity.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,
|
||||||
|
// storeImage = chatItem.storeImage
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// 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
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// companion object{
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//}
|
@ -0,0 +1,593 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.profile.mystore.chat
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
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.ui.chat.ChatAdapter
|
||||||
|
import com.alya.ecommerce_serang.ui.chat.ChatViewModel
|
||||||
|
import com.alya.ecommerce_serang.utils.Constants
|
||||||
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ChatStoreActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityChatBinding
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var apiService: ApiService
|
||||||
|
|
||||||
|
private lateinit var chatAdapter: ChatAdapter
|
||||||
|
|
||||||
|
private val viewModel: ChatViewModel by viewModels()
|
||||||
|
|
||||||
|
// For image attachment
|
||||||
|
private var tempImageUri: Uri? = null
|
||||||
|
|
||||||
|
// Typing indicator handler
|
||||||
|
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||||
|
private val stopTypingRunnable = Runnable {
|
||||||
|
viewModel.sendTypingStatus(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activity Result Launchers
|
||||||
|
private val pickImageLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
handleSelectedImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val takePictureLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
tempImageUri?.let { uri ->
|
||||||
|
handleSelectedImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityChatBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
sessionManager = SessionManager(this)
|
||||||
|
apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
|
||||||
|
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
// Apply insets to your root layout
|
||||||
|
|
||||||
|
|
||||||
|
// Get parameters from intent
|
||||||
|
val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0)
|
||||||
|
val productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0)
|
||||||
|
val productName = intent.getStringExtra(Constants.EXTRA_PRODUCT_NAME) ?: ""
|
||||||
|
val productPrice = intent.getStringExtra(Constants.EXTRA_PRODUCT_PRICE) ?: ""
|
||||||
|
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)
|
||||||
|
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
val token = sessionManager.getToken()
|
||||||
|
|
||||||
|
if (token.isEmpty()) {
|
||||||
|
// User not logged in, redirect to login
|
||||||
|
Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show()
|
||||||
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvStoreName.text = storeName
|
||||||
|
val fullImageUrl = when (val img = storeImg) {
|
||||||
|
is String -> {
|
||||||
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
|
}
|
||||||
|
else -> R.drawable.placeholder_image
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.into(binding.imgProfile)
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutChatInput) { view, insets ->
|
||||||
|
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
|
||||||
|
val bottomPadding = max(imeInsets.bottom, navBarInsets.bottom)
|
||||||
|
view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, bottomPadding)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle top inset on toolbar (status bar height)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.chatToolbar) { view, insets ->
|
||||||
|
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||||
|
view.setPadding(view.paddingLeft, statusBarHeight, view.paddingRight, view.paddingBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerChat) { view, insets ->
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
val bottomPadding = binding.layoutChatInput.height + navBarInsets.bottom
|
||||||
|
|
||||||
|
view.setPadding(
|
||||||
|
view.paddingLeft,
|
||||||
|
view.paddingTop,
|
||||||
|
view.paddingRight,
|
||||||
|
bottomPadding
|
||||||
|
)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// For RecyclerView, add bottom padding = chat input height + nav bar height (to avoid last message hidden)
|
||||||
|
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(binding.root,
|
||||||
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
||||||
|
|
||||||
|
private var startPaddingBottom = 0
|
||||||
|
private var endPaddingBottom = 0
|
||||||
|
|
||||||
|
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
||||||
|
startPaddingBottom = binding.layoutChatInput.paddingBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart(
|
||||||
|
animation: WindowInsetsAnimationCompat,
|
||||||
|
bounds: WindowInsetsAnimationCompat.BoundsCompat
|
||||||
|
): WindowInsetsAnimationCompat.BoundsCompat {
|
||||||
|
endPaddingBottom = binding.layoutChatInput.paddingBottom
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgress(
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
runningAnimations: MutableList<WindowInsetsAnimationCompat>
|
||||||
|
): WindowInsetsCompat {
|
||||||
|
val imeAnimation = runningAnimations.find {
|
||||||
|
it.typeMask and WindowInsetsCompat.Type.ime() != 0
|
||||||
|
} ?: return insets
|
||||||
|
|
||||||
|
val animatedBottomPadding = startPaddingBottom +
|
||||||
|
(endPaddingBottom - startPaddingBottom) * imeAnimation.interpolatedFraction
|
||||||
|
|
||||||
|
binding.layoutChatInput.setPadding(
|
||||||
|
binding.layoutChatInput.paddingLeft,
|
||||||
|
binding.layoutChatInput.paddingTop,
|
||||||
|
binding.layoutChatInput.paddingRight,
|
||||||
|
animatedBottomPadding.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.recyclerChat.setPadding(
|
||||||
|
binding.recyclerChat.paddingLeft,
|
||||||
|
binding.recyclerChat.paddingTop,
|
||||||
|
binding.recyclerChat.paddingRight,
|
||||||
|
animatedBottomPadding.toInt() + binding.layoutChatInput.height
|
||||||
|
)
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set chat parameters to ViewModel
|
||||||
|
viewModel.setChatParameters(
|
||||||
|
storeId = storeId,
|
||||||
|
productId = productId,
|
||||||
|
productName = productName,
|
||||||
|
productPrice = productPrice,
|
||||||
|
productImage = productImage,
|
||||||
|
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() {
|
||||||
|
chatAdapter = ChatAdapter()
|
||||||
|
binding.recyclerChat.apply {
|
||||||
|
adapter = chatAdapter
|
||||||
|
layoutManager = LinearLayoutManager(this@ChatStoreActivity).apply {
|
||||||
|
stackFromEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// binding.recyclerChat.setPadding(
|
||||||
|
// binding.recyclerChat.paddingLeft,
|
||||||
|
// binding.recyclerChat.paddingTop,
|
||||||
|
// binding.recyclerChat.paddingRight,
|
||||||
|
// binding.layoutChatInput.height + binding.root.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
// Back button
|
||||||
|
binding.btnBack.setOnClickListener {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options button
|
||||||
|
binding.btnOptions.setOnClickListener {
|
||||||
|
showOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send button
|
||||||
|
binding.btnSend.setOnClickListener {
|
||||||
|
val message = binding.editTextMessage.text.toString().trim()
|
||||||
|
val currentState = viewModel.state.value
|
||||||
|
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
|
||||||
|
viewModel.sendMessageStore(message)
|
||||||
|
binding.editTextMessage.text.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment button
|
||||||
|
binding.btnAttachment.setOnClickListener {
|
||||||
|
checkPermissionsAndShowImagePicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupTypingIndicator() {
|
||||||
|
binding.editTextMessage.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
viewModel.sendTypingStatus(true)
|
||||||
|
|
||||||
|
// Reset the timer
|
||||||
|
typingHandler.removeCallbacks(stopTypingRunnable)
|
||||||
|
typingHandler.postDelayed(stopTypingRunnable, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.editTextMessage.requestFocus()
|
||||||
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewModel() {
|
||||||
|
viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
|
||||||
|
if (chatRoomId > 0) {
|
||||||
|
// Chat room has been created, now we can join the Socket.IO room
|
||||||
|
viewModel.joinSocketRoom(chatRoomId)
|
||||||
|
|
||||||
|
// Now we can also load chat history
|
||||||
|
viewModel.loadChatHistory(chatRoomId)
|
||||||
|
Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe state changes using LiveData
|
||||||
|
viewModel.state.observe(this, Observer { state ->
|
||||||
|
// Update messages
|
||||||
|
chatAdapter.submitList(state.messages)
|
||||||
|
|
||||||
|
// Scroll to bottom if new message
|
||||||
|
if (state.messages.isNotEmpty()) {
|
||||||
|
binding.recyclerChat.scrollToPosition(state.messages.size - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update product info
|
||||||
|
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
|
||||||
|
binding.tvStoreName.text=state.storeName
|
||||||
|
|
||||||
|
val fullImageUrl = when (val img = state.productImageUrl) {
|
||||||
|
is String -> {
|
||||||
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
|
}
|
||||||
|
else -> R.drawable.placeholder_image
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load product image
|
||||||
|
if (!state.productImageUrl.isNullOrEmpty()) {
|
||||||
|
Glide.with(this@ChatStoreActivity)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.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)
|
||||||
|
} else {
|
||||||
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Show typing indicator
|
||||||
|
binding.tvTypingIndicator.visibility =
|
||||||
|
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
state.error?.let { error ->
|
||||||
|
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showOptionsMenu() {
|
||||||
|
val options = arrayOf(
|
||||||
|
getString(R.string.block_user),
|
||||||
|
getString(R.string.report),
|
||||||
|
getString(R.string.clear_chat),
|
||||||
|
getString(R.string.cancel)
|
||||||
|
)
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.options))
|
||||||
|
.setItems(options) { dialog, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> Toast.makeText(this, R.string.block_user_selected, Toast.LENGTH_SHORT).show()
|
||||||
|
1 -> Toast.makeText(this, R.string.report_selected, Toast.LENGTH_SHORT).show()
|
||||||
|
2 -> Toast.makeText(this, R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissionsAndShowImagePicker() {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA),
|
||||||
|
Constants.REQUEST_STORAGE_PERMISSION
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showImagePickerOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showImagePickerOptions() {
|
||||||
|
val options = arrayOf(
|
||||||
|
getString(R.string.take_photo),
|
||||||
|
getString(R.string.choose_from_gallery),
|
||||||
|
getString(R.string.cancel)
|
||||||
|
)
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.select_attachment))
|
||||||
|
.setItems(options) { dialog, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> openCamera()
|
||||||
|
1 -> openGallery()
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCamera() {
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val imageFileName = "IMG_${timeStamp}.jpg"
|
||||||
|
val storageDir = getExternalFilesDir(null)
|
||||||
|
val imageFile = File(storageDir, imageFileName)
|
||||||
|
|
||||||
|
tempImageUri = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
"${applicationContext.packageName}.fileprovider",
|
||||||
|
imageFile
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
takePictureLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openGallery() {
|
||||||
|
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
||||||
|
pickImageLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectedImage(uri: Uri) {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Processing selected image: $uri")
|
||||||
|
|
||||||
|
// Always use the copy-to-cache approach for reliability
|
||||||
|
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
val fileName = "chat_img_${System.currentTimeMillis()}.jpg"
|
||||||
|
val outputFile = File(cacheDir, fileName)
|
||||||
|
|
||||||
|
outputFile.outputStream().use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputFile.exists() && outputFile.length() > 0) {
|
||||||
|
if (outputFile.length() > 5 * 1024 * 1024) {
|
||||||
|
Log.e(TAG, "File too large: ${outputFile.length()} bytes")
|
||||||
|
Toast.makeText(this, "Image too large (max 5MB)", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}")
|
||||||
|
viewModel.setSelectedImageFile(outputFile)
|
||||||
|
Toast.makeText(this, "Image selected", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to create image file")
|
||||||
|
Toast.makeText(this, "Failed to process image", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Could not open input stream for URI: $uri")
|
||||||
|
Toast.makeText(this, "Could not access image", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error handling selected image", e)
|
||||||
|
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
showImagePickerOptions()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
typingHandler.removeCallbacks(stopTypingRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ChatActivity"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an intent to start the ChatActivity
|
||||||
|
*/
|
||||||
|
fun createIntent(
|
||||||
|
context: Activity,
|
||||||
|
storeId: Int,
|
||||||
|
productId: Int = 0,
|
||||||
|
productName: String? = null,
|
||||||
|
productPrice: String = "",
|
||||||
|
productImage: String? = null,
|
||||||
|
productRating: String? = null,
|
||||||
|
storeName: String? = null,
|
||||||
|
chatRoomId: Int = 0,
|
||||||
|
storeImage: String? = null
|
||||||
|
): Intent {
|
||||||
|
return Intent(context, ChatStoreActivity::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_STORE_IMAGE, storeImage)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
putExtra(Constants.EXTRA_CHAT_ROOM_ID, chatRoomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if implement typing status
|
||||||
|
// private fun handleConnectionState(state: ConnectionState) {
|
||||||
|
// when (state) {
|
||||||
|
// is ConnectionState.Connected -> {
|
||||||
|
// binding.tvConnectionStatus.visibility = View.GONE
|
||||||
|
// }
|
||||||
|
// is ConnectionState.Connecting -> {
|
||||||
|
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||||
|
// binding.tvConnectionStatus.text = getString(R.string.connecting)
|
||||||
|
// }
|
||||||
|
// is ConnectionState.Disconnected -> {
|
||||||
|
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||||
|
// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
|
||||||
|
// }
|
||||||
|
// is ConnectionState.Error -> {
|
||||||
|
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||||
|
// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
40
app/src/main/res/layout/activity_chat_list_store.xml
Normal file
40
app/src/main/res/layout/activity_chat_list_store.xml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.profile.mystore.chat.ChatListStoreActivity">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/header"
|
||||||
|
layout="@layout/header" />
|
||||||
|
|
||||||
|
<!-- <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"
|
||||||
|
app:dividerColor="@color/black_100" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/chatListRecyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
tools:listitem="@layout/item_chat"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
Reference in New Issue
Block a user