fix chat activity

This commit is contained in:
shaulascr
2025-05-02 07:13:17 +07:00
parent adeb0537f3
commit 3a06f65e96
23 changed files with 747 additions and 592 deletions

View File

@ -220,7 +220,7 @@ interface ApiService {
@Part("store_id") storeId: RequestBody, @Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody, @Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody, @Part("product_id") productId: RequestBody,
@Part("chatimg") chatimg: MultipartBody.Part @Part chatimg: MultipartBody.Part?
): Response<SendChatResponse> ): Response<SendChatResponse>
@PUT("chatstatus") @PUT("chatstatus")

View File

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.di package com.alya.ecommerce_serang.di
import android.content.Context
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.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.ui.chat.SocketIOService import com.alya.ecommerce_serang.ui.chat.SocketIOService
@ -8,7 +7,6 @@ import com.alya.ecommerce_serang.utils.SessionManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
@ -16,12 +14,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object ChatModule { object ChatModule {
@Provides
@Singleton
fun provideSessionManager(@ApplicationContext context: Context): SessionManager {
return SessionManager(context)
}
@Provides @Provides
@Singleton @Singleton
fun provideChatRepository(apiService: ApiService): UserRepository { fun provideChatRepository(apiService: ApiService): UserRepository {

View File

@ -10,7 +10,6 @@ import androidx.core.app.NotificationManagerCompat
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig 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.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -41,12 +40,6 @@ object NotificationModule {
return ApiConfig.getApiService(sessionManager) return ApiConfig.getApiService(sessionManager)
} }
@Provides
@Singleton
fun provideUserRepository(apiService: ApiService): UserRepository {
return UserRepository(apiService)
}
@Singleton @Singleton
@Provides @Provides
fun provideNotificationBuilder( fun provideNotificationBuilder(

View File

@ -58,6 +58,7 @@ class LoginActivity : AppCompatActivity() {
val sessionManager = SessionManager(this) val sessionManager = SessionManager(this)
sessionManager.saveToken(accessToken) sessionManager.saveToken(accessToken)
// sessionManager.saveUserId(response.userId)
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show()

View File

@ -37,12 +37,27 @@ class RegisterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
if (!sessionManager.getToken().isNullOrEmpty()) { Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'")
// User already logged in, redirect to MainActivity Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'")
startActivity(Intent(this, MainActivity::class.java))
finish() try {
// Use the new isLoggedIn method
if (sessionManager.isLoggedIn()) {
Log.d("RegisterActivity", "User logged in, redirecting to MainActivity")
startActivity(Intent(this, MainActivity::class.java))
finish()
return
} else {
Log.d("RegisterActivity", "User not logged in, showing RegisterActivity")
}
} catch (e: Exception) {
// Handle any exceptions
Log.e("RegisterActivity", "Error checking login status: ${e.message}", e)
// Clear potentially corrupt data
sessionManager.clearAll()
} }
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -61,8 +76,7 @@ class RegisterActivity : AppCompatActivity() {
windowInsets windowInsets
} }
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
// Observe OTP state // Observe OTP state
observeOtpState() observeOtpState()

View File

@ -1,6 +1,8 @@
package com.alya.ecommerce_serang.ui.chat package com.alya.ecommerce_serang.ui.chat
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@ -19,24 +21,21 @@ 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.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
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.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
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.ProductUserViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
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
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -47,27 +46,18 @@ class ChatActivity : AppCompatActivity() {
@Inject @Inject
lateinit var sessionManager: SessionManager lateinit var sessionManager: SessionManager
private lateinit var socketService: SocketIOService
@Inject
private lateinit var chatAdapter: ChatAdapter private lateinit var chatAdapter: ChatAdapter
private val viewModel: ChatViewModel by viewModels { private val viewModel: ChatViewModel by viewModels()
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ChatViewModel(userRepository, socketService, sessionManager)
}
}
// For image attachment // For image attachment
private var tempImageUri: Uri? = null private var tempImageUri: Uri? = null
// Chat parameters from intent // // Chat parameters from intent
private var chatRoomId: Int = 0 // private var chatRoomId: Int = 0
private var storeId: Int = 0 // private var storeId: Int = 0
private var productId: Int = 0 // private var productId: Int = 0
// Typing indicator handler // Typing indicator handler
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
@ -101,16 +91,40 @@ class ChatActivity : AppCompatActivity() {
binding = ActivityChatBinding.inflate(layoutInflater) binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
sessionManager = SessionManager(this)
Log.d("ChatActivity", "Token in storage: '${sessionManager.getToken()}'")
Log.d("ChatActivity", "User ID in storage: '${sessionManager.getUserId()}'")
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
// Get parameters from intent // Get parameters from intent
chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0) val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0)
storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0) val productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0)
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) ?: ""
// Check if user is logged in // Check if user is logged in
val userId = sessionManager.getUserId() val userId = sessionManager.getUserId()
val token = sessionManager.getToken() val token = sessionManager.getToken()
if (userId.isNullOrEmpty() || token.isNullOrEmpty()) { if (token.isEmpty()) {
// User not logged in, redirect to login // User not logged in, redirect to login
Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, LoginActivity::class.java)) startActivity(Intent(this, LoginActivity::class.java))
@ -118,30 +132,23 @@ class ChatActivity : AppCompatActivity() {
return return
} }
Log.d(TAG, "Chat Activity started - User ID: $userId, Chat Room: $chatRoomId") // Set chat parameters to ViewModel
viewModel.setChatParameters(
// Initialize ViewModel storeId = storeId,
initViewModel() productId = productId,
productName = productName,
productPrice = productPrice,
productImage = productImage,
productRating = productRating,
storeName = storeName
)
// Setup UI components // Setup UI components
setupRecyclerView() setupRecyclerView()
setupListeners() setupListeners()
setupTypingIndicator() setupTypingIndicator()
observeViewModel() observeViewModel()
}
private fun initViewModel() {
// Set chat parameters to ViewModel
viewModel.setChatParameters(
chatRoomId = chatRoomId,
storeId = storeId,
productId = productId,
productName = intent.getStringExtra(Constants.EXTRA_PRODUCT_NAME) ?: "",
productPrice = intent.getStringExtra(Constants.EXTRA_PRODUCT_PRICE) ?: "",
productImage = intent.getStringExtra(Constants.EXTRA_PRODUCT_IMAGE) ?: "",
productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f),
storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
)
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -154,6 +161,7 @@ class ChatActivity : AppCompatActivity() {
} }
} }
private fun setupListeners() { private fun setupListeners() {
// Back button // Back button
binding.btnBack.setOnClickListener { binding.btnBack.setOnClickListener {
@ -168,7 +176,8 @@ class ChatActivity : AppCompatActivity() {
// Send button // Send button
binding.btnSend.setOnClickListener { binding.btnSend.setOnClickListener {
val message = binding.editTextMessage.text.toString().trim() val message = binding.editTextMessage.text.toString().trim()
if (message.isNotEmpty() || viewModel.state.value?.hasAttachment ?: false) { val currentState = viewModel.state.value
if (message.isNotEmpty() || (currentState != null && currentState.hasAttachment)) {
viewModel.sendMessage(message) viewModel.sendMessage(message)
binding.editTextMessage.text.clear() binding.editTextMessage.text.clear()
} }
@ -197,79 +206,64 @@ class ChatActivity : AppCompatActivity() {
} }
private fun observeViewModel() { private fun observeViewModel() {
lifecycleScope.launch { viewModel.chatRoomId.observe(this, Observer { chatRoomId ->
viewModel.state.collectLatest { state -> if (chatRoomId > 0) {
// Update messages // Chat room has been created, now we can join the Socket.IO room
chatAdapter.submitList(state.messages) viewModel.joinSocketRoom(chatRoomId)
// Scroll to bottom if new message // Now we can also load chat history
if (state.messages.isNotEmpty()) { viewModel.loadChatHistory(chatRoomId)
binding.recyclerChat.scrollToPosition(state.messages.size - 1) Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
}
// Update product info
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
// Load product image
if (state.productImageUrl.isNotEmpty()) {
Glide.with(this@ChatActivity)
.load(BASE_URL + state.productImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
// Show/hide loading indicators
// binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
binding.btnSend.isEnabled = !state.isSending
// 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
// Handle connection state
handleConnectionState(state.connectionState)
// Show error if any
state.error?.let { error ->
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
} }
} })
// 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
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
// Load product image
if (state.productImageUrl.isNotEmpty()) {
Glide.with(this@ChatActivity)
.load(BASE_URL + state.productImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
// 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@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
})
} }
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)
}
}
}
private fun showOptionsMenu() { private fun showOptionsMenu() {
val options = arrayOf( val options = arrayOf(
@ -388,5 +382,56 @@ class ChatActivity : AppCompatActivity() {
companion object { companion object {
private const val TAG = "ChatActivity" private const val TAG = "ChatActivity"
/**
* Create an intent to start the ChatActivity
*/
fun createIntent(
context: Activity,
storeId: Int,
productId: Int,
productName: String?,
productPrice: String,
productImage: String?,
productRating: String?,
storeName: String?,
chatRoomId: Int = 0
){
val intent = Intent(context, ChatActivity::class.java).apply {
putExtra(Constants.EXTRA_STORE_ID, storeId)
putExtra(Constants.EXTRA_PRODUCT_ID, productId)
putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
putExtra(Constants.EXTRA_PRODUCT_RATING, productRating)
putExtra(Constants.EXTRA_STORE_NAME, storeName)
if (chatRoomId > 0) {
putExtra(Constants.EXTRA_CHAT_ROOM_ID, chatRoomId)
}
}
context.startActivity(intent)
}
} }
} }
//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)
// }
// }
// }

View File

@ -10,9 +10,10 @@ 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.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.bumptech.glide.Glide import com.bumptech.glide.Glide
class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatDiffCallback()) { class ChatAdapter : 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
@ -67,10 +68,10 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatDiff
// Show message status // Show message status
val statusIcon = when (message.status) { val statusIcon = when (message.status) {
Constants.STATUS_SENT -> R.drawable.ic_check Constants.STATUS_SENT -> R.drawable.check_single_24
Constants.STATUS_DELIVERED -> R.drawable.ic_double_check Constants.STATUS_DELIVERED -> R.drawable.check_double_24
Constants.STATUS_READ -> R.drawable.ic_double_check_read Constants.STATUS_READ -> R.drawable.check_double_read_24
else -> R.drawable.ic_check else -> R.drawable.check_single_24
} }
binding.imgStatus.setImageResource(statusIcon) binding.imgStatus.setImageResource(statusIcon)
@ -114,7 +115,7 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatDiff
// Load avatar image // Load avatar image
Glide.with(binding.root.context) Glide.with(binding.root.context)
.load(R.drawable.ic_person) // Replace with actual avatar URL if available .load(R.drawable.placeholder_image) // Replace with actual avatar URL if available
.circleCrop() .circleCrop()
.into(binding.imgAvatar) .into(binding.imgAvatar)
} }
@ -122,9 +123,9 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatDiff
} }
/** /**
* DiffCallback for optimizing RecyclerView updates * DiffUtil callback for optimizing RecyclerView updates
*/ */
class ChatDiffCallback : DiffUtil.ItemCallback<ChatUiMessage>() { class ChatMessageDiffCallback : DiffUtil.ItemCallback<ChatUiMessage>() {
override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean { override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }

View File

@ -1,343 +1,337 @@
package com.alya.ecommerce_serang.ui.chat //package com.alya.ecommerce_serang.ui.chat
//
import android.Manifest //import android.Manifest
import android.app.Activity //import android.app.Activity
import android.content.Intent //import android.content.Intent
import android.content.pm.PackageManager //import android.content.pm.PackageManager
import android.net.Uri //import android.net.Uri
import android.os.Bundle //import android.os.Bundle
import android.provider.MediaStore //import android.provider.MediaStore
import android.text.Editable //import android.text.Editable
import android.text.TextWatcher //import android.text.TextWatcher
import androidx.fragment.app.Fragment //import androidx.fragment.app.Fragment
import android.view.LayoutInflater //import android.view.LayoutInflater
import android.view.View //import android.view.View
import android.view.ViewGroup //import android.view.ViewGroup
import android.widget.Toast //import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts //import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat //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.fragment.app.viewModels //import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope //import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs //import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager //import androidx.recyclerview.widget.LinearLayoutManager
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.FragmentChatBinding //import com.alya.ecommerce_serang.databinding.FragmentChatBinding
import com.alya.ecommerce_serang.utils.Constants //import com.alya.ecommerce_serang.utils.Constants
import com.bumptech.glide.Glide //import com.bumptech.glide.Glide
import dagger.hilt.android.AndroidEntryPoint //import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
import java.io.File //import java.io.File
import java.text.SimpleDateFormat //import java.text.SimpleDateFormat
import java.util.Locale //import java.util.Locale
//
//@AndroidEntryPoint
/** //class ChatFragment : Fragment() {
* A simple [Fragment] subclass. //
* Use the [ChatFragment.newInstance] factory method to // private var _binding: FragmentChatBinding? = null
* create an instance of this fragment. // private val binding get() = _binding!!
*/ //
@AndroidEntryPoint // private val viewModel: ChatViewModel by viewModels()
class ChatFragment : Fragment() { //// private val args: ChatFragmentArgs by navArgs()
//
private var _binding: FragmentChatBinding? = null // private lateinit var chatAdapter: ChatAdapter
private val binding get() = _binding!! //
// // For image attachment
private val viewModel: ChatViewModel by viewModels() // private var tempImageUri: Uri? = null
private val args: ChatFragmentArgs by navArgs() //
// // Typing indicator handler
private lateinit var chatAdapter: ChatAdapter // private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
// private val stopTypingRunnable = Runnable {
// For image attachment // viewModel.sendTypingStatus(false)
private var tempImageUri: Uri? = null // }
//
// Typing indicator handler // // Activity Result Launchers
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) // private val pickImageLauncher = registerForActivityResult(
private val stopTypingRunnable = Runnable { // ActivityResultContracts.StartActivityForResult()
viewModel.sendTypingStatus(false) // ) { result ->
} // if (result.resultCode == Activity.RESULT_OK) {
// result.data?.data?.let { uri ->
// Activity Result Launchers // handleSelectedImage(uri)
private val pickImageLauncher = registerForActivityResult( // }
ActivityResultContracts.StartActivityForResult() // }
) { result -> // }
if (result.resultCode == Activity.RESULT_OK) { //
result.data?.data?.let { uri -> // private val takePictureLauncher = registerForActivityResult(
handleSelectedImage(uri) // ActivityResultContracts.StartActivityForResult()
} // ) { result ->
} // if (result.resultCode == Activity.RESULT_OK) {
} // tempImageUri?.let { uri ->
// handleSelectedImage(uri)
private val takePictureLauncher = registerForActivityResult( // }
ActivityResultContracts.StartActivityForResult() // }
) { result -> // }
if (result.resultCode == Activity.RESULT_OK) { //
tempImageUri?.let { uri -> // override fun onCreateView(
handleSelectedImage(uri) // inflater: LayoutInflater,
} // container: ViewGroup?,
} // savedInstanceState: Bundle?
} // ): View {
// _binding = FragmentChatBinding.inflate(inflater, container, false)
override fun onCreateView( // return binding.root
inflater: LayoutInflater, // }
container: ViewGroup?, //
savedInstanceState: Bundle? // override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
): View { // super.onViewCreated(view, savedInstanceState)
_binding = FragmentChatBinding.inflate(inflater, container, false) //
return binding.root // setupRecyclerView()
} // setupListeners()
// setupTypingIndicator()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // observeViewModel()
super.onViewCreated(view, savedInstanceState) // }
//
setupRecyclerView() // private fun setupRecyclerView() {
setupListeners() // chatAdapter = ChatAdapter()
setupTypingIndicator() // binding.recyclerChat.apply {
observeViewModel() // adapter = chatAdapter
} // layoutManager = LinearLayoutManager(requireContext()).apply {
// stackFromEnd = true
private fun setupRecyclerView() { // }
chatAdapter = ChatAdapter() // }
binding.recyclerChat.apply { // }
adapter = chatAdapter //
layoutManager = LinearLayoutManager(requireContext()).apply { // private fun setupListeners() {
stackFromEnd = true // // Back button
} // binding.btnBack.setOnClickListener {
} // requireActivity().onBackPressed()
} // }
//
private fun setupListeners() { // // Options button
// Back button // binding.btnOptions.setOnClickListener {
binding.btnBack.setOnClickListener { // showOptionsMenu()
requireActivity().onBackPressed() // }
} //
// // Send button
// Options button // binding.btnSend.setOnClickListener {
binding.btnOptions.setOnClickListener { // val message = binding.editTextMessage.text.toString().trim()
showOptionsMenu() // if (message.isNotEmpty() || viewModel.state.value.hasAttachment) {
} // viewModel.sendMessage(message)
// binding.editTextMessage.text.clear()
// Send button // }
binding.btnSend.setOnClickListener { // }
val message = binding.editTextMessage.text.toString().trim() //
if (message.isNotEmpty() || viewModel.state.value.hasAttachment) { // // Attachment button
viewModel.sendMessage(message) // binding.btnAttachment.setOnClickListener {
binding.editTextMessage.text.clear() // checkPermissionsAndShowImagePicker()
} // }
} // }
//
// Attachment button // private fun setupTypingIndicator() {
binding.btnAttachment.setOnClickListener { // binding.editTextMessage.addTextChangedListener(object : TextWatcher {
checkPermissionsAndShowImagePicker() // 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)
private fun setupTypingIndicator() { //
binding.editTextMessage.addTextChangedListener(object : TextWatcher { // // Reset the timer
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} // typingHandler.removeCallbacks(stopTypingRunnable)
// typingHandler.postDelayed(stopTypingRunnable, 1000)
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { // }
viewModel.sendTypingStatus(true) //
// override fun afterTextChanged(s: Editable?) {}
// Reset the timer // })
typingHandler.removeCallbacks(stopTypingRunnable) // }
typingHandler.postDelayed(stopTypingRunnable, 1000) //
} // private fun observeViewModel() {
// viewLifecycleOwner.lifecycleScope.launch {
override fun afterTextChanged(s: Editable?) {} // viewModel.state.collectLatest { state ->
}) // // Update messages
} // chatAdapter.submitList(state.messages)
//
private fun observeViewModel() { // // Scroll to bottom if new message
viewLifecycleOwner.lifecycleScope.launch { // if (state.messages.isNotEmpty()) {
viewModel.state.collectLatest { state -> // binding.recyclerChat.scrollToPosition(state.messages.size - 1)
// Update messages // }
chatAdapter.submitList(state.messages) //
// // Update product info
// Scroll to bottom if new message // binding.tvProductName.text = state.productName
if (state.messages.isNotEmpty()) { // binding.tvProductPrice.text = state.productPrice
binding.recyclerChat.scrollToPosition(state.messages.size - 1) // binding.ratingBar.rating = state.productRating
} // binding.tvRating.text = state.productRating.toString()
// binding.tvSellerName.text = state.storeName
// Update product info //
binding.tvProductName.text = state.productName // // Load product image
binding.tvProductPrice.text = state.productPrice // if (state.productImageUrl.isNotEmpty()) {
binding.ratingBar.rating = state.productRating // Glide.with(requireContext())
binding.tvRating.text = state.productRating.toString() // .load(BASE_URL + state.productImageUrl)
binding.tvSellerName.text = state.storeName // .centerCrop()
// .placeholder(R.drawable.placeholder_image)
// Load product image // .error(R.drawable.placeholder_image)
if (state.productImageUrl.isNotEmpty()) { // .into(binding.imgProduct)
Glide.with(requireContext()) // }
.load(BASE_URL + state.productImageUrl) //
.centerCrop() // // Show/hide loading indicators
.placeholder(R.drawable.placeholder_image) // binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
.error(R.drawable.placeholder_image) // binding.btnSend.isEnabled = !state.isSending
.into(binding.imgProduct) //
} // // Update attachment hint
// if (state.hasAttachment) {
// Show/hide loading indicators // binding.editTextMessage.hint = getString(R.string.image_attached)
binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE // } else {
binding.btnSend.isEnabled = !state.isSending // binding.editTextMessage.hint = getString(R.string.write_message)
// }
// Update attachment hint //
if (state.hasAttachment) { // // Show typing indicator
binding.editTextMessage.hint = getString(R.string.image_attached) // binding.tvTypingIndicator.visibility =
} else { // if (state.isOtherUserTyping) View.VISIBLE else View.GONE
binding.editTextMessage.hint = getString(R.string.write_message) //
} // // Handle connection state
// handleConnectionState(state.connectionState)
// Show typing indicator //
binding.tvTypingIndicator.visibility = // // Show error if any
if (state.isOtherUserTyping) View.VISIBLE else View.GONE // state.error?.let { error ->
// Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
// Handle connection state // viewModel.clearError()
handleConnectionState(state.connectionState) // }
// }
// Show error if any // }
state.error?.let { error -> // }
Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show() //
viewModel.clearError() // private fun handleConnectionState(state: ConnectionState) {
} // when (state) {
} // is ConnectionState.Connected -> {
} // binding.tvConnectionStatus.visibility = View.GONE
} // }
// is ConnectionState.Connecting -> {
private fun handleConnectionState(state: ConnectionState) { // binding.tvConnectionStatus.visibility = View.VISIBLE
when (state) { // binding.tvConnectionStatus.text = getString(R.string.connecting)
is ConnectionState.Connected -> { // }
binding.tvConnectionStatus.visibility = View.GONE // is ConnectionState.Disconnected -> {
} // binding.tvConnectionStatus.visibility = View.VISIBLE
is ConnectionState.Connecting -> { // binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
binding.tvConnectionStatus.visibility = View.VISIBLE // }
binding.tvConnectionStatus.text = getString(R.string.connecting) // is ConnectionState.Error -> {
} // binding.tvConnectionStatus.visibility = View.VISIBLE
is ConnectionState.Disconnected -> { // binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
binding.tvConnectionStatus.visibility = View.VISIBLE // }
binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting) // }
} // }
is ConnectionState.Error -> { //
binding.tvConnectionStatus.visibility = View.VISIBLE // private fun showOptionsMenu() {
binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message) // val options = arrayOf(
} // getString(R.string.block_user),
} // getString(R.string.report),
} // getString(R.string.clear_chat),
// getString(R.string.cancel)
private fun showOptionsMenu() { // )
val options = arrayOf( //
getString(R.string.block_user), // androidx.appcompat.app.AlertDialog.Builder(requireContext())
getString(R.string.report), // .setTitle(getString(R.string.options))
getString(R.string.clear_chat), // .setItems(options) { dialog, which ->
getString(R.string.cancel) // when (which) {
) // 0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show()
// 1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show()
androidx.appcompat.app.AlertDialog.Builder(requireContext()) // 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
.setTitle(getString(R.string.options)) // }
.setItems(options) { dialog, which -> // dialog.dismiss()
when (which) { // }
0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show() // .show()
1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show() // }
2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show() //
} // private fun checkPermissionsAndShowImagePicker() {
dialog.dismiss() // if (ContextCompat.checkSelfPermission(
} // requireContext(),
.show() // Manifest.permission.READ_EXTERNAL_STORAGE
} // ) != PackageManager.PERMISSION_GRANTED
// ) {
private fun checkPermissionsAndShowImagePicker() { // ActivityCompat.requestPermissions(
if (ContextCompat.checkSelfPermission( // requireActivity(),
requireContext(), // arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA),
Manifest.permission.READ_EXTERNAL_STORAGE // Constants.REQUEST_STORAGE_PERMISSION
) != PackageManager.PERMISSION_GRANTED // )
) { // } else {
ActivityCompat.requestPermissions( // showImagePickerOptions()
requireActivity(), // }
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA), // }
Constants.REQUEST_STORAGE_PERMISSION //
) // private fun showImagePickerOptions() {
} else { // val options = arrayOf(
showImagePickerOptions() // getString(R.string.take_photo),
} // getString(R.string.choose_from_gallery),
} // getString(R.string.cancel)
// )
private fun showImagePickerOptions() { //
val options = arrayOf( // androidx.appcompat.app.AlertDialog.Builder(requireContext())
getString(R.string.take_photo), // .setTitle(getString(R.string.select_attachment))
getString(R.string.choose_from_gallery), // .setItems(options) { dialog, which ->
getString(R.string.cancel) // when (which) {
) // 0 -> openCamera()
// 1 -> openGallery()
androidx.appcompat.app.AlertDialog.Builder(requireContext()) // }
.setTitle(getString(R.string.select_attachment)) // dialog.dismiss()
.setItems(options) { dialog, which -> // }
when (which) { // .show()
0 -> openCamera() // }
1 -> openGallery() //
} // private fun openCamera() {
dialog.dismiss() // val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
} // val imageFileName = "IMG_${timeStamp}.jpg"
.show() // val storageDir = requireContext().getExternalFilesDir(null)
} // val imageFile = File(storageDir, imageFileName)
//
private fun openCamera() { // tempImageUri = FileProvider.getUriForFile(
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) // requireContext(),
val imageFileName = "IMG_${timeStamp}.jpg" // "${requireContext().packageName}.fileprovider",
val storageDir = requireContext().getExternalFilesDir(null) // imageFile
val imageFile = File(storageDir, imageFileName) // )
//
tempImageUri = FileProvider.getUriForFile( // val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
requireContext(), // putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri)
"${requireContext().packageName}.fileprovider", // }
imageFile //
) // takePictureLauncher.launch(intent)
// }
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { //
putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri) // private fun openGallery() {
} // val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
// pickImageLauncher.launch(intent)
takePictureLauncher.launch(intent) // }
} //
// private fun handleSelectedImage(uri: Uri) {
private fun openGallery() { // // Get the file from Uri
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) // val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
pickImageLauncher.launch(intent) // val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null)
} // cursor?.moveToFirst()
// val columnIndex = cursor?.getColumnIndex(filePathColumn[0])
private fun handleSelectedImage(uri: Uri) { // val filePath = cursor?.getString(columnIndex ?: 0)
// Get the file from Uri // cursor?.close()
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) //
val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null) // if (filePath != null) {
cursor?.moveToFirst() // viewModel.setSelectedImageFile(File(filePath))
val columnIndex = cursor?.getColumnIndex(filePathColumn[0]) // Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show()
val filePath = cursor?.getString(columnIndex ?: 0) // }
cursor?.close() // }
//
if (filePath != null) { // override fun onRequestPermissionsResult(
viewModel.setSelectedImageFile(File(filePath)) // requestCode: Int,
Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show() // permissions: Array<out String>,
} // grantResults: IntArray
} // ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
override fun onRequestPermissionsResult( // if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
requestCode: Int, // if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
permissions: Array<out String>, // showImagePickerOptions()
grantResults: IntArray // } else {
) { // Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
super.onRequestPermissionsResult(requestCode, permissions, grantResults) // }
if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) { // }
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // }
showImagePickerOptions() //
} else { // override fun onDestroyView() {
Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show() // super.onDestroyView()
} // typingHandler.removeCallbacks(stopTypingRunnable)
} // _binding = null
} // }
//}
override fun onDestroyView() {
super.onDestroyView()
typingHandler.removeCallbacks(stopTypingRunnable)
_binding = null
}
}

View File

@ -1,32 +1,56 @@
package com.alya.ecommerce_serang.ui.chat package com.alya.ecommerce_serang.ui.chat
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.utils.viewmodel.ChatViewModel import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentChatListBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class ChatListFragment : Fragment() { class ChatListFragment : Fragment() {
companion object { private var _binding: FragmentChatListBinding? = null
fun newInstance() = ChatListFragment()
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 userRepository = UserRepository(apiService)
ChatViewModel(userRepository, socketService, sessionManager)
}
} }
private val viewModel: ChatViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
// TODO: Use the ViewModel
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
return inflater.inflate(R.layout.fragment_chat_list, container, false) _binding = FragmentChatListBinding.inflate(inflater, container, false)
return _binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
}
private fun setupView(){
binding.btnTrial.setOnClickListener{
val intent = Intent(requireContext(), ChatActivity::class.java)
startActivity(intent)
}
} }
} }

View File

@ -11,12 +11,14 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel
class ChatViewModel @Inject constructor( class ChatViewModel @Inject constructor(
private val chatRepository: UserRepository, private val chatRepository: UserRepository,
private val socketService: SocketIOService, private val socketService: SocketIOService,
@ -29,11 +31,15 @@ class ChatViewModel @Inject constructor(
private val _state = MutableLiveData(ChatUiState()) private val _state = MutableLiveData(ChatUiState())
val state: LiveData<ChatUiState> = _state val state: LiveData<ChatUiState> = _state
// Chat parameters private val _chatRoomId = MutableLiveData<Int>(0)
private var chatRoomId: Int = 0 val chatRoomId: LiveData<Int> = _chatRoomId
// Store and product parameters
private var storeId: Int = 0 private var storeId: Int = 0
private var productId: Int = 0 private var productId: Int = 0
private var currentUserId: Int = 0 private var currentUserId: Int? = 0
private var defaultUserId: Int = 0
// Product details for display // Product details for display
private var productName: String = "" private var productName: String = ""
@ -47,14 +53,29 @@ class ChatViewModel @Inject constructor(
init { init {
// Try to get current user ID from SessionManager // Try to get current user ID from SessionManager
currentUserId = sessionManager.getUserId()?.toIntOrNull() ?: 0 viewModelScope.launch {
when (val result = chatRepository.fetchUserProfile()) {
is Result.Success -> {
currentUserId = result.data?.userId
Log.e(TAG, "User ID: $currentUserId")
if (currentUserId == 0) { // Move the validation and subsequent logic inside the coroutine
Log.e(TAG, "Error: User ID is not set or invalid") if (currentUserId == 0) {
updateState { it.copy(error = "User authentication error. Please login again.") } Log.e(TAG, "Error: User ID is not set or invalid")
} else { updateState { it.copy(error = "User authentication error. Please login again.") }
// Set up socket listeners } else {
setupSocketListeners() // Set up socket listeners
setupSocketListeners()
}
}
is Result.Error -> {
Log.e(TAG, "Error fetching user profile: ${result.exception.message}")
updateState { it.copy(error = "User authentication error. Please login again.") }
}
is Result.Loading -> {
// Handle loading state if needed
}
}
} }
} }
@ -62,7 +83,6 @@ class ChatViewModel @Inject constructor(
* Set chat parameters received from activity * Set chat parameters received from activity
*/ */
fun setChatParameters( fun setChatParameters(
chatRoomId: Int,
storeId: Int, storeId: Int,
productId: Int, productId: Int,
productName: String, productName: String,
@ -71,7 +91,6 @@ class ChatViewModel @Inject constructor(
productRating: Float, productRating: Float,
storeName: String storeName: String
) { ) {
this.chatRoomId = chatRoomId
this.storeId = storeId this.storeId = storeId
this.productId = productId this.productId = productId
this.productName = productName this.productName = productName
@ -92,8 +111,23 @@ class ChatViewModel @Inject constructor(
} }
// Connect to socket and load chat history // Connect to socket and load chat history
socketService.connect() val existingChatRoomId = _chatRoomId.value ?: 0
loadChatHistory() if (existingChatRoomId > 0) {
// If we already have a chat room ID, we can load the chat history
loadChatHistory(existingChatRoomId)
// And join the Socket.IO room
joinSocketRoom(existingChatRoomId)
}
}
fun joinSocketRoom(roomId: Int) {
if (roomId <= 0) {
Log.e(TAG, "Cannot join room: Invalid room ID")
return
}
socketService.joinRoom()
} }
/** /**
@ -134,7 +168,7 @@ class ChatViewModel @Inject constructor(
// Listen for typing status updates // Listen for typing status updates
socketService.typingStatus.collect { typingStatus -> socketService.typingStatus.collect { typingStatus ->
typingStatus?.let { typingStatus?.let {
if (typingStatus.roomId == chatRoomId && typingStatus.userId != currentUserId) { if (typingStatus.roomId == (_chatRoomId.value ?: 0) && typingStatus.userId != currentUserId) {
updateState { it.copy(isOtherUserTyping = typingStatus.isTyping) } updateState { it.copy(isOtherUserTyping = typingStatus.isTyping) }
} }
} }
@ -154,8 +188,8 @@ class ChatViewModel @Inject constructor(
/** /**
* Loads chat history * Loads chat history
*/ */
fun loadChatHistory() { fun loadChatHistory(chatRoomId : Int) {
if (chatRoomId == 0) { if (chatRoomId <= 0) {
Log.e(TAG, "Cannot load chat history: Chat room ID is 0") Log.e(TAG, "Cannot load chat history: Chat room ID is 0")
return return
} }
@ -242,6 +276,17 @@ class ChatViewModel @Inject constructor(
Log.d(TAG, "Message sent successfully: ${chatLine.id}") Log.d(TAG, "Message sent successfully: ${chatLine.id}")
// Update the chat room ID if it's the first message
// This is the key part - we get the chat room ID from the response
val newChatRoomId = chatLine.chatRoomId
if ((_chatRoomId.value ?: 0) == 0 && newChatRoomId > 0) {
Log.d(TAG, "Chat room created: $newChatRoomId")
_chatRoomId.value = newChatRoomId
// Now that we have a chat room ID, we can join the Socket.IO room
joinSocketRoom(newChatRoomId)
}
// Emit the message via Socket.IO for real-time updates // Emit the message via Socket.IO for real-time updates
socketService.sendMessage(chatLine) socketService.sendMessage(chatLine)
@ -308,9 +353,10 @@ class ChatViewModel @Inject constructor(
* Sends typing status to the other user * Sends typing status to the other user
*/ */
fun sendTypingStatus(isTyping: Boolean) { fun sendTypingStatus(isTyping: Boolean) {
if (chatRoomId == 0) return val roomId = _chatRoomId.value ?: 0
if (roomId <= 0) return
socketService.sendTypingStatus(chatRoomId, isTyping) socketService.sendTypingStatus(roomId, isTyping)
} }
/** /**

View File

@ -28,6 +28,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.chat.ChatActivity
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
import com.alya.ecommerce_serang.ui.order.CheckoutActivity import com.alya.ecommerce_serang.ui.order.CheckoutActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -45,7 +46,6 @@ class DetailProductActivity : AppCompatActivity() {
private var reviewsAdapter: ReviewsAdapter? = null private var reviewsAdapter: ReviewsAdapter? = null
private var currentQuantity = 1 private var currentQuantity = 1
private val viewModel: ProductUserViewModel by viewModels { private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -219,6 +219,9 @@ class DetailProductActivity : AppCompatActivity() {
binding.tvDescription.text = product.description binding.tvDescription.text = product.description
binding.btnChat.setOnClickListener{
navigateToChat()
}
val fullImageUrl = when (val img = product.image) { val fullImageUrl = when (val img = product.image) {
is String -> { is String -> {
@ -382,8 +385,30 @@ class DetailProductActivity : AppCompatActivity() {
) )
} }
private fun navigateToChat(){
val productDetail = viewModel.productDetail.value ?: return
val storeDetail = viewModel.storeDetail.value
if (storeDetail !is Result.Success || storeDetail.data == null) {
Toast.makeText(this, "Store information not available", Toast.LENGTH_SHORT).show()
return
}
ChatActivity.createIntent(
context = this,
storeId = productDetail.storeId,
productId = productDetail.productId,
productName = productDetail.productName,
productPrice = productDetail.price,
productImage = productDetail.image,
productRating = productDetail.rating,
storeName = storeDetail.data.storeName,
chatRoomId = 0
)
}
companion object { companion object {
const val EXTRA_PRODUCT_ID = "extra_product_id" private const val EXTRA_PRODUCT_ID = "extra_product_id"
fun start(context: Context, productId: Int) { fun start(context: Context, productId: Int) {
val intent = Intent(context, DetailProductActivity::class.java) val intent = Intent(context, DetailProductActivity::class.java)

View File

@ -19,10 +19,11 @@ class SessionManager(context: Context) {
sharedPreferences.edit() { sharedPreferences.edit() {
putString(USER_TOKEN, token) putString(USER_TOKEN, token)
} }
Log.d("SessionManager", "Saved token: $token")
} }
fun getToken(): String? { fun getToken(): String {
val token = sharedPreferences.getString(USER_TOKEN, null) val token = sharedPreferences.getString(USER_TOKEN, "") ?: ""
Log.d("SessionManager", "Retrieved token: $token") Log.d("SessionManager", "Retrieved token: $token")
return token return token
} }
@ -34,12 +35,16 @@ class SessionManager(context: Context) {
Log.d("SessionManager", "Saved user ID: $userId") Log.d("SessionManager", "Saved user ID: $userId")
} }
fun getUserId(): String? { fun getUserId(): String {
val userId = sharedPreferences.getString(USER_ID, null) val userId = sharedPreferences.getString(USER_ID, "") ?: ""
Log.d("SessionManager", "Retrieved user ID: $userId") Log.d("SessionManager", "Retrieved user ID: $userId")
return userId return userId
} }
fun isLoggedIn(): Boolean {
return getToken().isNotEmpty()
}
fun clearUserId() { fun clearUserId() {
sharedPreferences.edit() { sharedPreferences.edit() {
remove(USER_ID) remove(USER_ID)
@ -52,6 +57,8 @@ class SessionManager(context: Context) {
} }
} }
//clear data when log out //clear data when log out
fun clearAll() { fun clearAll() {
sharedPreferences.edit() { sharedPreferences.edit() {

View File

@ -1,7 +0,0 @@
package com.alya.ecommerce_serang.utils.viewmodel
import androidx.lifecycle.ViewModel
class ChatViewModel : ViewModel() {
// TODO: Implement the ViewModel
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
</vector>

View File

@ -0,0 +1,11 @@
<?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="20dp" />
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
</shape>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#489EC6" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@ -2,11 +2,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.chat.ChatActivity"> tools:context=".ui.chat.ChatActivity">
<!-- Top Toolbar -->
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/chatToolbar" android:id="@+id/chatToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -175,9 +176,23 @@
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput" app:layout_constraintBottom_toTopOf="@+id/tvTypingIndicator"
app:layout_constraintTop_toBottomOf="@+id/cardProduct" /> app:layout_constraintTop_toBottomOf="@+id/cardProduct" />
<!-- Typing indicator -->
<TextView
android:id="@+id/tvTypingIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:text="User is typing..."
android:textColor="#666666"
android:textSize="12sp"
android:textStyle="italic"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
tools:visibility="visible" />
<!-- Chat input area --> <!-- Chat input area -->
<LinearLayout <LinearLayout
android:id="@+id/layoutChatInput" android:id="@+id/layoutChatInput"
@ -196,7 +211,7 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attachment" android:contentDescription="Attachment"
android:src="@drawable/ic_attachment" /> android:src="@drawable/baseline_attach_file_24" />
<EditText <EditText
android:id="@+id/editTextMessage" android:id="@+id/editTextMessage"
@ -205,7 +220,9 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_edit_text_background"
android:hint="Tulis pesan" android:hint="Tulis pesan"
android:fontFamily="@font/dmsans_regular"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:maxLines="4" android:maxLines="4"
android:minHeight="40dp" android:minHeight="40dp"
@ -218,60 +235,7 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Send" android:contentDescription="Send"
android:src="@drawable/ic_send" /> android:src="@drawable/baseline_attach_file_24" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/tvTypingIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:text="User is typing..."
android:textColor="#666666"
android:textSize="12sp"
android:textStyle="italic"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
tools:visibility="visible" />
<!-- Bottom navigation -->
<LinearLayout
android:id="@+id/bottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
android:elevation="8dp"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:id="@+id/btnHome"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Home"
android:src="@drawable/ic_home" />
<ImageButton
android:id="@+id/btnMenu"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Menu"
android:src="@drawable/ic_menu" />
<ImageButton
android:id="@+id/btnNotification"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Notification"
android:src="@drawable/ic_notification" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,8 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto">
tools:context=".ui.chat.ChatFragment">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/chatToolbar" android:id="@+id/chatToolbar"
@ -235,7 +234,7 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attachment" android:contentDescription="Attachment"
android:src="@drawable/ic_attachment" /> android:src="@drawable/baseline_attach_file_24" />
<EditText <EditText
android:id="@+id/editTextMessage" android:id="@+id/editTextMessage"
@ -244,7 +243,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_edit_text_rounded" android:background="@drawable/bg_edit_text_background"
android:hint="Tulis pesan" android:hint="Tulis pesan"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:maxLines="4" android:maxLines="4"
@ -258,7 +257,7 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Send" android:contentDescription="Send"
android:src="@drawable/ic_send" /> android:src="@drawable/baseline_attach_file_24" />
</LinearLayout> </LinearLayout>

View File

@ -10,4 +10,10 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:text="Hello" /> android:text="Hello" />
<Button
android:id="@+id/btn_trial"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="trial button"/>
</FrameLayout> </FrameLayout>

View File

@ -13,7 +13,7 @@
android:id="@+id/imgAvatar" android:id="@+id/imgAvatar"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:src="@drawable/profile_placeholder" android:src="@drawable/ic_person"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/layoutMessage" /> app:layout_constraintTop_toTopOf="@+id/layoutMessage" />

View File

@ -117,5 +117,25 @@
<item>Other reason</item> <item>Other reason</item>
</string-array> </string-array>
<!-- Chat Activity -->
<string name="image_attached">Image attached</string>
<string name="write_message">Tulis pesan</string>
<string name="options">Options</string>
<string name="block_user">Block User</string>
<string name="report">Report</string>
<string name="clear_chat">Clear Chat</string>
<string name="block_user_selected">Block user selected</string>
<string name="report_selected">Report selected</string>
<string name="clear_chat_selected">Clear chat selected</string>
<string name="permission_denied">Permission denied</string>
<string name="take_photo">Take Photo</string>
<string name="choose_from_gallery">Choose from Gallery</string>
<string name="select_attachment">Select Attachment</string>
<string name="image_selected">Image selected</string>
<string name="connecting">Connecting...</string>
<string name="disconnected_reconnecting">Disconnected. Reconnecting...</string>
<string name="connection_error">Connection error: %1$s</string>
<string name="typing">User is typing...</string>
</resources> </resources>