diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index aa21560..52ab2fc 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -220,7 +220,7 @@ interface ApiService { @Part("store_id") storeId: RequestBody, @Part("message") message: RequestBody, @Part("product_id") productId: RequestBody, - @Part("chatimg") chatimg: MultipartBody.Part + @Part chatimg: MultipartBody.Part? ): Response @PUT("chatstatus") diff --git a/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt b/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt index 5e67fb6..154f9d8 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt @@ -1,6 +1,5 @@ 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.repository.UserRepository import com.alya.ecommerce_serang.ui.chat.SocketIOService @@ -8,7 +7,6 @@ import com.alya.ecommerce_serang.utils.SessionManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -16,12 +14,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object ChatModule { - @Provides - @Singleton - fun provideSessionManager(@ApplicationContext context: Context): SessionManager { - return SessionManager(context) - } - @Provides @Singleton fun provideChatRepository(apiService: ApiService): UserRepository { diff --git a/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt index b982df2..5df72f4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt @@ -10,7 +10,6 @@ import androidx.core.app.NotificationManagerCompat import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiService -import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.utils.SessionManager import dagger.Module import dagger.Provides @@ -41,12 +40,6 @@ object NotificationModule { return ApiConfig.getApiService(sessionManager) } - @Provides - @Singleton - fun provideUserRepository(apiService: ApiService): UserRepository { - return UserRepository(apiService) - } - @Singleton @Provides fun provideNotificationBuilder( diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt index 49e34a5..e2c4839 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt @@ -58,6 +58,7 @@ class LoginActivity : AppCompatActivity() { val sessionManager = SessionManager(this) sessionManager.saveToken(accessToken) +// sessionManager.saveUserId(response.userId) Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index dbe2793..72d1e1a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -37,12 +37,27 @@ class RegisterActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + binding = ActivityRegisterBinding.inflate(layoutInflater) + setContentView(binding.root) sessionManager = SessionManager(this) - if (!sessionManager.getToken().isNullOrEmpty()) { - // User already logged in, redirect to MainActivity - startActivity(Intent(this, MainActivity::class.java)) - finish() + Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'") + Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'") + + 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) @@ -61,8 +76,7 @@ class RegisterActivity : AppCompatActivity() { windowInsets } - binding = ActivityRegisterBinding.inflate(layoutInflater) - setContentView(binding.root) + // Observe OTP state observeOtpState() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt index 432077c..ac62ef3 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt @@ -1,6 +1,8 @@ package com.alya.ecommerce_serang.ui.chat +import android.Manifest import android.app.Activity +import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -19,24 +21,21 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig -import com.alya.ecommerce_serang.data.repository.ProductRepository -import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.databinding.ActivityChatBinding 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.SessionManager import com.bumptech.glide.Glide 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 javax.inject.Inject @@ -47,27 +46,18 @@ class ChatActivity : AppCompatActivity() { @Inject lateinit var sessionManager: SessionManager - private lateinit var socketService: SocketIOService - - @Inject private lateinit var chatAdapter: ChatAdapter - private val viewModel: ChatViewModel by viewModels { - BaseViewModelFactory { - val apiService = ApiConfig.getApiService(sessionManager) - val userRepository = UserRepository(apiService) - ChatViewModel(userRepository, socketService, sessionManager) - } - } + private val viewModel: ChatViewModel by viewModels() // For image attachment private var tempImageUri: Uri? = null - // Chat parameters from intent - private var chatRoomId: Int = 0 - private var storeId: Int = 0 - private var productId: Int = 0 +// // Chat parameters from intent +// private var chatRoomId: Int = 0 +// private var storeId: Int = 0 +// private var productId: Int = 0 // Typing indicator handler private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) @@ -101,16 +91,40 @@ class ChatActivity : AppCompatActivity() { binding = ActivityChatBinding.inflate(layoutInflater) 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 - chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0) - storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0) - productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0) + val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0) + val productId = intent.getIntExtra(Constants.EXTRA_PRODUCT_ID, 0) + val productName = intent.getStringExtra(Constants.EXTRA_PRODUCT_NAME) ?: "" + val productPrice = intent.getStringExtra(Constants.EXTRA_PRODUCT_PRICE) ?: "" + val productImage = intent.getStringExtra(Constants.EXTRA_PRODUCT_IMAGE) ?: "" + val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f) + val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: "" + // Check if user is logged in val userId = sessionManager.getUserId() val token = sessionManager.getToken() - if (userId.isNullOrEmpty() || token.isNullOrEmpty()) { + if (token.isEmpty()) { // User not logged in, redirect to login Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show() startActivity(Intent(this, LoginActivity::class.java)) @@ -118,30 +132,23 @@ class ChatActivity : AppCompatActivity() { return } - Log.d(TAG, "Chat Activity started - User ID: $userId, Chat Room: $chatRoomId") - - // Initialize ViewModel - initViewModel() - + // Set chat parameters to ViewModel + viewModel.setChatParameters( + storeId = storeId, + productId = productId, + productName = productName, + productPrice = productPrice, + productImage = productImage, + productRating = productRating, + storeName = storeName + ) // Setup UI components setupRecyclerView() setupListeners() setupTypingIndicator() observeViewModel() - } - 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() { @@ -154,6 +161,7 @@ class ChatActivity : AppCompatActivity() { } } + private fun setupListeners() { // Back button binding.btnBack.setOnClickListener { @@ -168,7 +176,8 @@ class ChatActivity : AppCompatActivity() { // Send button binding.btnSend.setOnClickListener { 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) binding.editTextMessage.text.clear() } @@ -197,79 +206,64 @@ class ChatActivity : AppCompatActivity() { } private fun observeViewModel() { - lifecycleScope.launch { - viewModel.state.collectLatest { state -> - // Update messages - chatAdapter.submitList(state.messages) + viewModel.chatRoomId.observe(this, Observer { chatRoomId -> + if (chatRoomId > 0) { + // Chat room has been created, now we can join the Socket.IO room + viewModel.joinSocketRoom(chatRoomId) - // Scroll to bottom if new message - if (state.messages.isNotEmpty()) { - binding.recyclerChat.scrollToPosition(state.messages.size - 1) - } + // Now we can also load chat history + viewModel.loadChatHistory(chatRoomId) + 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() { val options = arrayOf( @@ -388,5 +382,56 @@ class ChatActivity : AppCompatActivity() { companion object { 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) + } } -} \ No newline at end of file +} + +//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) +// } +// } +// } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt index 1d41f53..a0d5ffc 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt @@ -10,9 +10,10 @@ import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.databinding.ItemMessageReceivedBinding import com.alya.ecommerce_serang.databinding.ItemMessageSentBinding +import com.alya.ecommerce_serang.utils.Constants import com.bumptech.glide.Glide -class ChatAdapter : ListAdapter(ChatDiffCallback()) { +class ChatAdapter : ListAdapter(ChatMessageDiffCallback()) { companion object { private const val VIEW_TYPE_MESSAGE_SENT = 1 @@ -67,10 +68,10 @@ class ChatAdapter : ListAdapter(ChatDiff // Show message status val statusIcon = when (message.status) { - Constants.STATUS_SENT -> R.drawable.ic_check - Constants.STATUS_DELIVERED -> R.drawable.ic_double_check - Constants.STATUS_READ -> R.drawable.ic_double_check_read - else -> R.drawable.ic_check + 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) @@ -114,7 +115,7 @@ class ChatAdapter : ListAdapter(ChatDiff // Load avatar image 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() .into(binding.imgAvatar) } @@ -122,9 +123,9 @@ class ChatAdapter : ListAdapter(ChatDiff } /** - * DiffCallback for optimizing RecyclerView updates + * DiffUtil callback for optimizing RecyclerView updates */ -class ChatDiffCallback : DiffUtil.ItemCallback() { +class ChatMessageDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean { return oldItem.id == newItem.id } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt index 3cb5e4d..4bd12ae 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt @@ -1,343 +1,337 @@ -package com.alya.ecommerce_serang.ui.chat - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.text.TextWatcher -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.LinearLayoutManager -import com.alya.ecommerce_serang.BuildConfig.BASE_URL -import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.databinding.FragmentChatBinding -import com.alya.ecommerce_serang.utils.Constants -import com.bumptech.glide.Glide -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import java.io.File -import java.text.SimpleDateFormat -import java.util.Locale - - -/** - * A simple [Fragment] subclass. - * Use the [ChatFragment.newInstance] factory method to - * create an instance of this fragment. - */ -@AndroidEntryPoint -class ChatFragment : Fragment() { - - private var _binding: FragmentChatBinding? = null - private val binding get() = _binding!! - - private val viewModel: ChatViewModel by viewModels() - private val args: ChatFragmentArgs by navArgs() - - private lateinit var chatAdapter: ChatAdapter - - // For image attachment - private var tempImageUri: Uri? = null - - // Typing indicator handler - private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) - private val stopTypingRunnable = Runnable { - viewModel.sendTypingStatus(false) - } - - // Activity Result Launchers - private val pickImageLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { uri -> - handleSelectedImage(uri) - } - } - } - - private val takePictureLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - tempImageUri?.let { uri -> - handleSelectedImage(uri) - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentChatBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupRecyclerView() - setupListeners() - setupTypingIndicator() - observeViewModel() - } - - private fun setupRecyclerView() { - chatAdapter = ChatAdapter() - binding.recyclerChat.apply { - adapter = chatAdapter - layoutManager = LinearLayoutManager(requireContext()).apply { - stackFromEnd = true - } - } - } - - private fun setupListeners() { - // Back button - binding.btnBack.setOnClickListener { - requireActivity().onBackPressed() - } - - // Options button - binding.btnOptions.setOnClickListener { - showOptionsMenu() - } - - // Send button - binding.btnSend.setOnClickListener { - val message = binding.editTextMessage.text.toString().trim() - if (message.isNotEmpty() || viewModel.state.value.hasAttachment) { - viewModel.sendMessage(message) - binding.editTextMessage.text.clear() - } - } - - // Attachment button - binding.btnAttachment.setOnClickListener { - checkPermissionsAndShowImagePicker() - } - } - - private fun setupTypingIndicator() { - binding.editTextMessage.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - viewModel.sendTypingStatus(true) - - // Reset the timer - typingHandler.removeCallbacks(stopTypingRunnable) - typingHandler.postDelayed(stopTypingRunnable, 1000) - } - - override fun afterTextChanged(s: Editable?) {} - }) - } - - private fun observeViewModel() { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.state.collectLatest { 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(requireContext()) - .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(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 -> { - 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() { - val options = arrayOf( - getString(R.string.block_user), - getString(R.string.report), - getString(R.string.clear_chat), - getString(R.string.cancel) - ) - - androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle(getString(R.string.options)) - .setItems(options) { dialog, which -> - 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() - 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show() - } - dialog.dismiss() - } - .show() - } - - private fun checkPermissionsAndShowImagePicker() { - if (ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.READ_EXTERNAL_STORAGE - ) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - requireActivity(), - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA), - Constants.REQUEST_STORAGE_PERMISSION - ) - } else { - showImagePickerOptions() - } - } - - private fun showImagePickerOptions() { - val options = arrayOf( - getString(R.string.take_photo), - getString(R.string.choose_from_gallery), - getString(R.string.cancel) - ) - - androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle(getString(R.string.select_attachment)) - .setItems(options) { dialog, which -> - when (which) { - 0 -> openCamera() - 1 -> openGallery() - } - dialog.dismiss() - } - .show() - } - - private fun openCamera() { - val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val imageFileName = "IMG_${timeStamp}.jpg" - val storageDir = requireContext().getExternalFilesDir(null) - val imageFile = File(storageDir, imageFileName) - - tempImageUri = FileProvider.getUriForFile( - requireContext(), - "${requireContext().packageName}.fileprovider", - imageFile - ) - - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { - putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri) - } - - takePictureLauncher.launch(intent) - } - - private fun openGallery() { - val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) - pickImageLauncher.launch(intent) - } - - private fun handleSelectedImage(uri: Uri) { - // Get the file from Uri - val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) - val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null) - cursor?.moveToFirst() - val columnIndex = cursor?.getColumnIndex(filePathColumn[0]) - val filePath = cursor?.getString(columnIndex ?: 0) - cursor?.close() - - if (filePath != null) { - viewModel.setSelectedImageFile(File(filePath)) - Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show() - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showImagePickerOptions() - } else { - Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show() - } - } - } - - override fun onDestroyView() { - super.onDestroyView() - typingHandler.removeCallbacks(stopTypingRunnable) - _binding = null - } -} \ No newline at end of file +//package com.alya.ecommerce_serang.ui.chat +// +//import android.Manifest +//import android.app.Activity +//import android.content.Intent +//import android.content.pm.PackageManager +//import android.net.Uri +//import android.os.Bundle +//import android.provider.MediaStore +//import android.text.Editable +//import android.text.TextWatcher +//import androidx.fragment.app.Fragment +//import android.view.LayoutInflater +//import android.view.View +//import android.view.ViewGroup +//import android.widget.Toast +//import androidx.activity.result.contract.ActivityResultContracts +//import androidx.core.app.ActivityCompat +//import androidx.core.content.ContextCompat +//import androidx.core.content.FileProvider +//import androidx.fragment.app.viewModels +//import androidx.lifecycle.lifecycleScope +//import androidx.navigation.fragment.navArgs +//import androidx.recyclerview.widget.LinearLayoutManager +//import com.alya.ecommerce_serang.BuildConfig.BASE_URL +//import com.alya.ecommerce_serang.R +//import com.alya.ecommerce_serang.databinding.FragmentChatBinding +//import com.alya.ecommerce_serang.utils.Constants +//import com.bumptech.glide.Glide +//import dagger.hilt.android.AndroidEntryPoint +//import kotlinx.coroutines.launch +//import java.io.File +//import java.text.SimpleDateFormat +//import java.util.Locale +// +//@AndroidEntryPoint +//class ChatFragment : Fragment() { +// +// private var _binding: FragmentChatBinding? = null +// private val binding get() = _binding!! +// +// private val viewModel: ChatViewModel by viewModels() +//// private val args: ChatFragmentArgs by navArgs() +// +// private lateinit var chatAdapter: ChatAdapter +// +// // For image attachment +// private var tempImageUri: Uri? = null +// +// // Typing indicator handler +// private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) +// private val stopTypingRunnable = Runnable { +// viewModel.sendTypingStatus(false) +// } +// +// // Activity Result Launchers +// private val pickImageLauncher = registerForActivityResult( +// ActivityResultContracts.StartActivityForResult() +// ) { result -> +// if (result.resultCode == Activity.RESULT_OK) { +// result.data?.data?.let { uri -> +// handleSelectedImage(uri) +// } +// } +// } +// +// private val takePictureLauncher = registerForActivityResult( +// ActivityResultContracts.StartActivityForResult() +// ) { result -> +// if (result.resultCode == Activity.RESULT_OK) { +// tempImageUri?.let { uri -> +// handleSelectedImage(uri) +// } +// } +// } +// +// override fun onCreateView( +// inflater: LayoutInflater, +// container: ViewGroup?, +// savedInstanceState: Bundle? +// ): View { +// _binding = FragmentChatBinding.inflate(inflater, container, false) +// return binding.root +// } +// +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// super.onViewCreated(view, savedInstanceState) +// +// setupRecyclerView() +// setupListeners() +// setupTypingIndicator() +// observeViewModel() +// } +// +// private fun setupRecyclerView() { +// chatAdapter = ChatAdapter() +// binding.recyclerChat.apply { +// adapter = chatAdapter +// layoutManager = LinearLayoutManager(requireContext()).apply { +// stackFromEnd = true +// } +// } +// } +// +// private fun setupListeners() { +// // Back button +// binding.btnBack.setOnClickListener { +// requireActivity().onBackPressed() +// } +// +// // Options button +// binding.btnOptions.setOnClickListener { +// showOptionsMenu() +// } +// +// // Send button +// binding.btnSend.setOnClickListener { +// val message = binding.editTextMessage.text.toString().trim() +// if (message.isNotEmpty() || viewModel.state.value.hasAttachment) { +// viewModel.sendMessage(message) +// binding.editTextMessage.text.clear() +// } +// } +// +// // Attachment button +// binding.btnAttachment.setOnClickListener { +// checkPermissionsAndShowImagePicker() +// } +// } +// +// private fun setupTypingIndicator() { +// binding.editTextMessage.addTextChangedListener(object : TextWatcher { +// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} +// +// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { +// viewModel.sendTypingStatus(true) +// +// // Reset the timer +// typingHandler.removeCallbacks(stopTypingRunnable) +// typingHandler.postDelayed(stopTypingRunnable, 1000) +// } +// +// override fun afterTextChanged(s: Editable?) {} +// }) +// } +// +// private fun observeViewModel() { +// viewLifecycleOwner.lifecycleScope.launch { +// viewModel.state.collectLatest { 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(requireContext()) +// .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(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 -> { +// 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() { +// val options = arrayOf( +// getString(R.string.block_user), +// getString(R.string.report), +// getString(R.string.clear_chat), +// getString(R.string.cancel) +// ) +// +// androidx.appcompat.app.AlertDialog.Builder(requireContext()) +// .setTitle(getString(R.string.options)) +// .setItems(options) { dialog, which -> +// 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() +// 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show() +// } +// dialog.dismiss() +// } +// .show() +// } +// +// private fun checkPermissionsAndShowImagePicker() { +// if (ContextCompat.checkSelfPermission( +// requireContext(), +// Manifest.permission.READ_EXTERNAL_STORAGE +// ) != PackageManager.PERMISSION_GRANTED +// ) { +// ActivityCompat.requestPermissions( +// requireActivity(), +// arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA), +// Constants.REQUEST_STORAGE_PERMISSION +// ) +// } else { +// showImagePickerOptions() +// } +// } +// +// private fun showImagePickerOptions() { +// val options = arrayOf( +// getString(R.string.take_photo), +// getString(R.string.choose_from_gallery), +// getString(R.string.cancel) +// ) +// +// androidx.appcompat.app.AlertDialog.Builder(requireContext()) +// .setTitle(getString(R.string.select_attachment)) +// .setItems(options) { dialog, which -> +// when (which) { +// 0 -> openCamera() +// 1 -> openGallery() +// } +// dialog.dismiss() +// } +// .show() +// } +// +// private fun openCamera() { +// val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) +// val imageFileName = "IMG_${timeStamp}.jpg" +// val storageDir = requireContext().getExternalFilesDir(null) +// val imageFile = File(storageDir, imageFileName) +// +// tempImageUri = FileProvider.getUriForFile( +// requireContext(), +// "${requireContext().packageName}.fileprovider", +// imageFile +// ) +// +// val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { +// putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri) +// } +// +// takePictureLauncher.launch(intent) +// } +// +// private fun openGallery() { +// val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) +// pickImageLauncher.launch(intent) +// } +// +// private fun handleSelectedImage(uri: Uri) { +// // Get the file from Uri +// val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) +// val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null) +// cursor?.moveToFirst() +// val columnIndex = cursor?.getColumnIndex(filePathColumn[0]) +// val filePath = cursor?.getString(columnIndex ?: 0) +// cursor?.close() +// +// if (filePath != null) { +// viewModel.setSelectedImageFile(File(filePath)) +// Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show() +// } +// } +// +// override fun onRequestPermissionsResult( +// requestCode: Int, +// permissions: Array, +// grantResults: IntArray +// ) { +// super.onRequestPermissionsResult(requestCode, permissions, grantResults) +// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) { +// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// showImagePickerOptions() +// } else { +// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show() +// } +// } +// } +// +// override fun onDestroyView() { +// super.onDestroyView() +// typingHandler.removeCallbacks(stopTypingRunnable) +// _binding = null +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt index a177a58..a43fe72 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt @@ -1,32 +1,56 @@ package com.alya.ecommerce_serang.ui.chat +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.utils.viewmodel.ChatViewModel +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +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() { - companion object { - fun newInstance() = ChatListFragment() + 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 userRepository = UserRepository(apiService) + ChatViewModel(userRepository, socketService, sessionManager) + } } - - private val viewModel: ChatViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sessionManager = SessionManager(requireContext()) - // TODO: Use the ViewModel } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt index fa3c8ee..bcabe72 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -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.utils.Constants import com.alya.ecommerce_serang.utils.SessionManager +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File import java.util.Locale import java.util.TimeZone import javax.inject.Inject +@HiltViewModel class ChatViewModel @Inject constructor( private val chatRepository: UserRepository, private val socketService: SocketIOService, @@ -29,11 +31,15 @@ class ChatViewModel @Inject constructor( private val _state = MutableLiveData(ChatUiState()) val state: LiveData = _state - // Chat parameters - private var chatRoomId: Int = 0 + private val _chatRoomId = MutableLiveData(0) + val chatRoomId: LiveData = _chatRoomId + + // Store and product parameters private var storeId: 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 private var productName: String = "" @@ -47,14 +53,29 @@ class ChatViewModel @Inject constructor( init { // 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) { - Log.e(TAG, "Error: User ID is not set or invalid") - updateState { it.copy(error = "User authentication error. Please login again.") } - } else { - // Set up socket listeners - setupSocketListeners() + // Move the validation and subsequent logic inside the coroutine + if (currentUserId == 0) { + Log.e(TAG, "Error: User ID is not set or invalid") + updateState { it.copy(error = "User authentication error. Please login again.") } + } else { + // 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 */ fun setChatParameters( - chatRoomId: Int, storeId: Int, productId: Int, productName: String, @@ -71,7 +91,6 @@ class ChatViewModel @Inject constructor( productRating: Float, storeName: String ) { - this.chatRoomId = chatRoomId this.storeId = storeId this.productId = productId this.productName = productName @@ -92,8 +111,23 @@ class ChatViewModel @Inject constructor( } // Connect to socket and load chat history - socketService.connect() - loadChatHistory() + val existingChatRoomId = _chatRoomId.value ?: 0 + 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 socketService.typingStatus.collect { typingStatus -> typingStatus?.let { - if (typingStatus.roomId == chatRoomId && typingStatus.userId != currentUserId) { + if (typingStatus.roomId == (_chatRoomId.value ?: 0) && typingStatus.userId != currentUserId) { updateState { it.copy(isOtherUserTyping = typingStatus.isTyping) } } } @@ -154,8 +188,8 @@ class ChatViewModel @Inject constructor( /** * Loads chat history */ - fun loadChatHistory() { - if (chatRoomId == 0) { + fun loadChatHistory(chatRoomId : Int) { + if (chatRoomId <= 0) { Log.e(TAG, "Cannot load chat history: Chat room ID is 0") return } @@ -242,6 +276,17 @@ class ChatViewModel @Inject constructor( 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 socketService.sendMessage(chatLine) @@ -308,9 +353,10 @@ class ChatViewModel @Inject constructor( * Sends typing status to the other user */ 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) } /** diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index 4be3e57..e849a91 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -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.Result 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.order.CheckoutActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory @@ -45,7 +46,6 @@ class DetailProductActivity : AppCompatActivity() { private var reviewsAdapter: ReviewsAdapter? = null private var currentQuantity = 1 - private val viewModel: ProductUserViewModel by viewModels { BaseViewModelFactory { val apiService = ApiConfig.getApiService(sessionManager) @@ -219,6 +219,9 @@ class DetailProductActivity : AppCompatActivity() { binding.tvDescription.text = product.description + binding.btnChat.setOnClickListener{ + navigateToChat() + } val fullImageUrl = when (val img = product.image) { 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 { - const val EXTRA_PRODUCT_ID = "extra_product_id" + private const val EXTRA_PRODUCT_ID = "extra_product_id" fun start(context: Context, productId: Int) { val intent = Intent(context, DetailProductActivity::class.java) diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt index f04b37e..d3fd47b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt @@ -19,10 +19,11 @@ class SessionManager(context: Context) { sharedPreferences.edit() { putString(USER_TOKEN, token) } + Log.d("SessionManager", "Saved token: $token") } - fun getToken(): String? { - val token = sharedPreferences.getString(USER_TOKEN, null) + fun getToken(): String { + val token = sharedPreferences.getString(USER_TOKEN, "") ?: "" Log.d("SessionManager", "Retrieved token: $token") return token } @@ -34,12 +35,16 @@ class SessionManager(context: Context) { Log.d("SessionManager", "Saved user ID: $userId") } - fun getUserId(): String? { - val userId = sharedPreferences.getString(USER_ID, null) + fun getUserId(): String { + val userId = sharedPreferences.getString(USER_ID, "") ?: "" Log.d("SessionManager", "Retrieved user ID: $userId") return userId } + fun isLoggedIn(): Boolean { + return getToken().isNotEmpty() + } + fun clearUserId() { sharedPreferences.edit() { remove(USER_ID) @@ -52,6 +57,8 @@ class SessionManager(context: Context) { } } + + //clear data when log out fun clearAll() { sharedPreferences.edit() { diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt deleted file mode 100644 index 394a67a..0000000 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.alya.ecommerce_serang.utils.viewmodel - -import androidx.lifecycle.ViewModel - -class ChatViewModel : ViewModel() { - // TODO: Implement the ViewModel -} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_attach_file_24.xml b/app/src/main/res/drawable/baseline_attach_file_24.xml new file mode 100644 index 0000000..fe3f21d --- /dev/null +++ b/app/src/main/res/drawable/baseline_attach_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_edit_text_background.xml b/app/src/main/res/drawable/bg_edit_text_background.xml new file mode 100644 index 0000000..0cfc787 --- /dev/null +++ b/app/src/main/res/drawable/bg_edit_text_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/check_double_24.xml b/app/src/main/res/drawable/check_double_24.xml new file mode 100644 index 0000000..06b7aff --- /dev/null +++ b/app/src/main/res/drawable/check_double_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/check_double_read_24.xml b/app/src/main/res/drawable/check_double_read_24.xml new file mode 100644 index 0000000..33b9cbc --- /dev/null +++ b/app/src/main/res/drawable/check_double_read_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/check_single_24.xml b/app/src/main/res/drawable/check_single_24.xml new file mode 100644 index 0000000..c3d38c5 --- /dev/null +++ b/app/src/main/res/drawable/check_single_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 074ead1..f6e2b69 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -2,11 +2,12 @@ + + + + + android:src="@drawable/baseline_attach_file_24" /> + android:src="@drawable/baseline_attach_file_24" /> - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index 073b01a..53816c6 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -3,8 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:app="http://schemas.android.com/apk/res-auto" - tools:context=".ui.chat.ChatFragment"> + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:src="@drawable/baseline_attach_file_24" /> + android:src="@drawable/baseline_attach_file_24" /> diff --git a/app/src/main/res/layout/fragment_chat_list.xml b/app/src/main/res/layout/fragment_chat_list.xml index 94b500b..e701c83 100644 --- a/app/src/main/res/layout/fragment_chat_list.xml +++ b/app/src/main/res/layout/fragment_chat_list.xml @@ -10,4 +10,10 @@ android:layout_height="match_parent" android:text="Hello" /> +