mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
fix chat activity
This commit is contained in:
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
// }
|
||||
//}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -1,7 +0,0 @@
|
||||
package com.alya.ecommerce_serang.utils.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
// TODO: Implement the ViewModel
|
||||
}
|
5
app/src/main/res/drawable/baseline_attach_file_24.xml
Normal file
5
app/src/main/res/drawable/baseline_attach_file_24.xml
Normal 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>
|
11
app/src/main/res/drawable/bg_edit_text_background.xml
Normal file
11
app/src/main/res/drawable/bg_edit_text_background.xml
Normal 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>
|
5
app/src/main/res/drawable/check_double_24.xml
Normal file
5
app/src/main/res/drawable/check_double_24.xml
Normal 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>
|
5
app/src/main/res/drawable/check_double_read_24.xml
Normal file
5
app/src/main/res/drawable/check_double_read_24.xml
Normal 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>
|
5
app/src/main/res/drawable/check_single_24.xml
Normal file
5
app/src/main/res/drawable/check_single_24.xml
Normal 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>
|
@ -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>
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
@ -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" />
|
||||
|
||||
|
@ -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>
|
Reference in New Issue
Block a user