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("message") message: RequestBody,
@Part("product_id") productId: RequestBody,
@Part("chatimg") chatimg: MultipartBody.Part
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@PUT("chatstatus")

View File

@ -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 {

View File

@ -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(

View File

@ -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()

View File

@ -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()

View File

@ -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)
}
}
}
}
//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.databinding.ItemMessageReceivedBinding
import com.alya.ecommerce_serang.databinding.ItemMessageSentBinding
import com.alya.ecommerce_serang.utils.Constants
import com.bumptech.glide.Glide
class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatDiffCallback()) {
class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMessageDiffCallback()) {
companion object {
private const val VIEW_TYPE_MESSAGE_SENT = 1
@ -67,10 +68,10 @@ class ChatAdapter : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(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<ChatUiMessage, RecyclerView.ViewHolder>(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<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 {
return oldItem.id == newItem.id
}

View File

@ -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<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showImagePickerOptions()
} else {
Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
typingHandler.removeCallbacks(stopTypingRunnable)
_binding = null
}
}
//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<out String>,
// grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// showImagePickerOptions()
// } else {
// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
// }
// }
// }
//
// override fun onDestroyView() {
// super.onDestroyView()
// typingHandler.removeCallbacks(stopTypingRunnable)
// _binding = null
// }
//}

View File

@ -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)
}
}
}

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.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<ChatUiState> = _state
// Chat parameters
private var chatRoomId: Int = 0
private val _chatRoomId = MutableLiveData<Int>(0)
val chatRoomId: LiveData<Int> = _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)
}
/**

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.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)

View File

@ -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() {

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"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.chat.ChatActivity">
<!-- Top Toolbar -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/chatToolbar"
android:layout_width="match_parent"
@ -175,9 +176,23 @@
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
app:layout_constraintBottom_toTopOf="@+id/tvTypingIndicator"
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 -->
<LinearLayout
android:id="@+id/layoutChatInput"
@ -196,7 +211,7 @@
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attachment"
android:src="@drawable/ic_attachment" />
android:src="@drawable/baseline_attach_file_24" />
<EditText
android:id="@+id/editTextMessage"
@ -205,7 +220,9 @@
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/bg_edit_text_background"
android:hint="Tulis pesan"
android:fontFamily="@font/dmsans_regular"
android:inputType="textMultiLine"
android:maxLines="4"
android:minHeight="40dp"
@ -218,60 +235,7 @@
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Send"
android:src="@drawable/ic_send" />
android:src="@drawable/baseline_attach_file_24" />
</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>

View File

@ -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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/chatToolbar"
@ -235,7 +234,7 @@
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attachment"
android:src="@drawable/ic_attachment" />
android:src="@drawable/baseline_attach_file_24" />
<EditText
android:id="@+id/editTextMessage"
@ -244,7 +243,7 @@
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/bg_edit_text_rounded"
android:background="@drawable/bg_edit_text_background"
android:hint="Tulis pesan"
android:inputType="textMultiLine"
android:maxLines="4"
@ -258,7 +257,7 @@
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Send"
android:src="@drawable/ic_send" />
android:src="@drawable/baseline_attach_file_24" />
</LinearLayout>

View File

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

View File

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

View File

@ -117,5 +117,25 @@
<item>Other reason</item>
</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>