mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
Merge branch 'screen-features'
This commit is contained in:
@ -60,7 +60,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.chat.ChatActivity"
|
android:name=".ui.chat.ChatActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden" /> <!-- <provider -->
|
android:windowSoftInputMode="adjustResize" /> <!-- <provider -->
|
||||||
<!-- android:name="androidx.startup.InitializationProvider" -->
|
<!-- android:name="androidx.startup.InitializationProvider" -->
|
||||||
<!-- android:authorities="${applicationId}.androidx-startup" -->
|
<!-- android:authorities="${applicationId}.androidx-startup" -->
|
||||||
<!-- tools:node="remove" /> -->
|
<!-- tools:node="remove" /> -->
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.chat.ChatStoreActivity"
|
android:name=".ui.profile.mystore.chat.ChatStoreActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
||||||
|
@ -7,6 +7,7 @@ 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.ChatItemList
|
||||||
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
|
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
|
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
@ -293,4 +294,22 @@ class ChatRepository @Inject constructor(
|
|||||||
Result.Error(e)
|
Result.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun fetchProductDetail(productId: Int): ProductResponse? {
|
||||||
|
return try {
|
||||||
|
val response = apiService.getDetailProduct(productId)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val productResponse = response.body()
|
||||||
|
Log.d("Order Repository", "Product detail fetched successfully: ${productResponse?.product?.productName}")
|
||||||
|
productResponse
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Log.e("Order Repository", "Error fetching product detail. Code: ${response.code()}, Error: $errorBody")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Order Repository", "Exception fetching product", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -34,6 +34,7 @@ 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.databinding.ActivityChatBinding
|
import com.alya.ecommerce_serang.databinding.ActivityChatBinding
|
||||||
import com.alya.ecommerce_serang.ui.auth.LoginActivity
|
import com.alya.ecommerce_serang.ui.auth.LoginActivity
|
||||||
|
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
||||||
import com.alya.ecommerce_serang.utils.Constants
|
import com.alya.ecommerce_serang.utils.Constants
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@ -43,7 +44,6 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ChatActivity : AppCompatActivity() {
|
class ChatActivity : AppCompatActivity() {
|
||||||
@ -103,9 +103,6 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
// Apply insets to your root layout
|
|
||||||
|
|
||||||
|
|
||||||
// Get parameters from intent
|
// Get parameters from intent
|
||||||
val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0)
|
val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0)
|
||||||
val productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0)
|
val productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0)
|
||||||
@ -117,6 +114,8 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
||||||
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
||||||
|
|
||||||
|
val shouldAttachProduct = intent.getBooleanExtra(Constants.EXTRA_ATTACH_PRODUCT, false)
|
||||||
|
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
val token = sessionManager.getToken()
|
val token = sessionManager.getToken()
|
||||||
|
|
||||||
@ -141,84 +140,6 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(binding.imgProfile)
|
.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
|
// Set chat parameters to ViewModel
|
||||||
viewModel.setChatParameters(
|
viewModel.setChatParameters(
|
||||||
storeId = storeId,
|
storeId = storeId,
|
||||||
@ -230,8 +151,14 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
storeName = storeName
|
storeName = storeName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (shouldAttachProduct && productId > 0) {
|
||||||
|
viewModel.enableProductAttachment()
|
||||||
|
showProductAttachmentToast()
|
||||||
|
}
|
||||||
|
|
||||||
// Setup UI components
|
// Setup UI components
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
setupWindowInsets()
|
||||||
setupListeners()
|
setupListeners()
|
||||||
setupTypingIndicator()
|
setupTypingIndicator()
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
@ -243,20 +170,95 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showProductAttachmentToast() {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Product will be attached to your message",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
chatAdapter = ChatAdapter()
|
chatAdapter = ChatAdapter { productInfo ->
|
||||||
|
// This lambda will be called when user taps on a product bubble
|
||||||
|
handleProductClick(productInfo)
|
||||||
|
}
|
||||||
binding.recyclerChat.apply {
|
binding.recyclerChat.apply {
|
||||||
adapter = chatAdapter
|
adapter = chatAdapter
|
||||||
layoutManager = LinearLayoutManager(this@ChatActivity).apply {
|
layoutManager = LinearLayoutManager(this@ChatActivity)
|
||||||
stackFromEnd = true
|
// Use clipToPadding to allow content to scroll under padding
|
||||||
}
|
clipToPadding = false
|
||||||
|
// Set minimal padding - we'll handle spacing differently
|
||||||
|
setPadding(paddingLeft, paddingTop, paddingRight, 16)
|
||||||
}
|
}
|
||||||
// binding.recyclerChat.setPadding(
|
}
|
||||||
// binding.recyclerChat.paddingLeft,
|
|
||||||
// binding.recyclerChat.paddingTop,
|
private fun setupWindowInsets() {
|
||||||
// binding.recyclerChat.paddingRight,
|
ViewCompat.setOnApplyWindowInsetsListener(binding.chatToolbar) { view, insets ->
|
||||||
// binding.layoutChatInput.height + binding.root.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||||
// )
|
view.updatePadding(top = statusBarInsets.top)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IME (keyboard) and navigation bar insets for the input layout only
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutChatInput) { view, insets ->
|
||||||
|
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
|
||||||
|
Log.d(TAG, "Insets - IME: ${imeInsets.bottom}, NavBar: ${navBarInsets.bottom}")
|
||||||
|
|
||||||
|
val bottomInset = if (imeInsets.bottom > 0) {
|
||||||
|
imeInsets.bottom
|
||||||
|
} else {
|
||||||
|
navBarInsets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only apply padding to the input layout
|
||||||
|
view.updatePadding(bottom = bottomInset)
|
||||||
|
|
||||||
|
// When keyboard appears, scroll to bottom to keep last message visible
|
||||||
|
if (imeInsets.bottom > 0) {
|
||||||
|
// Keyboard is visible - scroll to bottom with delay to ensure layout is complete
|
||||||
|
binding.recyclerChat.postDelayed({
|
||||||
|
scrollToBottomSmooth()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth animation for keyboard transitions
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(
|
||||||
|
binding.layoutChatInput,
|
||||||
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
||||||
|
|
||||||
|
override fun onProgress(
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
runningAnimations: MutableList<WindowInsetsAnimationCompat>
|
||||||
|
): WindowInsetsCompat {
|
||||||
|
val imeAnimation = runningAnimations.find {
|
||||||
|
it.typeMask and WindowInsetsCompat.Type.ime() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imeAnimation != null) {
|
||||||
|
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
val targetBottomInset = if (imeInsets.bottom > 0) imeInsets.bottom else navBarInsets.bottom
|
||||||
|
|
||||||
|
// Only animate input layout padding
|
||||||
|
binding.layoutChatInput.updatePadding(bottom = targetBottomInset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
||||||
|
super.onEnd(animation)
|
||||||
|
// Smooth scroll to bottom after animation
|
||||||
|
scrollToBottomSmooth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -276,8 +278,14 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
val message = binding.editTextMessage.text.toString().trim()
|
val message = binding.editTextMessage.text.toString().trim()
|
||||||
val currentState = viewModel.state.value
|
val currentState = viewModel.state.value
|
||||||
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
|
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
|
||||||
|
// This will automatically handle product attachment if enabled
|
||||||
viewModel.sendMessage(message)
|
viewModel.sendMessage(message)
|
||||||
binding.editTextMessage.text.clear()
|
binding.editTextMessage.text.clear()
|
||||||
|
|
||||||
|
// Instantly scroll to show new message
|
||||||
|
binding.recyclerChat.postDelayed({
|
||||||
|
scrollToBottomInstant()
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,6 +293,38 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
binding.btnAttachment.setOnClickListener {
|
binding.btnAttachment.setOnClickListener {
|
||||||
checkPermissionsAndShowImagePicker()
|
checkPermissionsAndShowImagePicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Product card click to enable/disable product attachment
|
||||||
|
binding.productContainer.setOnClickListener {
|
||||||
|
toggleProductAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleProductAttachment() {
|
||||||
|
val currentState = viewModel.state.value
|
||||||
|
if (currentState?.hasProductAttachment == true) {
|
||||||
|
// Disable product attachment
|
||||||
|
viewModel.disableProductAttachment()
|
||||||
|
updateProductAttachmentUI(false)
|
||||||
|
Toast.makeText(this, "Product attachment disabled", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
// Enable product attachment
|
||||||
|
viewModel.enableProductAttachment()
|
||||||
|
updateProductAttachmentUI(true)
|
||||||
|
Toast.makeText(this, "Product will be attached to your next message", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProductAttachmentUI(isEnabled: Boolean) {
|
||||||
|
if (isEnabled) {
|
||||||
|
// Show visual indicator that product will be attached
|
||||||
|
binding.productContainer.setBackgroundResource(R.drawable.bg_product_selected)
|
||||||
|
binding.editTextMessage.hint = "Type your message (product will be attached)"
|
||||||
|
} else {
|
||||||
|
// Reset to normal state
|
||||||
|
binding.productContainer.setBackgroundResource(R.drawable.bg_product_normal)
|
||||||
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTypingIndicator() {
|
private fun setupTypingIndicator() {
|
||||||
@ -302,33 +342,64 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
override fun afterTextChanged(s: Editable?) {}
|
override fun afterTextChanged(s: Editable?) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Focus and show keyboard
|
||||||
binding.editTextMessage.requestFocus()
|
binding.editTextMessage.requestFocus()
|
||||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
binding.editTextMessage.post {
|
||||||
imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT)
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToBottomSmooth() {
|
||||||
|
val messageCount = chatAdapter.itemCount
|
||||||
|
if (messageCount > 0) {
|
||||||
|
binding.recyclerChat.post {
|
||||||
|
// Use smooth scroll to bottom
|
||||||
|
binding.recyclerChat.smoothScrollToPosition(messageCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToBottomInstant() {
|
||||||
|
val messageCount = chatAdapter.itemCount
|
||||||
|
if (messageCount > 0) {
|
||||||
|
binding.recyclerChat.post {
|
||||||
|
// Instant scroll for new messages
|
||||||
|
binding.recyclerChat.scrollToPosition(messageCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension function to make padding updates cleaner
|
||||||
|
private fun View.updatePadding(
|
||||||
|
left: Int = paddingLeft,
|
||||||
|
top: Int = paddingTop,
|
||||||
|
right: Int = paddingRight,
|
||||||
|
bottom: Int = paddingBottom
|
||||||
|
) {
|
||||||
|
setPadding(left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
|
viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
|
||||||
if (chatRoomId > 0) {
|
if (chatRoomId > 0) {
|
||||||
// Chat room has been created, now we can join the Socket.IO room
|
|
||||||
viewModel.joinSocketRoom(chatRoomId)
|
viewModel.joinSocketRoom(chatRoomId)
|
||||||
|
|
||||||
// Now we can also load chat history
|
|
||||||
viewModel.loadChatHistory(chatRoomId)
|
viewModel.loadChatHistory(chatRoomId)
|
||||||
Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
|
Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Observe state changes using LiveData
|
|
||||||
viewModel.state.observe(this, Observer { state ->
|
viewModel.state.observe(this, Observer { state ->
|
||||||
// Update messages
|
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
|
||||||
chatAdapter.submitList(state.messages)
|
|
||||||
|
|
||||||
// Scroll to bottom if new message
|
// Update messages
|
||||||
if (state.messages.isNotEmpty()) {
|
val previousCount = chatAdapter.itemCount
|
||||||
binding.recyclerChat.scrollToPosition(state.messages.size - 1)
|
chatAdapter.submitList(state.messages) {
|
||||||
|
Log.d(TAG, "Messages submitted to adapter")
|
||||||
|
// Only auto-scroll for new messages or initial load
|
||||||
|
if (previousCount == 0 || state.messages.size > previousCount) {
|
||||||
|
scrollToBottomInstant()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update product info
|
// Update product info
|
||||||
@ -338,7 +409,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
binding.ratingBar.rating = state.productRating
|
binding.ratingBar.rating = state.productRating
|
||||||
binding.tvRating.text = state.productRating.toString()
|
binding.tvRating.text = state.productRating.toString()
|
||||||
binding.tvSellerName.text = state.storeName
|
binding.tvSellerName.text = state.storeName
|
||||||
binding.tvStoreName.text=state.storeName
|
binding.tvStoreName.text = state.storeName
|
||||||
|
|
||||||
val fullImageUrl = when (val img = state.productImageUrl) {
|
val fullImageUrl = when (val img = state.productImageUrl) {
|
||||||
is String -> {
|
is String -> {
|
||||||
@ -347,7 +418,6 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
else -> R.drawable.placeholder_image
|
else -> R.drawable.placeholder_image
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load product image
|
|
||||||
if (!state.productImageUrl.isNullOrEmpty()) {
|
if (!state.productImageUrl.isNullOrEmpty()) {
|
||||||
Glide.with(this@ChatActivity)
|
Glide.with(this@ChatActivity)
|
||||||
.load(fullImageUrl)
|
.load(fullImageUrl)
|
||||||
@ -357,13 +427,15 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
.into(binding.imgProduct)
|
.into(binding.imgProduct)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the product section is visible
|
|
||||||
binding.productContainer.visibility = View.VISIBLE
|
binding.productContainer.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
// Hide the product section if info is missing
|
|
||||||
binding.productContainer.visibility = View.GONE
|
binding.productContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateInputHint(state)
|
||||||
|
|
||||||
|
// Update product card visual feedback
|
||||||
|
updateProductCardUI(state.hasProductAttachment)
|
||||||
|
|
||||||
// Update attachment hint
|
// Update attachment hint
|
||||||
if (state.hasAttachment) {
|
if (state.hasAttachment) {
|
||||||
@ -372,7 +444,6 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
binding.editTextMessage.hint = getString(R.string.write_message)
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show typing indicator
|
// Show typing indicator
|
||||||
binding.tvTypingIndicator.visibility =
|
binding.tvTypingIndicator.visibility =
|
||||||
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||||
@ -385,6 +456,45 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateInputHint(state: ChatUiState) {
|
||||||
|
binding.editTextMessage.hint = when {
|
||||||
|
state.hasAttachment -> getString(R.string.image_attached)
|
||||||
|
state.hasProductAttachment -> "Type your message (product will be attached)"
|
||||||
|
else -> getString(R.string.write_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProductCardUI(hasProductAttachment: Boolean) {
|
||||||
|
if (hasProductAttachment) {
|
||||||
|
// Show visual indicator that product will be attached
|
||||||
|
binding.productContainer.setBackgroundResource(R.drawable.bg_product_selected)
|
||||||
|
} else {
|
||||||
|
// Reset to normal state
|
||||||
|
binding.productContainer.setBackgroundResource(R.drawable.bg_product_normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleProductClick(productInfo: ProductInfo) {
|
||||||
|
// Navigate to product detail
|
||||||
|
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// You can navigate to product detail here
|
||||||
|
navigateToProductDetail(productInfo.productId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToProductDetail(productId: Int) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(this, DetailProductActivity::class.java).apply {
|
||||||
|
putExtra("PRODUCT_ID", productId)
|
||||||
|
// Add other necessary extras
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, "Cannot open product details", Toast.LENGTH_SHORT).show()
|
||||||
|
Log.e(TAG, "Error navigating to product detail", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showOptionsMenu() {
|
private fun showOptionsMenu() {
|
||||||
val options = arrayOf(
|
val options = arrayOf(
|
||||||
getString(R.string.block_user),
|
getString(R.string.block_user),
|
||||||
@ -538,7 +648,8 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
productRating: String? = null,
|
productRating: String? = null,
|
||||||
storeName: String? = null,
|
storeName: String? = null,
|
||||||
chatRoomId: Int = 0,
|
chatRoomId: Int = 0,
|
||||||
storeImage: String? = null
|
storeImage: String? = null,
|
||||||
|
attachProduct: Boolean = false // NEW: Flag to auto-attach product
|
||||||
) {
|
) {
|
||||||
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_STORE_ID, storeId)
|
||||||
@ -547,6 +658,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
|
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
|
||||||
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
||||||
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
|
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
|
||||||
|
putExtra(Constants.EXTRA_ATTACH_PRODUCT, attachProduct) // NEW
|
||||||
|
|
||||||
// Convert productRating string to float if provided
|
// Convert productRating string to float if provided
|
||||||
if (productRating != null) {
|
if (productRating != null) {
|
||||||
|
@ -8,56 +8,71 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
|
import com.alya.ecommerce_serang.databinding.ItemMessageProductReceivedBinding
|
||||||
|
import com.alya.ecommerce_serang.databinding.ItemMessageProductSentBinding
|
||||||
import com.alya.ecommerce_serang.databinding.ItemMessageReceivedBinding
|
import com.alya.ecommerce_serang.databinding.ItemMessageReceivedBinding
|
||||||
import com.alya.ecommerce_serang.databinding.ItemMessageSentBinding
|
import com.alya.ecommerce_serang.databinding.ItemMessageSentBinding
|
||||||
import com.alya.ecommerce_serang.utils.Constants
|
import com.alya.ecommerce_serang.utils.Constants
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMessageDiffCallback()) {
|
class ChatAdapter(
|
||||||
|
private val onProductClick: ((ProductInfo) -> Unit)? = null
|
||||||
|
) : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMessageDiffCallback()) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_MESSAGE_SENT = 1
|
private const val VIEW_TYPE_MESSAGE_SENT = 1
|
||||||
private const val VIEW_TYPE_MESSAGE_RECEIVED = 2
|
private const val VIEW_TYPE_MESSAGE_RECEIVED = 2
|
||||||
|
private const val VIEW_TYPE_PRODUCT_SENT = 3
|
||||||
|
private const val VIEW_TYPE_PRODUCT_RECEIVED = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
val message = getItem(position)
|
||||||
|
return when {
|
||||||
|
message.messageType == MessageType.PRODUCT && message.isSentByMe -> VIEW_TYPE_PRODUCT_SENT
|
||||||
|
message.messageType == MessageType.PRODUCT && !message.isSentByMe -> VIEW_TYPE_PRODUCT_RECEIVED
|
||||||
|
message.isSentByMe -> VIEW_TYPE_MESSAGE_SENT
|
||||||
|
else -> VIEW_TYPE_MESSAGE_RECEIVED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return if (viewType == VIEW_TYPE_MESSAGE_SENT) {
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
val binding = ItemMessageSentBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
return when (viewType) {
|
||||||
parent,
|
VIEW_TYPE_MESSAGE_SENT -> {
|
||||||
false
|
val binding = ItemMessageSentBinding.inflate(inflater, parent, false)
|
||||||
)
|
SentMessageViewHolder(binding)
|
||||||
SentMessageViewHolder(binding)
|
}
|
||||||
} else {
|
VIEW_TYPE_MESSAGE_RECEIVED -> {
|
||||||
val binding = ItemMessageReceivedBinding.inflate(
|
val binding = ItemMessageReceivedBinding.inflate(inflater, parent, false)
|
||||||
LayoutInflater.from(parent.context),
|
ReceivedMessageViewHolder(binding)
|
||||||
parent,
|
}
|
||||||
false
|
VIEW_TYPE_PRODUCT_SENT -> {
|
||||||
)
|
val binding = ItemMessageProductSentBinding.inflate(inflater, parent, false)
|
||||||
ReceivedMessageViewHolder(binding)
|
SentProductViewHolder(binding)
|
||||||
|
}
|
||||||
|
VIEW_TYPE_PRODUCT_RECEIVED -> {
|
||||||
|
val binding = ItemMessageProductReceivedBinding.inflate(inflater, parent, false)
|
||||||
|
ReceivedProductViewHolder(binding)
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Unknown view type: $viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
when (holder.itemViewType) {
|
when (holder) {
|
||||||
VIEW_TYPE_MESSAGE_SENT -> (holder as SentMessageViewHolder).bind(message)
|
is SentMessageViewHolder -> holder.bind(message)
|
||||||
VIEW_TYPE_MESSAGE_RECEIVED -> (holder as ReceivedMessageViewHolder).bind(message)
|
is ReceivedMessageViewHolder -> holder.bind(message)
|
||||||
}
|
is SentProductViewHolder -> holder.bind(message)
|
||||||
}
|
is ReceivedProductViewHolder -> holder.bind(message)
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
val message = getItem(position)
|
|
||||||
return if (message.isSentByMe) {
|
|
||||||
VIEW_TYPE_MESSAGE_SENT
|
|
||||||
} else {
|
|
||||||
VIEW_TYPE_MESSAGE_RECEIVED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewHolder for messages sent by the current user
|
* ViewHolder for regular messages sent by the current user
|
||||||
*/
|
*/
|
||||||
inner class SentMessageViewHolder(private val binding: ItemMessageSentBinding) :
|
inner class SentMessageViewHolder(private val binding: ItemMessageSentBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
@ -99,7 +114,7 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewHolder for messages received from other users
|
* ViewHolder for regular messages received from other users
|
||||||
*/
|
*/
|
||||||
inner class ReceivedMessageViewHolder(private val binding: ItemMessageReceivedBinding) :
|
inner class ReceivedMessageViewHolder(private val binding: ItemMessageReceivedBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
@ -135,6 +150,89 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMess
|
|||||||
.into(binding.imgAvatar)
|
.into(binding.imgAvatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewHolder for product messages sent by the current user
|
||||||
|
*/
|
||||||
|
inner class SentProductViewHolder(private val binding: ItemMessageProductSentBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(message: ChatUiMessage) {
|
||||||
|
// For product bubble, we don't show the text message here
|
||||||
|
binding.tvTimestamp.text = message.time
|
||||||
|
|
||||||
|
// Show message status
|
||||||
|
val statusIcon = when (message.status) {
|
||||||
|
Constants.STATUS_SENT -> R.drawable.check_single_24
|
||||||
|
Constants.STATUS_DELIVERED -> R.drawable.check_double_24
|
||||||
|
Constants.STATUS_READ -> R.drawable.check_double_read_24
|
||||||
|
else -> R.drawable.check_single_24
|
||||||
|
}
|
||||||
|
binding.imgStatus.setImageResource(statusIcon)
|
||||||
|
|
||||||
|
// Bind product info
|
||||||
|
message.productInfo?.let { product ->
|
||||||
|
binding.tvProductName.text = product.productName
|
||||||
|
binding.tvProductPrice.text = product.productPrice
|
||||||
|
|
||||||
|
// Load product image
|
||||||
|
val fullImageUrl = if (product.productImage.startsWith("/")) {
|
||||||
|
BASE_URL + product.productImage.substring(1)
|
||||||
|
} else {
|
||||||
|
product.productImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(binding.root.context)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.centerCrop()
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.error(R.drawable.placeholder_image)
|
||||||
|
.into(binding.imgProduct)
|
||||||
|
|
||||||
|
// Handle product click
|
||||||
|
binding.layoutProduct.setOnClickListener {
|
||||||
|
onProductClick?.invoke(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewHolder for product messages received from other users
|
||||||
|
*/
|
||||||
|
inner class ReceivedProductViewHolder(private val binding: ItemMessageProductReceivedBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(message: ChatUiMessage) {
|
||||||
|
// For product bubble, we don't show the text message here
|
||||||
|
binding.tvTimestamp.text = message.time
|
||||||
|
|
||||||
|
// Bind product info
|
||||||
|
message.productInfo?.let { product ->
|
||||||
|
binding.tvProductName.text = product.productName
|
||||||
|
binding.tvProductPrice.text = product.productPrice
|
||||||
|
|
||||||
|
// Load product image
|
||||||
|
val fullImageUrl = if (product.productImage.startsWith("/")) {
|
||||||
|
BASE_URL + product.productImage.substring(1)
|
||||||
|
} else {
|
||||||
|
product.productImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(binding.root.context)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.centerCrop()
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.error(R.drawable.placeholder_image)
|
||||||
|
.into(binding.imgProduct)
|
||||||
|
|
||||||
|
// Handle product click
|
||||||
|
binding.layoutProduct.setOnClickListener {
|
||||||
|
onProductClick?.invoke(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -483,8 +483,10 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
productRating = productDetail.rating,
|
productRating = productDetail.rating,
|
||||||
storeName = storeDetail.data.storeName,
|
storeName = storeDetail.data.storeName,
|
||||||
chatRoomId = 0,
|
chatRoomId = 0,
|
||||||
storeImage = storeDetail.data.storeImage
|
storeImage = storeDetail.data.storeImage,
|
||||||
)
|
attachProduct = true // This will auto-attach the product!
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class ChatListStoreActivity : AppCompatActivity() {
|
|||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
Log.d(TAG, "Chat list fetch success. Data size: ${result.data.size}")
|
Log.d(TAG, "Chat list fetch success. Data size: ${result.data.size}")
|
||||||
val adapter = ChatListAdapter(result.data) { chatItem ->
|
val adapter = ChatStoreListAdapter(result.data) { chatItem ->
|
||||||
Log.d(TAG, "Chat item clicked: storeId=${chatItem.storeId}, chatRoomId=${chatItem.chatRoomId}")
|
Log.d(TAG, "Chat item clicked: storeId=${chatItem.storeId}, chatRoomId=${chatItem.chatRoomId}")
|
||||||
val intent = ChatStoreActivity.createIntent(
|
val intent = ChatStoreActivity.createIntent(
|
||||||
context = this,
|
context = this,
|
||||||
@ -93,7 +93,10 @@ class ChatListStoreActivity : AppCompatActivity() {
|
|||||||
storeName = chatItem.storeName,
|
storeName = chatItem.storeName,
|
||||||
chatRoomId = chatItem.chatRoomId,
|
chatRoomId = chatItem.chatRoomId,
|
||||||
storeImage = chatItem.storeImage,
|
storeImage = chatItem.storeImage,
|
||||||
userId = chatItem.userId
|
userId = chatItem.userId,
|
||||||
|
userName = chatItem.userName,
|
||||||
|
userImg = chatItem.userImage
|
||||||
|
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
//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{
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//}
|
|
@ -15,7 +15,6 @@ import android.util.Log
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@ -23,7 +22,6 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
@ -45,7 +43,6 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ChatStoreActivity : AppCompatActivity() {
|
class ChatStoreActivity : AppCompatActivity() {
|
||||||
@ -102,10 +99,10 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
|
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
// WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
enableEdgeToEdge()
|
// enableEdgeToEdge()
|
||||||
|
//
|
||||||
// Apply insets to your root layout
|
// // Apply insets to your root layout
|
||||||
|
|
||||||
|
|
||||||
// Get parameters from intent
|
// Get parameters from intent
|
||||||
@ -119,6 +116,8 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
|
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
|
||||||
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
||||||
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
||||||
|
val userName = intent.getStringExtra(Constants.EXTRA_USER_NAME) ?: ""
|
||||||
|
val userImg = intent.getStringExtra(Constants.EXTRA_USER_IMAGE) ?: ""
|
||||||
|
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
val token = sessionManager.getToken()
|
val token = sessionManager.getToken()
|
||||||
@ -131,8 +130,8 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvStoreName.text = storeName
|
binding.tvStoreName.text = userName
|
||||||
val fullImageUrl = when (val img = storeImg) {
|
val fullImageUrl = when (val img = userImg) {
|
||||||
is String -> {
|
is String -> {
|
||||||
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
}
|
}
|
||||||
@ -144,84 +143,6 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(binding.imgProfile)
|
.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
|
// Set chat parameters to ViewModel
|
||||||
viewModel.setChatParametersStore(
|
viewModel.setChatParametersStore(
|
||||||
storeId = storeId,
|
storeId = storeId,
|
||||||
@ -234,8 +155,10 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
storeName = storeName
|
storeName = storeName
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup UI components
|
|
||||||
|
// Then setup other components
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
setupWindowInsets()
|
||||||
setupListeners()
|
setupListeners()
|
||||||
setupTypingIndicator()
|
setupTypingIndicator()
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
@ -251,18 +174,140 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
chatAdapter = ChatAdapter()
|
chatAdapter = ChatAdapter()
|
||||||
binding.recyclerChat.apply {
|
binding.recyclerChat.apply {
|
||||||
adapter = chatAdapter
|
adapter = chatAdapter
|
||||||
layoutManager = LinearLayoutManager(this@ChatStoreActivity).apply {
|
layoutManager = LinearLayoutManager(this@ChatStoreActivity)
|
||||||
stackFromEnd = true
|
// Use clipToPadding to allow content to scroll under padding
|
||||||
}
|
clipToPadding = false
|
||||||
|
// Set minimal padding - we'll handle spacing differently
|
||||||
|
setPadding(paddingLeft, paddingTop, paddingRight, 16)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupWindowInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.chatToolbar) { view, insets ->
|
||||||
|
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||||
|
view.updatePadding(top = statusBarInsets.top)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IME (keyboard) and navigation bar insets for the input layout only
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutChatInput) { view, insets ->
|
||||||
|
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
|
||||||
|
Log.d(TAG, "Insets - IME: ${imeInsets.bottom}, NavBar: ${navBarInsets.bottom}")
|
||||||
|
|
||||||
|
val bottomInset = if (imeInsets.bottom > 0) {
|
||||||
|
imeInsets.bottom
|
||||||
|
} else {
|
||||||
|
navBarInsets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only apply padding to the input layout
|
||||||
|
view.updatePadding(bottom = bottomInset)
|
||||||
|
|
||||||
|
// When keyboard appears, scroll to bottom to keep last message visible
|
||||||
|
if (imeInsets.bottom > 0) {
|
||||||
|
// Keyboard is visible - scroll to bottom with delay to ensure layout is complete
|
||||||
|
binding.recyclerChat.postDelayed({
|
||||||
|
scrollToBottomSmooth()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth animation for keyboard transitions
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(
|
||||||
|
binding.layoutChatInput,
|
||||||
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
||||||
|
|
||||||
|
override fun onProgress(
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
runningAnimations: MutableList<WindowInsetsAnimationCompat>
|
||||||
|
): WindowInsetsCompat {
|
||||||
|
val imeAnimation = runningAnimations.find {
|
||||||
|
it.typeMask and WindowInsetsCompat.Type.ime() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imeAnimation != null) {
|
||||||
|
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
val targetBottomInset = if (imeInsets.bottom > 0) imeInsets.bottom else navBarInsets.bottom
|
||||||
|
|
||||||
|
// Only animate input layout padding
|
||||||
|
binding.layoutChatInput.updatePadding(bottom = targetBottomInset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
||||||
|
super.onEnd(animation)
|
||||||
|
// Smooth scroll to bottom after animation
|
||||||
|
scrollToBottomSmooth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun updateRecyclerViewPadding(inputLayoutBottomPadding: Int) {
|
||||||
|
// // Calculate total bottom padding needed for RecyclerView
|
||||||
|
// // This ensures the last message is visible above the input layout
|
||||||
|
// val inputLayoutHeight = binding.layoutChatInput.height
|
||||||
|
// val totalBottomPadding = inputLayoutHeight + inputLayoutBottomPadding
|
||||||
|
//
|
||||||
|
// binding.recyclerChat.setPadding(
|
||||||
|
// binding.recyclerChat.paddingLeft,
|
||||||
|
// binding.recyclerChat.paddingTop,
|
||||||
|
// binding.recyclerChat.paddingRight,
|
||||||
|
// totalBottomPadding
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Scroll to bottom if there are messages
|
||||||
|
// val messageCount = chatAdapter.itemCount
|
||||||
|
// if (messageCount > 0) {
|
||||||
|
// binding.recyclerChat.post {
|
||||||
|
// binding.recyclerChat.scrollToPosition(messageCount - 1)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// binding.recyclerChat.setPadding(
|
// binding.recyclerChat.setPadding(
|
||||||
// binding.recyclerChat.paddingLeft,
|
// binding.recyclerChat.paddingLeft,
|
||||||
// binding.recyclerChat.paddingTop,
|
// binding.recyclerChat.paddingTop,
|
||||||
// binding.recyclerChat.paddingRight,
|
// binding.recyclerChat.paddingRight,
|
||||||
// binding.layoutChatInput.height + binding.root.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
// binding.layoutChatInput.height + binding.root.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
private fun scrollToBottomSmooth() {
|
||||||
|
val messageCount = chatAdapter.itemCount
|
||||||
|
if (messageCount > 0) {
|
||||||
|
binding.recyclerChat.post {
|
||||||
|
// Use smooth scroll to bottom
|
||||||
|
binding.recyclerChat.smoothScrollToPosition(messageCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scrollToBottomInstant() {
|
||||||
|
val messageCount = chatAdapter.itemCount
|
||||||
|
if (messageCount > 0) {
|
||||||
|
binding.recyclerChat.post {
|
||||||
|
// Instant scroll for new messages
|
||||||
|
binding.recyclerChat.scrollToPosition(messageCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension function to make padding updates cleaner
|
||||||
|
private fun View.updatePadding(
|
||||||
|
left: Int = paddingLeft,
|
||||||
|
top: Int = paddingTop,
|
||||||
|
right: Int = paddingRight,
|
||||||
|
bottom: Int = paddingBottom
|
||||||
|
) {
|
||||||
|
setPadding(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
// Back button
|
// Back button
|
||||||
@ -282,6 +327,11 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
|
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
|
||||||
viewModel.sendMessageStore(message)
|
viewModel.sendMessageStore(message)
|
||||||
binding.editTextMessage.text.clear()
|
binding.editTextMessage.text.clear()
|
||||||
|
|
||||||
|
// Instantly scroll to show new message
|
||||||
|
binding.recyclerChat.postDelayed({
|
||||||
|
scrollToBottomInstant()
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,33 +356,34 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
override fun afterTextChanged(s: Editable?) {}
|
override fun afterTextChanged(s: Editable?) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Focus and show keyboard
|
||||||
binding.editTextMessage.requestFocus()
|
binding.editTextMessage.requestFocus()
|
||||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
binding.editTextMessage.post {
|
||||||
imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT)
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
|
viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
|
||||||
if (chatRoomId > 0) {
|
if (chatRoomId > 0) {
|
||||||
// Chat room has been created, now we can join the Socket.IO room
|
|
||||||
viewModel.joinSocketRoom(chatRoomId)
|
viewModel.joinSocketRoom(chatRoomId)
|
||||||
|
|
||||||
// Now we can also load chat history
|
|
||||||
viewModel.loadChatHistory(chatRoomId)
|
viewModel.loadChatHistory(chatRoomId)
|
||||||
Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
|
Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Observe state changes using LiveData
|
|
||||||
viewModel.state.observe(this, Observer { state ->
|
viewModel.state.observe(this, Observer { state ->
|
||||||
// Update messages
|
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
|
||||||
chatAdapter.submitList(state.messages)
|
|
||||||
|
|
||||||
// Scroll to bottom if new message
|
// Update messages
|
||||||
if (state.messages.isNotEmpty()) {
|
val previousCount = chatAdapter.itemCount
|
||||||
binding.recyclerChat.scrollToPosition(state.messages.size - 1)
|
chatAdapter.submitList(state.messages) {
|
||||||
|
Log.d(TAG, "Messages submitted to adapter")
|
||||||
|
// Only auto-scroll for new messages or initial load
|
||||||
|
if (previousCount == 0 || state.messages.size > previousCount) {
|
||||||
|
scrollToBottomInstant()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update product info
|
// Update product info
|
||||||
@ -342,7 +393,7 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
binding.ratingBar.rating = state.productRating
|
binding.ratingBar.rating = state.productRating
|
||||||
binding.tvRating.text = state.productRating.toString()
|
binding.tvRating.text = state.productRating.toString()
|
||||||
binding.tvSellerName.text = state.storeName
|
binding.tvSellerName.text = state.storeName
|
||||||
binding.tvStoreName.text=state.storeName
|
// binding.tvStoreName.text = state.storeName
|
||||||
|
|
||||||
val fullImageUrl = when (val img = state.productImageUrl) {
|
val fullImageUrl = when (val img = state.productImageUrl) {
|
||||||
is String -> {
|
is String -> {
|
||||||
@ -351,7 +402,6 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
else -> R.drawable.placeholder_image
|
else -> R.drawable.placeholder_image
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load product image
|
|
||||||
if (!state.productImageUrl.isNullOrEmpty()) {
|
if (!state.productImageUrl.isNullOrEmpty()) {
|
||||||
Glide.with(this@ChatStoreActivity)
|
Glide.with(this@ChatStoreActivity)
|
||||||
.load(fullImageUrl)
|
.load(fullImageUrl)
|
||||||
@ -361,14 +411,11 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
.into(binding.imgProduct)
|
.into(binding.imgProduct)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the product section is visible
|
|
||||||
binding.productContainer.visibility = View.VISIBLE
|
binding.productContainer.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
// Hide the product section if info is missing
|
|
||||||
binding.productContainer.visibility = View.GONE
|
binding.productContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update attachment hint
|
// Update attachment hint
|
||||||
if (state.hasAttachment) {
|
if (state.hasAttachment) {
|
||||||
binding.editTextMessage.hint = getString(R.string.image_attached)
|
binding.editTextMessage.hint = getString(R.string.image_attached)
|
||||||
@ -376,7 +423,6 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
binding.editTextMessage.hint = getString(R.string.write_message)
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show typing indicator
|
// Show typing indicator
|
||||||
binding.tvTypingIndicator.visibility =
|
binding.tvTypingIndicator.visibility =
|
||||||
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||||
@ -543,7 +589,9 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
storeName: String? = null,
|
storeName: String? = null,
|
||||||
chatRoomId: Int = 0,
|
chatRoomId: Int = 0,
|
||||||
storeImage: String? = null,
|
storeImage: String? = null,
|
||||||
userId: Int
|
userId: Int,
|
||||||
|
userName: String,
|
||||||
|
userImg: String? = null
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, ChatStoreActivity::class.java).apply {
|
return Intent(context, ChatStoreActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_STORE_ID, storeId)
|
putExtra(Constants.EXTRA_STORE_ID, storeId)
|
||||||
@ -553,6 +601,8 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
||||||
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
|
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
|
||||||
putExtra(Constants.EXTRA_USER_ID, userId)
|
putExtra(Constants.EXTRA_USER_ID, userId)
|
||||||
|
putExtra(Constants.EXTRA_USER_NAME,userName)
|
||||||
|
putExtra(Constants.EXTRA_USER_IMAGE, userImg)
|
||||||
|
|
||||||
// Convert productRating string to float if provided
|
// Convert productRating string to float if provided
|
||||||
if (productRating != null) {
|
if (productRating != null) {
|
||||||
|
@ -13,20 +13,20 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
class ChatListAdapter(
|
class ChatStoreListAdapter(
|
||||||
private val chatList: List<ChatItemList>,
|
private val chatList: List<ChatItemList>,
|
||||||
private val onClick: (ChatItemList) -> Unit
|
private val onClick: (ChatItemList) -> Unit
|
||||||
) : RecyclerView.Adapter<ChatListAdapter.ChatViewHolder>() {
|
) : RecyclerView.Adapter<ChatStoreListAdapter.ChatViewHolder>() {
|
||||||
|
|
||||||
inner class ChatViewHolder(private val binding: ItemChatBinding) :
|
inner class ChatViewHolder(private val binding: ItemChatBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(chat: ChatItemList) {
|
fun bind(chat: ChatItemList) {
|
||||||
binding.txtStoreName.text = chat.storeName
|
binding.txtStoreName.text = chat.userName
|
||||||
binding.txtMessage.text = chat.message
|
binding.txtMessage.text = chat.message
|
||||||
binding.txtTime.text = formatTime(chat.latestMessageTime)
|
binding.txtTime.text = formatTime(chat.latestMessageTime)
|
||||||
|
|
||||||
// Process image URL properly
|
// Process image URL properly
|
||||||
val imageUrl = chat.storeImage?.let {
|
val imageUrl = chat.userImage?.let {
|
||||||
if (it.startsWith("/")) BASE_URL + it else it
|
if (it.startsWith("/")) BASE_URL + it else it
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,11 @@ object Constants {
|
|||||||
const val EXTRA_PRODUCT_RATING = "product_rating"
|
const val EXTRA_PRODUCT_RATING = "product_rating"
|
||||||
const val EXTRA_STORE_IMAGE = "store_image"
|
const val EXTRA_STORE_IMAGE = "store_image"
|
||||||
const val EXTRA_USER_ID = "user_id"
|
const val EXTRA_USER_ID = "user_id"
|
||||||
|
const val EXTRA_USER_NAME = "user_name"
|
||||||
|
const val EXTRA_USER_IMAGE = "user_image"
|
||||||
|
const val EXTRA_ATTACH_PRODUCT = "extra_attach_product"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Request codes
|
// Request codes
|
||||||
const val REQUEST_IMAGE_PICK = 1001
|
const val REQUEST_IMAGE_PICK = 1001
|
||||||
|
9
app/src/main/res/drawable/bg_product_bubble.xml
Normal file
9
app/src/main/res/drawable/bg_product_bubble.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#F5F5F5" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#E0E0E0" />
|
||||||
|
</shape>
|
9
app/src/main/res/drawable/bg_product_normal.xml
Normal file
9
app/src/main/res/drawable/bg_product_normal.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#E0E0E0" />
|
||||||
|
</shape>
|
9
app/src/main/res/drawable/bg_product_selected.xml
Normal file
9
app/src/main/res/drawable/bg_product_selected.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E3F2FD" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#2196F3" />
|
||||||
|
</shape>
|
87
app/src/main/res/layout/item_message_product_received.xml
Normal file
87
app/src/main/res/layout/item_message_product_received.xml
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="60dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<!-- Product bubble only - no text message -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@drawable/bg_message_received"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:maxWidth="280dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<!-- Product card with horizontal layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutProduct"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="@android:color/transparent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imgProduct"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@drawable/bg_product_inactive"
|
||||||
|
android:src="@drawable/placeholder_image" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProductName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Claude AI Pro - Solusi AI Canggih 1 Bulan"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProductPrice"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Rp25.000"
|
||||||
|
android:textColor="#E91E63"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTimestamp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:textSize="10sp"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/layoutMessage"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/layoutMessage"
|
||||||
|
tools:text="15:36" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
95
app/src/main/res/layout/item_message_product_sent.xml
Normal file
95
app/src/main/res/layout/item_message_product_sent.xml
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="60dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<!-- Product bubble only - no text message -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_message_sent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:maxWidth="280dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<!-- Product card with horizontal layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutProduct"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="@android:color/transparent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imgProduct"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@drawable/bg_product_inactive"
|
||||||
|
android:src="@drawable/placeholder_image" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProductName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Claude AI Pro - Solusi AI Canggih 1 Bulan"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProductPrice"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Rp25.000"
|
||||||
|
android:textColor="#E91E63"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTimestamp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:textSize="10sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/imgStatus"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/layoutMessage"
|
||||||
|
tools:text="15:35" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imgStatus"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:src="@drawable/check_double_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/tvTimestamp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/layoutMessage"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/tvTimestamp" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Reference in New Issue
Block a user