diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index aea446e..83acdfd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,10 +2,10 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
- id("kotlin-kapt")
- id ("androidx.navigation.safeargs")
+ alias(libs.plugins.ksp) // Use KSP instead of kapt
+ id("androidx.navigation.safeargs")
id("kotlin-parcelize")
-// id("com.google.dagger.hilt.android")
+ alias(libs.plugins.dagger.hilt) // Use alias from catalog
}
val localProperties = Properties().apply {
@@ -98,11 +98,27 @@ dependencies {
implementation("org.osmdroid:osmdroid-android:6.0.3")
// implementation(libs.hilt.android)
-// kapt("com.google.dagger:hilt-compiler:2.48")
-//
-// // For ViewModel injection (if needed)
-// implementation(libs.androidx.hilt.lifecycle.viewmodel)
-// kapt("androidx.hilt:hilt-compiler:1.0.0")
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ // Androidx Hilt
+ implementation(libs.androidx.hilt.navigation.fragment)
+ implementation(libs.androidx.hilt.work)
+ ksp(libs.androidx.hilt.compiler)
+
+ implementation("androidx.work:work-runtime-ktx:2.8.1")
+ implementation("androidx.work:work-runtime:2.8.1")
+
+ implementation("io.ktor:ktor-client-android:3.0.1")
+ implementation("io.ktor:ktor-client-core:3.0.1")
+ implementation("io.ktor:ktor-client-websockets:3.0.1")
+ implementation("io.ktor:ktor-client-logging:3.0.1")
+ implementation("io.ktor:ktor-client-okhttp:3.0.1")
+ implementation("io.ktor:ktor-client-content-negotiation:3.0.1")
+ implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.1")
+
+ implementation("io.socket:socket.io-client:2.1.0") // or latest version
+
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fada1ca..47fc23d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,10 +7,16 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt
index 31a11eb..11361c4 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt
@@ -1,7 +1,18 @@
package com.alya.ecommerce_serang.app
import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
-//@HiltAndroidApp
+@HiltAndroidApp
class App : Application(){
+// override fun onCreate() {
+// super.onCreate()
+//
+// val sessionManager = SessionManager(this)
+// if (sessionManager.getUserId() != null) {
+// val serviceIntent = Intent(this, SimpleWebSocketService::class.java)
+// startService(serviceIntent)
+// }
+// }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ChatRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ChatRequest.kt
new file mode 100644
index 0000000..13064e8
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ChatRequest.kt
@@ -0,0 +1,5 @@
+package com.alya.ecommerce_serang.data.api.dto
+
+class ChatRequest {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt
index 650eab3..d1a4482 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt
@@ -8,7 +8,11 @@ data class RegisterRequest (
val password: String?,
val username: String?,
val phone: String?,
- @SerializedName("birth_date") val birthDate: String?,
- @SerializedName("userimg") val image: String?,
+ @SerializedName("birth_date")
+ val birthDate: String?,
+
+ @SerializedName("userimg")
+ val image: String? = null,
+
val otp: String? = null
)
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt
new file mode 100644
index 0000000..87c39cf
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt
@@ -0,0 +1,8 @@
+package com.alya.ecommerce_serang.data.api.dto
+
+import com.google.gson.annotations.SerializedName
+
+data class SearchRequest(
+ @SerializedName("search_query")
+ val searchQuery: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/UpdateChatRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/UpdateChatRequest.kt
new file mode 100644
index 0000000..e21b42f
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/UpdateChatRequest.kt
@@ -0,0 +1,6 @@
+package com.alya.ecommerce_serang.data.api.dto
+
+data class UpdateChatRequest (
+ val id: Int,
+ val status: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatHistoryResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatHistoryResponse.kt
new file mode 100644
index 0000000..95fbd16
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/ChatHistoryResponse.kt
@@ -0,0 +1,39 @@
+package com.alya.ecommerce_serang.data.api.response.chat
+
+import com.google.gson.annotations.SerializedName
+
+data class ChatHistoryResponse(
+
+ @field:SerializedName("chat")
+ val chat: List,
+
+ @field:SerializedName("message")
+ val message: String
+)
+
+data class ChatItem(
+
+ @field:SerializedName("attachment")
+ val attachment: String? = null,
+
+ @field:SerializedName("product_id")
+ val productId: Int,
+
+ @field:SerializedName("chat_room_id")
+ val chatRoomId: Int,
+
+ @field:SerializedName("created_at")
+ val createdAt: String,
+
+ @field:SerializedName("id")
+ val id: Int,
+
+ @field:SerializedName("message")
+ val message: String,
+
+ @field:SerializedName("sender_id")
+ val senderId: Int,
+
+ @field:SerializedName("status")
+ val status: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/SendChatResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/SendChatResponse.kt
new file mode 100644
index 0000000..61daec2
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/SendChatResponse.kt
@@ -0,0 +1,39 @@
+package com.alya.ecommerce_serang.data.api.response.chat
+
+import com.google.gson.annotations.SerializedName
+
+data class SendChatResponse(
+
+ @field:SerializedName("chatLine")
+ val chatLine: ChatLine,
+
+ @field:SerializedName("message")
+ val message: String
+)
+
+data class ChatLine(
+
+ @field:SerializedName("attachment")
+ val attachment: String? = null,
+
+ @field:SerializedName("product_id")
+ val productId: Int,
+
+ @field:SerializedName("chat_room_id")
+ val chatRoomId: Int,
+
+ @field:SerializedName("created_at")
+ val createdAt: String,
+
+ @field:SerializedName("id")
+ val id: Int,
+
+ @field:SerializedName("message")
+ val message: String,
+
+ @field:SerializedName("sender_id")
+ val senderId: Int,
+
+ @field:SerializedName("status")
+ val status: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/UpdateChatResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/UpdateChatResponse.kt
new file mode 100644
index 0000000..6cb3912
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/chat/UpdateChatResponse.kt
@@ -0,0 +1,39 @@
+package com.alya.ecommerce_serang.data.api.response.chat
+
+import com.google.gson.annotations.SerializedName
+
+data class UpdateChatResponse(
+
+ @field:SerializedName("address")
+ val address: Address,
+
+ @field:SerializedName("message")
+ val message: String
+)
+
+data class Address(
+
+ @field:SerializedName("attachment")
+ val attachment: String? = null,
+
+ @field:SerializedName("product_id")
+ val productId: Int,
+
+ @field:SerializedName("chat_room_id")
+ val chatRoomId: Int,
+
+ @field:SerializedName("created_at")
+ val createdAt: String,
+
+ @field:SerializedName("id")
+ val id: Int,
+
+ @field:SerializedName("message")
+ val message: String,
+
+ @field:SerializedName("sender_id")
+ val senderId: Int,
+
+ @field:SerializedName("status")
+ val status: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/CreateSearchResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/CreateSearchResponse.kt
new file mode 100644
index 0000000..b6490bd
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/CreateSearchResponse.kt
@@ -0,0 +1,24 @@
+package com.alya.ecommerce_serang.data.api.response.product
+
+import com.google.gson.annotations.SerializedName
+
+data class CreateSearchResponse(
+
+ @field:SerializedName("search")
+ val search: Search
+)
+
+data class Search(
+
+ @field:SerializedName("user_id")
+ val userId: Int,
+
+ @field:SerializedName("created_at")
+ val createdAt: String,
+
+ @field:SerializedName("id")
+ val id: Int,
+
+ @field:SerializedName("search_query")
+ val searchQuery: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/SearchHistoryResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/SearchHistoryResponse.kt
new file mode 100644
index 0000000..43dabc0
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/product/SearchHistoryResponse.kt
@@ -0,0 +1,15 @@
+package com.alya.ecommerce_serang.data.api.response.product
+
+import com.google.gson.annotations.SerializedName
+
+data class SearchHistoryResponse(
+
+ @field:SerializedName("data")
+ val data: List
+)
+
+data class DataItem(
+
+ @field:SerializedName("search_query")
+ val searchQuery: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
index 15d6858..e0e5352 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
@@ -10,9 +10,12 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
+import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
+import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
+
import okhttp3.MultipartBody
import okhttp3.RequestBody
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
@@ -21,6 +24,9 @@ import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse
+import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
+import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
+import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
@@ -41,6 +47,10 @@ import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddres
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
+import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
+import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
+
+
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
@@ -48,6 +58,8 @@ import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.HeaderMap
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
@@ -225,4 +237,31 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part
): Response
+ @POST("search")
+ suspend fun saveSearchQuery(
+ @Body searchRequest: SearchRequest
+ ): Response
+
+ @GET("search")
+ suspend fun getSearchHistory(): Response
+
+ @Multipart
+ @POST("sendchat")
+ suspend fun sendChatLine(
+ @Part("store_id") storeId: RequestBody,
+ @Part("message") message: RequestBody,
+ @Part("product_id") productId: RequestBody,
+ @Part chatimg: MultipartBody.Part?
+ ): Response
+
+
+ @PUT("chatstatus")
+ suspend fun updateChatStatus(
+ @Body request: UpdateChatRequest
+ ): Response
+
+ @GET("chat/{chatRoomId}")
+ suspend fun getChatDetail(
+ @Path("chatRoomId") chatRoomId: Int
+ ): Response
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt
new file mode 100644
index 0000000..751f93b
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt
@@ -0,0 +1,131 @@
+package com.alya.ecommerce_serang.data.repository
+
+import android.util.Log
+import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
+import com.alya.ecommerce_serang.data.api.dto.UserProfile
+import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
+import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
+import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
+import com.alya.ecommerce_serang.data.api.retrofit.ApiService
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import java.io.File
+import javax.inject.Inject
+
+class ChatRepository @Inject constructor(
+ private val apiService: ApiService
+) {
+ private val TAG = "ChatRepository"
+
+ suspend fun fetchUserProfile(): Result {
+ return try {
+ val response = apiService.getUserProfile()
+ if (response.isSuccessful) {
+ response.body()?.user?.let {
+ Result.Success(it) // ✅ Returning only UserProfile
+ } ?: Result.Error(Exception("User data not found"))
+ } else {
+ Result.Error(Exception("Error fetching profile: ${response.code()}"))
+ }
+ } catch (e: Exception) {
+ Result.Error(e)
+ }
+ }
+
+ suspend fun sendChatMessage(
+ storeId: Int,
+ message: String,
+ productId: Int,
+ imageFile: File? = null
+ ): Result {
+ return try {
+ // Create request bodies for text fields
+ val storeIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), storeId.toString())
+ val messageBody = RequestBody.create("text/plain".toMediaTypeOrNull(), message)
+ val productIdBody = RequestBody.create("text/plain".toMediaTypeOrNull(), productId.toString())
+
+ // Create multipart body for the image file
+ val imageMultipart = if (imageFile != null && imageFile.exists()) {
+ // Log detailed file information
+ Log.d(TAG, "Image file: ${imageFile.absolutePath}")
+ Log.d(TAG, "Image file size: ${imageFile.length()} bytes")
+ Log.d(TAG, "Image file exists: ${imageFile.exists()}")
+ Log.d(TAG, "Image file can read: ${imageFile.canRead()}")
+
+ val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), imageFile)
+ MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile)
+ } else {
+ // Pass null when no image is provided
+ null
+ }
+
+ // Log request info
+ Log.d(TAG, "Sending message to store ID: $storeId, product ID: $productId")
+ Log.d(TAG, "Message content: $message")
+ Log.d(TAG, "Has image: ${imageFile != null && imageFile.exists()}")
+
+ // Make the API call
+ val response = apiService.sendChatLine(
+ storeId = storeIdBody,
+ message = messageBody,
+ productId = productIdBody,
+ chatimg = imageMultipart
+ )
+
+ if (response.isSuccessful) {
+ response.body()?.let {
+ Result.Success(it)
+ } ?: Result.Error(Exception("Send chat response is empty"))
+ } else {
+ val errorBody = response.errorBody()?.string() ?: "Unknown error"
+ Log.e(TAG, "HTTP Error: ${response.code()}, Body: $errorBody")
+ Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception sending message", e)
+ e.printStackTrace()
+ Result.Error(e)
+ }
+ }
+
+ suspend fun updateMessageStatus(
+ messageId: Int,
+ status: String
+ ): Result {
+ return try {
+ val requestBody = UpdateChatRequest(
+ id = messageId,
+ status = status
+ )
+
+ val response = apiService.updateChatStatus(requestBody)
+
+ if (response.isSuccessful) {
+ response.body()?.let {
+ Result.Success(it)
+ } ?: Result.Error(Exception("Update status response is empty"))
+ } else {
+ Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
+ }
+ } catch (e: Exception) {
+ Result.Error(e)
+ }
+ }
+
+ suspend fun getChatHistory(chatRoomId: Int): Result {
+ return try {
+ val response = apiService.getChatDetail(chatRoomId)
+
+ if (response.isSuccessful) {
+ response.body()?.let {
+ Result.Success(it)
+ } ?: Result.Error(Exception("Chat history response is empty"))
+ } else {
+ Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
+ }
+ } catch (e: Exception) {
+ Result.Error(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt
index dbf61cc..cee8bc0 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt
@@ -5,13 +5,16 @@ import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.Preorder
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
+import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
+import com.alya.ecommerce_serang.data.api.response.product.Search
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
+import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -194,6 +197,70 @@ class ProductRepository(private val apiService: ApiService) {
}
}
+ suspend fun searchProducts(query: String): Result> =
+ withContext(Dispatchers.IO) {
+ try {
+ // First save the search query
+ saveSearchQuery(query)
+
+ // Then fetch all products
+ val response = apiService.getAllProduct()
+
+ if (response.isSuccessful) {
+ val allProducts = response.body()?.products ?: emptyList()
+
+ // Filter products based on the search query
+ val filteredProducts = allProducts.filter { product ->
+ product.name.contains(query, ignoreCase = true) ||
+ (product.description?.contains(query, ignoreCase = true) ?: false)
+ }
+
+ Log.d(TAG, "Found ${filteredProducts.size} products matching '$query'")
+ Result.Success(filteredProducts)
+ } else {
+ Result.Error(Exception("Failed to fetch products for search. Code: ${response.code()}"))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error searching products", e)
+ Result.Error(e)
+ }
+ }
+
+ suspend fun saveSearchQuery(query: String): Result =
+ withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.saveSearchQuery(SearchRequest(query))
+
+ if (response.isSuccessful) {
+ Result.Success(response.body()?.search)
+ } else {
+ Log.e(TAG, "Failed to save search query. Code: ${response.code()}")
+ Result.Error(Exception("Failed to save search query"))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error saving search query", e)
+ Result.Error(e)
+ }
+ }
+
+ suspend fun getSearchHistory(): Result> =
+ withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.getSearchHistory()
+
+ if (response.isSuccessful) {
+ val searches = response.body()?.data?.map { it.searchQuery } ?: emptyList()
+ Result.Success(searches)
+ } else {
+ Log.e(TAG, "Failed to fetch search history. Code: ${response.code()}")
+ Result.Error(Exception("Failed to fetch search history"))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error fetching search history", e)
+ Result.Error(e)
+ }
+ }
+
suspend fun updateProduct(productId: Int?, updatedProduct: Map) : UpdateProductResponse {
// Build the request with the updated fields
val response = apiService.updateProduct(productId, updatedProduct)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
index 5b4c97c..dfcd46b 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
@@ -56,6 +56,102 @@ class UserRepository(private val apiService: ApiService) {
}
}
+// suspend fun sendChatMessage(
+// storeId: Int,
+// message: String,
+// productId: Int,
+// imageFile: File? = null
+// ): Result {
+// return try {
+// // Create multipart request builder
+// val requestBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
+//
+// // Add text fields
+// requestBodyBuilder.addFormDataPart("store_id", storeId.toString())
+// requestBodyBuilder.addFormDataPart("message", message)
+// requestBodyBuilder.addFormDataPart("product_id", productId.toString())
+//
+// // Add image if it exists
+// if (imageFile != null && imageFile.exists()) {
+// val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
+// requestBodyBuilder.addFormDataPart("chatimg", imageFile.name, requestFile)
+// }
+//
+// // Build the final request body
+// val requestBody = requestBodyBuilder.build()
+//
+// // Make the API call using a custom endpoint that takes a plain MultipartBody
+// val response = apiService.sendChatLineWithBody(requestBody)
+//
+// if (response.isSuccessful) {
+// response.body()?.let {
+// Result.Success(it)
+// } ?: Result.Error(Exception("Send chat response is empty"))
+// } else {
+// val errorBody = response.errorBody()?.string() ?: "Unknown error"
+// Log.e("ChatRepository", "HTTP Error: ${response.code()}, Body: $errorBody")
+// Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
+// }
+// } catch (e: Exception) {
+// Log.e("ChatRepository", "Exception sending message", e)
+// e.printStackTrace()
+// Result.Error(e)
+// }
+// }
+//
+// /**
+// * Updates the status of a message (sent, delivered, read)
+// *
+// * @param messageId The ID of the message to update
+// * @param status The new status to set
+// * @return Result containing the updated message details or error
+// */
+// suspend fun updateMessageStatus(
+// messageId: Int,
+// status: String
+// ): Result {
+// return try {
+// val requestBody = UpdateChatRequest(
+// id = messageId,
+// status = status
+// )
+//
+// val response = apiService.updateChatStatus(requestBody)
+//
+// if (response.isSuccessful) {
+// response.body()?.let {
+// Result.Success(it)
+// } ?: Result.Error(Exception("Update status response is empty"))
+// } else {
+// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
+// }
+// } catch (e: Exception) {
+// Result.Error(e)
+// }
+// }
+//
+// /**
+// * Gets the chat history for a specific chat room
+// *
+// * @param chatRoomId The ID of the chat room
+// * @return Result containing the list of chat messages or error
+// */
+// suspend fun getChatHistory(chatRoomId: Int): Result {
+// return try {
+// val response = apiService.getChatDetail(chatRoomId)
+//
+// if (response.isSuccessful) {
+// response.body()?.let {
+// Result.Success(it)
+// } ?: Result.Error(Exception("Chat history response is empty"))
+// } else {
+// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
+// }
+// } catch (e: Exception) {
+// Result.Error(e)
+// }
+// }
+
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt b/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt
new file mode 100644
index 0000000..89d5d8b
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt
@@ -0,0 +1,35 @@
+package com.alya.ecommerce_serang.di
+
+import com.alya.ecommerce_serang.data.api.retrofit.ApiService
+import com.alya.ecommerce_serang.data.repository.ChatRepository
+import com.alya.ecommerce_serang.data.repository.UserRepository
+import com.alya.ecommerce_serang.ui.chat.SocketIOService
+import com.alya.ecommerce_serang.utils.SessionManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ChatModule {
+
+ @Provides
+ @Singleton
+ fun provideChatRepository(apiService: ApiService): ChatRepository {
+ return ChatRepository(apiService)
+ }
+
+ @Provides
+ @Singleton
+ fun provideUserRepository(apiService: ApiService): UserRepository {
+ return UserRepository(apiService)
+ }
+
+ @Provides
+ @Singleton
+ fun provideSocketIOService(sessionManager: SessionManager): SocketIOService {
+ return SocketIOService(sessionManager)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt
new file mode 100644
index 0000000..5df72f4
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt
@@ -0,0 +1,82 @@
+package com.alya.ecommerce_serang.di
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.graphics.Color
+import android.os.Build
+import androidx.core.app.NotificationCompat
+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.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
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NotificationModule {
+
+ @Provides
+ @Singleton
+ fun provideContext(@ApplicationContext context: Context): Context {
+ return context
+ }
+
+ @Provides
+ @Singleton
+ fun provideSessionManager(@ApplicationContext context: Context): SessionManager {
+ return SessionManager(context)
+ }
+
+ @Provides
+ @Singleton
+ fun provideApiService(sessionManager: SessionManager): ApiService {
+ return ApiConfig.getApiService(sessionManager)
+ }
+
+ @Singleton
+ @Provides
+ fun provideNotificationBuilder(
+ @ApplicationContext context: Context
+ ): NotificationCompat.Builder {
+ // Create a unique channel ID for your app
+ val channelId = "websocket_notifications"
+
+ // Ensure the notification channel exists
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ channelId,
+ "WebSocket Notifications",
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "Notifications received via WebSocket"
+ enableLights(true)
+ lightColor = Color.BLUE
+ enableVibration(true)
+ vibrationPattern = longArrayOf(0, 1000, 500, 1000)
+ }
+
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.baseline_alarm_24)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setAutoCancel(true)
+ }
+
+ @Singleton
+ @Provides
+ fun provideNotificationManager(
+ @ApplicationContext context: Context
+ ): NotificationManagerCompat {
+ return NotificationManagerCompat.from(context)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt
index 3877327..0c93bc6 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt
@@ -1,7 +1,16 @@
package com.alya.ecommerce_serang.ui
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
@@ -9,27 +18,73 @@ 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.databinding.ActivityMainBinding
+import com.alya.ecommerce_serang.ui.notif.WebSocketManager
import com.alya.ecommerce_serang.utils.SessionManager
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
-//@AndroidEntryPoint
+@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
+// private val viewModel: NotifViewModel by viewModels()
private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
}
+
+ companion object{
+ private const val NOTIFICATION_PERMISSION_CODE = 100
+ }
+
+ @Inject
+ lateinit var webSocketManager: WebSocketManager
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
- apiService = ApiConfig.getApiService(sessionManager) // Inject SessionManager
+ apiService = ApiConfig.getApiService(sessionManager)
+
+ 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,
+ 0
+ )
+ windowInsets
+ }
+
+ requestNotificationPermissionIfNeeded()
+
+ // Start WebSocket service through WebSocketManager after permission check
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
+ webSocketManager.startWebSocketConnection()
+ }
+ } else {
+ webSocketManager.startWebSocketConnection()
+ }
setupBottomNavigation()
observeDestinationChanges()
+ }
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isFinishing) {
+ webSocketManager.stopWebSocketConnection()
+ }
}
private fun setupBottomNavigation() {
@@ -58,7 +113,40 @@ class MainActivity : AppCompatActivity() {
navController.addOnDestinationChangedListener { _, destination, _ ->
binding.bottomNavigation.isVisible = when (destination.id) {
R.id.homeFragment, R.id.chatFragment, R.id.profileFragment -> true
- else -> false // Bottom Navigation tidak terlihat di layar lain
+ else -> false
+ }
+ }
+ }
+
+ private fun requestNotificationPermissionIfNeeded() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
+ NOTIFICATION_PERMISSION_CODE
+ )
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ if (requestCode == NOTIFICATION_PERMISSION_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
+ webSocketManager.startWebSocketConnection()
+ } else {
+ Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
}
}
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
index 49e34a5..e2c4839 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
@@ -58,6 +58,7 @@ class LoginActivity : AppCompatActivity() {
val sessionManager = SessionManager(this)
sessionManager.saveToken(accessToken)
+// sessionManager.saveUserId(response.userId)
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show()
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
index ec126d1..72d1e1a 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
@@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.ui.auth
+import android.app.DatePickerDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
@@ -7,6 +8,9 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
@@ -16,6 +20,9 @@ import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
@@ -30,17 +37,45 @@ class RegisterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- sessionManager = SessionManager(this)
- if (!sessionManager.getToken().isNullOrEmpty()) {
- // User already logged in, redirect to MainActivity
- startActivity(Intent(this, MainActivity::class.java))
- finish()
- }
-
- enableEdgeToEdge()
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
+ sessionManager = SessionManager(this)
+ 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)
+
+ 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
+ }
+
// Observe OTP state
observeOtpState()
@@ -53,7 +88,7 @@ class RegisterActivity : AppCompatActivity() {
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
val name = binding.etFullname.text.toString()
- val image = "not yet"
+ val image = null
val userData = RegisterRequest(name, email, password, username, phone, birthDate, image)
@@ -94,6 +129,9 @@ class RegisterActivity : AppCompatActivity() {
startActivity(intent)
}
+ binding.etBirthDate.setOnClickListener{
+ showDatePicker()
+ }
}
private fun observeOtpState() {
@@ -140,4 +178,21 @@ class RegisterActivity : AppCompatActivity() {
}
}
}
+
+ private fun showDatePicker() {
+ val calendar = Calendar.getInstance()
+ val year = calendar.get(Calendar.YEAR)
+ val month = calendar.get(Calendar.MONTH)
+ val day = calendar.get(Calendar.DAY_OF_MONTH)
+
+ DatePickerDialog(
+ this,
+ { _, selectedYear, selectedMonth, selectedDay ->
+ calendar.set(selectedYear, selectedMonth, selectedDay)
+ val sdf = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
+ binding.etBirthDate.setText(sdf.format(calendar.time))
+ },
+ year, month, day
+ ).show()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt
new file mode 100644
index 0000000..619185e
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt
@@ -0,0 +1,443 @@
+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
+import android.os.Bundle
+import android.provider.MediaStore
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+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.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.api.retrofit.ApiService
+import com.alya.ecommerce_serang.databinding.ActivityChatBinding
+import com.alya.ecommerce_serang.ui.auth.LoginActivity
+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 java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ChatActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityChatBinding
+
+ @Inject
+ lateinit var sessionManager: SessionManager
+
+ @Inject
+ lateinit var apiService: ApiService
+
+ private lateinit var chatAdapter: ChatAdapter
+
+ 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
+
+ // 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 onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityChatBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ sessionManager = SessionManager(this)
+ apiService = ApiConfig.getApiService(sessionManager)
+
+ 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
+ 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 token = sessionManager.getToken()
+
+ 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))
+ finish()
+ return
+ }
+
+ // 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 setupRecyclerView() {
+ chatAdapter = ChatAdapter()
+ binding.recyclerChat.apply {
+ adapter = chatAdapter
+ layoutManager = LinearLayoutManager(this@ChatActivity).apply {
+ stackFromEnd = true
+ }
+ }
+ }
+
+
+ private fun setupListeners() {
+ // Back button
+ binding.btnBack.setOnClickListener {
+ onBackPressed()
+ }
+
+ // Options button
+ binding.btnOptions.setOnClickListener {
+ showOptionsMenu()
+ }
+
+ // Send button
+ binding.btnSend.setOnClickListener {
+ val message = binding.editTextMessage.text.toString().trim()
+ val currentState = viewModel.state.value
+ if (message.isNotEmpty() || (currentState != null && currentState.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() {
+ 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)
+
+ // Now we can also load chat history
+ viewModel.loadChatHistory(chatRoomId)
+ Log.d(TAG, "Chat Activity started - Chat Room: $chatRoomId")
+
+ }
+ })
+ // 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 showOptionsMenu() {
+ val options = arrayOf(
+ getString(R.string.block_user),
+ getString(R.string.report),
+ getString(R.string.clear_chat),
+ getString(R.string.cancel)
+ )
+
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.options))
+ .setItems(options) { dialog, which ->
+ when (which) {
+ 0 -> Toast.makeText(this, R.string.block_user_selected, Toast.LENGTH_SHORT).show()
+ 1 -> Toast.makeText(this, R.string.report_selected, Toast.LENGTH_SHORT).show()
+ 2 -> Toast.makeText(this, R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
+ }
+ dialog.dismiss()
+ }
+ .show()
+ }
+
+ private fun checkPermissionsAndShowImagePicker() {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ 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)
+ )
+
+ AlertDialog.Builder(this)
+ .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 = getExternalFilesDir(null)
+ val imageFile = File(storageDir, imageFileName)
+
+ tempImageUri = FileProvider.getUriForFile(
+ this,
+ "${applicationContext.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 = 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(this, R.string.image_selected, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ showImagePickerOptions()
+ } else {
+ Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ typingHandler.removeCallbacks(stopTypingRunnable)
+ }
+
+ 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)
+// }
+// }
+// }
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt
new file mode 100644
index 0000000..0e2f083
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt
@@ -0,0 +1,136 @@
+package com.alya.ecommerce_serang.ui.chat
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+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(ChatMessageDiffCallback()) {
+
+ companion object {
+ private const val VIEW_TYPE_MESSAGE_SENT = 1
+ private const val VIEW_TYPE_MESSAGE_RECEIVED = 2
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return if (viewType == VIEW_TYPE_MESSAGE_SENT) {
+ val binding = ItemMessageSentBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ SentMessageViewHolder(binding)
+ } else {
+ val binding = ItemMessageReceivedBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ ReceivedMessageViewHolder(binding)
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val message = getItem(position)
+
+ when (holder.itemViewType) {
+ VIEW_TYPE_MESSAGE_SENT -> (holder as SentMessageViewHolder).bind(message)
+ VIEW_TYPE_MESSAGE_RECEIVED -> (holder as ReceivedMessageViewHolder).bind(message)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ val message = getItem(position)
+ return if (message.isSentByMe) {
+ VIEW_TYPE_MESSAGE_SENT
+ } else {
+ VIEW_TYPE_MESSAGE_RECEIVED
+ }
+ }
+
+ /**
+ * ViewHolder for messages sent by the current user
+ */
+ inner class SentMessageViewHolder(private val binding: ItemMessageSentBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(message: ChatUiMessage) {
+ binding.tvMessage.text = message.message
+ binding.tvTimestamp.text = message.time
+
+ // Show message status
+ val statusIcon = when (message.status) {
+ Constants.STATUS_SENT -> R.drawable.check_single_24
+ Constants.STATUS_DELIVERED -> R.drawable.check_double_24
+ Constants.STATUS_READ -> R.drawable.check_double_read_24
+ else -> R.drawable.check_single_24
+ }
+ binding.imgStatus.setImageResource(statusIcon)
+
+ // Handle attachment if exists
+ if (message.attachment?.isNotEmpty() == true) {
+ binding.imgAttachment.visibility = View.VISIBLE
+ Glide.with(binding.root.context)
+ .load(BASE_URL + message.attachment)
+ .centerCrop()
+ .placeholder(R.drawable.placeholder_image)
+ .error(R.drawable.placeholder_image)
+ .into(binding.imgAttachment)
+ } else {
+ binding.imgAttachment.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * ViewHolder for messages received from other users
+ */
+ inner class ReceivedMessageViewHolder(private val binding: ItemMessageReceivedBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(message: ChatUiMessage) {
+ binding.tvMessage.text = message.message
+ binding.tvTimestamp.text = message.time
+
+ // Handle attachment if exists
+ if (message.attachment?.isNotEmpty() == true) {
+ binding.imgAttachment.visibility = View.VISIBLE
+ Glide.with(binding.root.context)
+ .load(BASE_URL + message.attachment)
+ .centerCrop()
+ .placeholder(R.drawable.placeholder_image)
+ .error(R.drawable.placeholder_image)
+ .into(binding.imgAttachment)
+ } else {
+ binding.imgAttachment.visibility = View.GONE
+ }
+
+ // Load avatar image
+ Glide.with(binding.root.context)
+ .load(R.drawable.placeholder_image) // Replace with actual avatar URL if available
+ .circleCrop()
+ .into(binding.imgAvatar)
+ }
+ }
+}
+
+/**
+ * DiffUtil callback for optimizing RecyclerView updates
+ */
+class ChatMessageDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
+ return oldItem == newItem
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt
index 79c13a4..4bd12ae 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt
@@ -1,32 +1,337 @@
-package com.alya.ecommerce_serang.ui.chat
-
-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
-
-class ChatFragment : Fragment() {
-
- companion object {
- fun newInstance() = ChatFragment()
- }
-
- private val viewModel: ChatViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // TODO: Use the ViewModel
- }
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return inflater.inflate(R.layout.fragment_chat, container, false)
- }
-}
\ No newline at end of file
+//package com.alya.ecommerce_serang.ui.chat
+//
+//import android.Manifest
+//import android.app.Activity
+//import android.content.Intent
+//import android.content.pm.PackageManager
+//import android.net.Uri
+//import android.os.Bundle
+//import android.provider.MediaStore
+//import android.text.Editable
+//import android.text.TextWatcher
+//import androidx.fragment.app.Fragment
+//import android.view.LayoutInflater
+//import android.view.View
+//import android.view.ViewGroup
+//import android.widget.Toast
+//import androidx.activity.result.contract.ActivityResultContracts
+//import androidx.core.app.ActivityCompat
+//import androidx.core.content.ContextCompat
+//import androidx.core.content.FileProvider
+//import androidx.fragment.app.viewModels
+//import androidx.lifecycle.lifecycleScope
+//import androidx.navigation.fragment.navArgs
+//import androidx.recyclerview.widget.LinearLayoutManager
+//import com.alya.ecommerce_serang.BuildConfig.BASE_URL
+//import com.alya.ecommerce_serang.R
+//import com.alya.ecommerce_serang.databinding.FragmentChatBinding
+//import com.alya.ecommerce_serang.utils.Constants
+//import com.bumptech.glide.Glide
+//import dagger.hilt.android.AndroidEntryPoint
+//import kotlinx.coroutines.launch
+//import java.io.File
+//import java.text.SimpleDateFormat
+//import java.util.Locale
+//
+//@AndroidEntryPoint
+//class ChatFragment : Fragment() {
+//
+// private var _binding: FragmentChatBinding? = null
+// private val binding get() = _binding!!
+//
+// private val viewModel: ChatViewModel by viewModels()
+//// private val args: ChatFragmentArgs by navArgs()
+//
+// private lateinit var chatAdapter: ChatAdapter
+//
+// // For image attachment
+// private var tempImageUri: Uri? = null
+//
+// // Typing indicator handler
+// private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
+// private val stopTypingRunnable = Runnable {
+// viewModel.sendTypingStatus(false)
+// }
+//
+// // Activity Result Launchers
+// private val pickImageLauncher = registerForActivityResult(
+// ActivityResultContracts.StartActivityForResult()
+// ) { result ->
+// if (result.resultCode == Activity.RESULT_OK) {
+// result.data?.data?.let { uri ->
+// handleSelectedImage(uri)
+// }
+// }
+// }
+//
+// private val takePictureLauncher = registerForActivityResult(
+// ActivityResultContracts.StartActivityForResult()
+// ) { result ->
+// if (result.resultCode == Activity.RESULT_OK) {
+// tempImageUri?.let { uri ->
+// handleSelectedImage(uri)
+// }
+// }
+// }
+//
+// override fun onCreateView(
+// inflater: LayoutInflater,
+// container: ViewGroup?,
+// savedInstanceState: Bundle?
+// ): View {
+// _binding = FragmentChatBinding.inflate(inflater, container, false)
+// return binding.root
+// }
+//
+// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+// super.onViewCreated(view, savedInstanceState)
+//
+// setupRecyclerView()
+// setupListeners()
+// setupTypingIndicator()
+// observeViewModel()
+// }
+//
+// private fun setupRecyclerView() {
+// chatAdapter = ChatAdapter()
+// binding.recyclerChat.apply {
+// adapter = chatAdapter
+// layoutManager = LinearLayoutManager(requireContext()).apply {
+// stackFromEnd = true
+// }
+// }
+// }
+//
+// private fun setupListeners() {
+// // Back button
+// binding.btnBack.setOnClickListener {
+// requireActivity().onBackPressed()
+// }
+//
+// // Options button
+// binding.btnOptions.setOnClickListener {
+// showOptionsMenu()
+// }
+//
+// // Send button
+// binding.btnSend.setOnClickListener {
+// val message = binding.editTextMessage.text.toString().trim()
+// if (message.isNotEmpty() || viewModel.state.value.hasAttachment) {
+// viewModel.sendMessage(message)
+// binding.editTextMessage.text.clear()
+// }
+// }
+//
+// // Attachment button
+// binding.btnAttachment.setOnClickListener {
+// checkPermissionsAndShowImagePicker()
+// }
+// }
+//
+// private fun setupTypingIndicator() {
+// binding.editTextMessage.addTextChangedListener(object : TextWatcher {
+// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+//
+// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+// viewModel.sendTypingStatus(true)
+//
+// // Reset the timer
+// typingHandler.removeCallbacks(stopTypingRunnable)
+// typingHandler.postDelayed(stopTypingRunnable, 1000)
+// }
+//
+// override fun afterTextChanged(s: Editable?) {}
+// })
+// }
+//
+// private fun observeViewModel() {
+// viewLifecycleOwner.lifecycleScope.launch {
+// viewModel.state.collectLatest { state ->
+// // Update messages
+// chatAdapter.submitList(state.messages)
+//
+// // Scroll to bottom if new message
+// if (state.messages.isNotEmpty()) {
+// binding.recyclerChat.scrollToPosition(state.messages.size - 1)
+// }
+//
+// // Update product info
+// binding.tvProductName.text = state.productName
+// binding.tvProductPrice.text = state.productPrice
+// binding.ratingBar.rating = state.productRating
+// binding.tvRating.text = state.productRating.toString()
+// binding.tvSellerName.text = state.storeName
+//
+// // Load product image
+// if (state.productImageUrl.isNotEmpty()) {
+// Glide.with(requireContext())
+// .load(BASE_URL + state.productImageUrl)
+// .centerCrop()
+// .placeholder(R.drawable.placeholder_image)
+// .error(R.drawable.placeholder_image)
+// .into(binding.imgProduct)
+// }
+//
+// // Show/hide loading indicators
+// binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
+// binding.btnSend.isEnabled = !state.isSending
+//
+// // Update attachment hint
+// if (state.hasAttachment) {
+// binding.editTextMessage.hint = getString(R.string.image_attached)
+// } else {
+// binding.editTextMessage.hint = getString(R.string.write_message)
+// }
+//
+// // Show typing indicator
+// binding.tvTypingIndicator.visibility =
+// if (state.isOtherUserTyping) View.VISIBLE else View.GONE
+//
+// // Handle connection state
+// handleConnectionState(state.connectionState)
+//
+// // Show error if any
+// state.error?.let { error ->
+// Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
+// viewModel.clearError()
+// }
+// }
+// }
+// }
+//
+// private fun handleConnectionState(state: ConnectionState) {
+// when (state) {
+// is ConnectionState.Connected -> {
+// binding.tvConnectionStatus.visibility = View.GONE
+// }
+// is ConnectionState.Connecting -> {
+// binding.tvConnectionStatus.visibility = View.VISIBLE
+// binding.tvConnectionStatus.text = getString(R.string.connecting)
+// }
+// is ConnectionState.Disconnected -> {
+// binding.tvConnectionStatus.visibility = View.VISIBLE
+// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
+// }
+// is ConnectionState.Error -> {
+// binding.tvConnectionStatus.visibility = View.VISIBLE
+// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
+// }
+// }
+// }
+//
+// private fun showOptionsMenu() {
+// val options = arrayOf(
+// getString(R.string.block_user),
+// getString(R.string.report),
+// getString(R.string.clear_chat),
+// getString(R.string.cancel)
+// )
+//
+// androidx.appcompat.app.AlertDialog.Builder(requireContext())
+// .setTitle(getString(R.string.options))
+// .setItems(options) { dialog, which ->
+// when (which) {
+// 0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show()
+// 1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show()
+// 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
+// }
+// dialog.dismiss()
+// }
+// .show()
+// }
+//
+// private fun checkPermissionsAndShowImagePicker() {
+// if (ContextCompat.checkSelfPermission(
+// requireContext(),
+// Manifest.permission.READ_EXTERNAL_STORAGE
+// ) != PackageManager.PERMISSION_GRANTED
+// ) {
+// ActivityCompat.requestPermissions(
+// requireActivity(),
+// arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA),
+// Constants.REQUEST_STORAGE_PERMISSION
+// )
+// } else {
+// showImagePickerOptions()
+// }
+// }
+//
+// private fun showImagePickerOptions() {
+// val options = arrayOf(
+// getString(R.string.take_photo),
+// getString(R.string.choose_from_gallery),
+// getString(R.string.cancel)
+// )
+//
+// androidx.appcompat.app.AlertDialog.Builder(requireContext())
+// .setTitle(getString(R.string.select_attachment))
+// .setItems(options) { dialog, which ->
+// when (which) {
+// 0 -> openCamera()
+// 1 -> openGallery()
+// }
+// dialog.dismiss()
+// }
+// .show()
+// }
+//
+// private fun openCamera() {
+// val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+// val imageFileName = "IMG_${timeStamp}.jpg"
+// val storageDir = requireContext().getExternalFilesDir(null)
+// val imageFile = File(storageDir, imageFileName)
+//
+// tempImageUri = FileProvider.getUriForFile(
+// requireContext(),
+// "${requireContext().packageName}.fileprovider",
+// imageFile
+// )
+//
+// val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
+// putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri)
+// }
+//
+// takePictureLauncher.launch(intent)
+// }
+//
+// private fun openGallery() {
+// val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+// pickImageLauncher.launch(intent)
+// }
+//
+// private fun handleSelectedImage(uri: Uri) {
+// // Get the file from Uri
+// val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
+// val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null)
+// cursor?.moveToFirst()
+// val columnIndex = cursor?.getColumnIndex(filePathColumn[0])
+// val filePath = cursor?.getString(columnIndex ?: 0)
+// cursor?.close()
+//
+// if (filePath != null) {
+// viewModel.setSelectedImageFile(File(filePath))
+// Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show()
+// }
+// }
+//
+// override fun onRequestPermissionsResult(
+// requestCode: Int,
+// permissions: Array,
+// grantResults: IntArray
+// ) {
+// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
+// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+// showImagePickerOptions()
+// } else {
+// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
+// }
+// }
+// }
+//
+// override fun onDestroyView() {
+// super.onDestroyView()
+// typingHandler.removeCallbacks(stopTypingRunnable)
+// _binding = null
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt
new file mode 100644
index 0000000..38979d2
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt
@@ -0,0 +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.data.api.retrofit.ApiConfig
+import com.alya.ecommerce_serang.data.repository.ChatRepository
+import com.alya.ecommerce_serang.databinding.FragmentChatListBinding
+import com.alya.ecommerce_serang.utils.BaseViewModelFactory
+import com.alya.ecommerce_serang.utils.SessionManager
+
+class ChatListFragment : Fragment() {
+
+ private var _binding: FragmentChatListBinding? = null
+
+ private val binding get() = _binding!!
+ private lateinit var socketService: SocketIOService
+ private lateinit var sessionManager: SessionManager
+ private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
+ BaseViewModelFactory {
+ val apiService = ApiConfig.getApiService(sessionManager)
+ val chatRepository = ChatRepository(apiService)
+ ChatViewModel(chatRepository, socketService, sessionManager)
+ }
+ }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ sessionManager = SessionManager(requireContext())
+
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentChatListBinding.inflate(inflater, container, false)
+ return _binding!!.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupView()
+ }
+
+ private fun setupView(){
+ binding.btnTrial.setOnClickListener{
+ val intent = Intent(requireContext(), ChatActivity::class.java)
+ startActivity(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt
new file mode 100644
index 0000000..1ebaca8
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt
@@ -0,0 +1,463 @@
+package com.alya.ecommerce_serang.ui.chat
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.alya.ecommerce_serang.data.api.response.chat.ChatItem
+import com.alya.ecommerce_serang.data.api.response.chat.ChatLine
+import com.alya.ecommerce_serang.data.repository.ChatRepository
+import com.alya.ecommerce_serang.data.repository.Result
+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: ChatRepository,
+ private val socketService: SocketIOService,
+ private val sessionManager: SessionManager
+) : ViewModel() {
+
+ private val TAG = "ChatViewModel"
+
+ // UI state using LiveData
+ private val _state = MutableLiveData(ChatUiState())
+ val state: LiveData = _state
+
+ private val _chatRoomId = MutableLiveData(0)
+ val chatRoomId: LiveData = _chatRoomId
+
+ // Store and product parameters
+ private var storeId: Int = 0
+ private var productId: Int = 0
+ private var currentUserId: Int? = null
+ private var defaultUserId: Int = 0
+
+ // Product details for display
+ private var productName: String = ""
+ private var productPrice: String = ""
+ private var productImage: String = ""
+ private var productRating: Float = 0f
+ private var storeName: String = ""
+
+ // For image attachment
+ private var selectedImageFile: File? = null
+
+ init {
+ // Try to get current user ID from the repository
+ viewModelScope.launch {
+ when (val result = chatRepository.fetchUserProfile()) {
+ is Result.Success -> {
+ currentUserId = result.data?.userId
+ Log.e(TAG, "User ID: $currentUserId")
+
+ // Move the validation and subsequent logic inside the coroutine
+ if (currentUserId == null || 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
+ }
+ }
+ }
+ }
+
+ /**
+ * Set chat parameters received from activity
+ */
+ fun setChatParameters(
+ storeId: Int,
+ productId: Int,
+ productName: String,
+ productPrice: String,
+ productImage: String,
+ productRating: Float,
+ storeName: String
+ ) {
+ this.storeId = storeId
+ this.productId = productId
+ this.productName = productName
+ this.productPrice = productPrice
+ this.productImage = productImage
+ this.productRating = productRating
+ this.storeName = storeName
+
+ // Update state with product info
+ updateState {
+ it.copy(
+ productName = productName,
+ productPrice = productPrice,
+ productImageUrl = productImage,
+ productRating = productRating,
+ storeName = storeName
+ )
+ }
+
+ // Connect to socket and load chat history
+ 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()
+ }
+
+ /**
+ * Sets up listeners for Socket.IO events
+ */
+ private fun setupSocketListeners() {
+ viewModelScope.launch {
+ // Listen for connection state changes
+ socketService.connectionState.collect { connectionState ->
+ updateState { it.copy(connectionState = connectionState) }
+
+ // Join room when connected
+ if (connectionState is ConnectionState.Connected) {
+ socketService.joinRoom()
+ }
+ }
+ }
+
+ viewModelScope.launch {
+ // Listen for new messages
+ socketService.newMessages.collect { chatLine ->
+ chatLine?.let {
+ val currentMessages = _state.value?.messages ?: listOf()
+ val updatedMessages = currentMessages.toMutableList().apply {
+ add(convertChatLineToUiMessage(it))
+ }
+ updateState { it.copy(messages = updatedMessages) }
+
+ // Update message status if received from others
+ if (it.senderId != currentUserId) {
+ updateMessageStatus(it.id, Constants.STATUS_READ)
+ }
+ }
+ }
+ }
+
+ viewModelScope.launch {
+ // Listen for typing status updates
+ socketService.typingStatus.collect { typingStatus ->
+ typingStatus?.let {
+ if (typingStatus.roomId == (_chatRoomId.value ?: 0) && typingStatus.userId != currentUserId) {
+ updateState { it.copy(isOtherUserTyping = typingStatus.isTyping) }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function to update LiveData state
+ */
+ private fun updateState(update: (ChatUiState) -> ChatUiState) {
+ _state.value?.let {
+ _state.value = update(it)
+ }
+ }
+
+ /**
+ * Loads chat history
+ */
+ fun loadChatHistory(chatRoomId: Int) {
+ if (chatRoomId <= 0) {
+ Log.e(TAG, "Cannot load chat history: Chat room ID is 0")
+ return
+ }
+
+ viewModelScope.launch {
+ updateState { it.copy(isLoading = true) }
+
+ when (val result = chatRepository.getChatHistory(chatRoomId)) {
+ is Result.Success -> {
+ val messages = result.data.chat.map { chatLine ->
+ convertChatLineToUiMessageHistory(chatLine)
+ }
+
+ updateState {
+ it.copy(
+ messages = messages,
+ isLoading = false,
+ error = null
+ )
+ }
+
+ Log.d(TAG, "Loaded ${messages.size} messages for chat room $chatRoomId")
+
+ // Update status of unread messages
+ result.data.chat
+ .filter { it.senderId != currentUserId && it.status != Constants.STATUS_READ }
+ .forEach { updateMessageStatus(it.id, Constants.STATUS_READ) }
+ }
+ is Result.Error -> {
+ updateState {
+ it.copy(
+ isLoading = false,
+ error = result.exception.message
+ )
+ }
+ Log.e(TAG, "Error loading chat history: ${result.exception.message}")
+ }
+ is Result.Loading -> {
+ updateState { it.copy(isLoading = true) }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends a chat message
+ */
+ fun sendMessage(message: String) {
+ if (message.isBlank()) return
+
+ if (storeId == 0 || productId == 0) {
+ Log.e(TAG, "Cannot send message: Store ID or Product ID is 0")
+ updateState { it.copy(error = "Cannot send message. Invalid parameters.") }
+ return
+ }
+
+ viewModelScope.launch {
+ updateState { it.copy(isSending = true) }
+
+ when (val result = chatRepository.sendChatMessage(
+ storeId = storeId,
+ message = message,
+ productId = productId,
+ imageFile = selectedImageFile
+ )) {
+ is Result.Success -> {
+ // Add new message to the list
+ val chatLine = result.data.chatLine
+ val newMessage = convertChatLineToUiMessage(chatLine)
+
+ val currentMessages = _state.value?.messages ?: listOf()
+ val updatedMessages = currentMessages.toMutableList().apply {
+ add(newMessage)
+ }
+
+ updateState {
+ it.copy(
+ messages = updatedMessages,
+ isSending = false,
+ hasAttachment = false,
+ error = null
+ )
+ }
+
+ 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)
+
+ // Clear the image attachment
+ selectedImageFile = null
+ }
+ is Result.Error -> {
+ val errorMsg = if (result.exception.message.isNullOrEmpty() || result.exception.message == "{}") {
+ "Failed to send message. Please try again."
+ } else {
+ result.exception.message
+ }
+
+ updateState {
+ it.copy(
+ isSending = false,
+ error = errorMsg
+ )
+ }
+ Log.e(TAG, "Error sending message: ${result.exception.message}")
+ }
+ is Result.Loading -> {
+ updateState { it.copy(isSending = true) }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates a message status (delivered, read)
+ */
+ fun updateMessageStatus(messageId: Int, status: String) {
+ viewModelScope.launch {
+ try {
+ val result = chatRepository.updateMessageStatus(messageId, status)
+
+ if (result is Result.Success) {
+ // Update local message status
+ val currentMessages = _state.value?.messages ?: listOf()
+ val updatedMessages = currentMessages.map { message ->
+ if (message.id == messageId) {
+ message.copy(status = status)
+ } else {
+ message
+ }
+ }
+ updateState { it.copy(messages = updatedMessages) }
+
+ Log.d(TAG, "Message status updated: $messageId -> $status")
+ } else if (result is Result.Error) {
+ Log.e(TAG, "Error updating message status: ${result.exception.message}")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception updating message status", e)
+ }
+ }
+ }
+
+ /**
+ * Sets the selected image file for attachment
+ */
+ fun setSelectedImageFile(file: File?) {
+ selectedImageFile = file
+ updateState { it.copy(hasAttachment = file != null) }
+
+ Log.d(TAG, "Image attachment ${if (file != null) "selected" else "cleared"}")
+ }
+
+ /**
+ * Sends typing status to the other user
+ */
+ fun sendTypingStatus(isTyping: Boolean) {
+ val roomId = _chatRoomId.value ?: 0
+ if (roomId <= 0) return
+
+ socketService.sendTypingStatus(roomId, isTyping)
+ }
+
+ /**
+ * Clears any error message in the state
+ */
+ fun clearError() {
+ updateState { it.copy(error = null) }
+ }
+
+ /**
+ * Converts a ChatLine from API to a UI message model
+ */
+ private fun convertChatLineToUiMessage(chatLine: ChatLine): ChatUiMessage {
+ // Format the timestamp for display
+ val formattedTime = try {
+ val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ inputFormat.timeZone = TimeZone.getTimeZone("UTC")
+ val outputFormat = java.text.SimpleDateFormat("HH:mm", Locale.getDefault())
+
+ val date = inputFormat.parse(chatLine.createdAt)
+ date?.let { outputFormat.format(it) } ?: ""
+ } catch (e: Exception) {
+ Log.e(TAG, "Error formatting date: ${chatLine.createdAt}", e)
+ ""
+ }
+
+ return ChatUiMessage(
+ id = chatLine.id,
+ message = chatLine.message,
+ attachment = chatLine.attachment ?: "", // Handle null attachment
+ status = chatLine.status,
+ time = formattedTime,
+ isSentByMe = chatLine.senderId == currentUserId
+ )
+ }
+
+ private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
+ // Format the timestamp for display
+ val formattedTime = try {
+ val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ inputFormat.timeZone = TimeZone.getTimeZone("UTC")
+ val outputFormat = java.text.SimpleDateFormat("HH:mm", Locale.getDefault())
+
+ val date = inputFormat.parse(chatItem.createdAt)
+ date?.let { outputFormat.format(it) } ?: ""
+ } catch (e: Exception) {
+ Log.e(TAG, "Error formatting date: ${chatItem.createdAt}", e)
+ ""
+ }
+
+ return ChatUiMessage(
+ attachment = chatItem.attachment, // Handle null attachment
+ id = chatItem.id,
+ message = chatItem.message,
+ status = chatItem.status,
+ time = formattedTime,
+ isSentByMe = chatItem.senderId == currentUserId,
+ )
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ // Disconnect Socket.IO when ViewModel is cleared
+ socketService.disconnect()
+ Log.d(TAG, "ViewModel cleared, Socket.IO disconnected")
+ }
+}
+
+/**
+ * Data class representing the UI state for the chat screen
+ */
+data class ChatUiState(
+ val messages: List = emptyList(),
+ val isLoading: Boolean = false,
+ val isSending: Boolean = false,
+ val hasAttachment: Boolean = false,
+ val isOtherUserTyping: Boolean = false,
+ val error: String? = null,
+ val connectionState: ConnectionState = ConnectionState.Disconnected(),
+
+ // Product info
+ val productName: String = "",
+ val productPrice: String = "",
+ val productImageUrl: String = "",
+ val productRating: Float = 0f,
+ val storeName: String = ""
+)
+
+/**
+ * Data class representing a chat message in the UI
+ */
+data class ChatUiMessage(
+ val id: Int,
+ val message: String,
+ val attachment: String?,
+ val status: String,
+ val time: String,
+ val isSentByMe: Boolean
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt
new file mode 100644
index 0000000..1ac378f
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt
@@ -0,0 +1,252 @@
+package com.alya.ecommerce_serang.ui.chat
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.alya.ecommerce_serang.BuildConfig
+import com.alya.ecommerce_serang.data.api.response.chat.ChatLine
+import com.alya.ecommerce_serang.utils.Constants
+import com.alya.ecommerce_serang.utils.SessionManager
+import com.google.gson.Gson
+import io.socket.client.IO
+import io.socket.client.Socket
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.json.JSONObject
+import java.net.URISyntaxException
+
+class SocketIOService(
+ private val sessionManager: SessionManager
+) {
+ private val TAG = "SocketIOService"
+
+ // Socket.IO client
+ private var socket: Socket? = null
+
+ // Connection state
+ private var isConnected = false
+
+ // StateFlows for internal observing (these are needed for suspend functions in ViewModel)
+ private val _connectionState = MutableStateFlow(ConnectionState.Disconnected())
+ val connectionState: StateFlow = _connectionState
+
+ private val _newMessages = MutableStateFlow(null)
+ val newMessages: StateFlow = _newMessages
+
+ private val _typingStatus = MutableStateFlow(null)
+ val typingStatus: StateFlow = _typingStatus
+
+ // LiveData for Activity/Fragment observing
+ private val _connectionStateLiveData = MutableLiveData(ConnectionState.Disconnected())
+ val connectionStateLiveData: LiveData = _connectionStateLiveData
+
+ private val _newMessagesLiveData = MutableLiveData()
+ val newMessagesLiveData: LiveData = _newMessagesLiveData
+
+ private val _typingStatusLiveData = MutableLiveData()
+ val typingStatusLiveData: LiveData = _typingStatusLiveData
+
+ /**
+ * Initializes the Socket.IO client
+ */
+ init {
+ try {
+ // Get token from SessionManager
+ val token = sessionManager.getToken()
+
+ // Set up Socket.IO options with auth token
+ val options = IO.Options().apply {
+ forceNew = true
+ reconnection = true
+ reconnectionAttempts = 5
+ reconnectionDelay = 3000
+
+ // Add auth information
+ if (!token.isNullOrEmpty()) {
+ auth = mapOf("token" to token)
+ }
+ }
+
+ // Create Socket.IO client
+ socket = IO.socket(BuildConfig.BASE_URL, options)
+
+ // Set up event listeners
+ setupSocketListeners()
+
+ Log.d(TAG, "Socket.IO initialized with token: $token")
+ } catch (e: URISyntaxException) {
+ Log.e(TAG, "Error initializing Socket.IO client", e)
+ _connectionState.value = ConnectionState.Error("Error initializing Socket.IO: ${e.message}")
+ _connectionStateLiveData.value = ConnectionState.Error("Error initializing Socket.IO: ${e.message}")
+ }
+ }
+
+ /**
+ * Sets up Socket.IO event listeners
+ */
+ private fun setupSocketListeners() {
+ socket?.let { socket ->
+ // Connection events
+ socket.on(Socket.EVENT_CONNECT) {
+ Log.d(TAG, "Socket.IO connected")
+ isConnected = true
+ _connectionState.value = ConnectionState.Connected
+ _connectionStateLiveData.postValue(ConnectionState.Connected)
+ }
+
+ socket.on(Socket.EVENT_DISCONNECT) {
+ Log.d(TAG, "Socket.IO disconnected")
+ isConnected = false
+ _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
+ _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
+ }
+
+ socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
+ val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
+ Log.e(TAG, "Socket.IO connection error: $error")
+ isConnected = false
+ _connectionState.value = ConnectionState.Error("Connection error: $error")
+ _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
+ }
+
+ // Chat events
+ socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
+ try {
+ if (args.isNotEmpty() && args[0] != null) {
+ val messageJson = args[0].toString()
+ Log.d(TAG, "Received new message: $messageJson")
+ val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
+ _newMessages.value = chatLine
+ _newMessagesLiveData.postValue(chatLine)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error parsing new message event", e)
+ }
+ }
+
+ socket.on(Constants.EVENT_TYPING) { args ->
+ try {
+ if (args.isNotEmpty() && args[0] != null) {
+ val typingData = args[0] as JSONObject
+ val userId = typingData.getInt("userId")
+ val roomId = typingData.getInt("roomId")
+ val isTyping = typingData.getBoolean("isTyping")
+
+ Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
+ val status = TypingStatus(userId, roomId, isTyping)
+ _typingStatus.value = status
+ _typingStatusLiveData.postValue(status)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error parsing typing event", e)
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects to the Socket.IO server
+ */
+ fun connect() {
+ if (isConnected) return
+
+ Log.d(TAG, "Connecting to Socket.IO server...")
+ _connectionState.value = ConnectionState.Connecting
+ _connectionStateLiveData.value = ConnectionState.Connecting
+ socket?.connect()
+ }
+
+ /**
+ * Joins a specific chat room
+ */
+ fun joinRoom() {
+ if (!isConnected) {
+ connect()
+ return
+ }
+
+ // Get user ID from SessionManager
+ val userId = sessionManager.getUserId()
+ if (userId.isNullOrEmpty()) {
+ Log.e(TAG, "Cannot join room: User ID is null or empty")
+ return
+ }
+
+ // Join the room using the current user's ID
+ socket?.emit("joinRoom", userId)
+ Log.d(TAG, "Joined room for user: $userId")
+ }
+
+ /**
+ * Emits a new message event
+ */
+ fun sendMessage(message: ChatLine) {
+ if (!isConnected) {
+ connect()
+ return
+ }
+
+ val messageJson = Gson().toJson(message)
+ socket?.emit(Constants.EVENT_NEW_MESSAGE, messageJson)
+ Log.d(TAG, "Sent message via Socket.IO: $messageJson")
+ }
+
+ /**
+ * Sends typing status update
+ */
+ fun sendTypingStatus(roomId: Int, isTyping: Boolean) {
+ if (!isConnected) return
+
+ // Get user ID from SessionManager
+ val userId = sessionManager.getUserId()?.toIntOrNull()
+ if (userId == null) {
+ Log.e(TAG, "Cannot send typing status: User ID is null or invalid")
+ return
+ }
+
+ val typingData = JSONObject().apply {
+ put("userId", userId)
+ put("roomId", roomId)
+ put("isTyping", isTyping)
+ }
+
+ socket?.emit(Constants.EVENT_TYPING, typingData)
+ Log.d(TAG, "Sent typing status: User $userId in room $roomId is typing: $isTyping")
+ }
+
+ /**
+ * Disconnects from the Socket.IO server
+ */
+ fun disconnect() {
+ Log.d(TAG, "Disconnecting from Socket.IO server...")
+ socket?.disconnect()
+ isConnected = false
+ _connectionState.value = ConnectionState.Disconnected("Disconnected by user")
+ _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected by user"))
+ }
+
+ /**
+ * Returns whether the socket is connected
+ */
+ val isSocketConnected: Boolean
+ get() = isConnected
+}
+
+/**
+ * Sealed class representing connection states
+ */
+sealed class ConnectionState {
+ object Connecting : ConnectionState()
+ object Connected : ConnectionState()
+ data class Disconnected(val reason: String = "") : ConnectionState()
+ data class Error(val message: String) : ConnectionState()
+}
+
+/**
+ * Data class for typing status events
+ */
+data class TypingStatus(
+ val userId: Int,
+ val roomId: Int,
+ val isTyping: Boolean
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt
index c0dd046..c19d02e 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt
@@ -6,12 +6,14 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
@@ -19,6 +21,7 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
+import com.alya.ecommerce_serang.ui.notif.NotificationActivity
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
@@ -47,6 +50,7 @@ class HomeFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
+
}
override fun onCreateView(
@@ -60,9 +64,12 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+
initUi()
setupRecyclerView()
observeData()
+ setupSearchView()
+
}
private fun setupRecyclerView() {
@@ -95,6 +102,41 @@ class HomeFragment : Fragment() {
}
}
+ private fun setupSearchView() {
+ binding.searchContainer.search.apply {
+ // When user clicks the search box, navigate to search fragment
+ setOnClickListener {
+ findNavController().navigate(
+ HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(null)
+ )
+ }
+
+// Handle search action if user presses search on keyboard
+ setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ val query = text.toString().trim()
+ if (query.isNotEmpty()) {
+ findNavController().navigate(
+ HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(query)
+ )
+ }
+ return@setOnEditorActionListener true
+ }
+ false
+ }
+ }
+
+ // Setup cart and notification buttons
+ binding.searchContainer.btnCart.setOnClickListener {
+ // Navigate to cart
+ }
+
+ binding.searchContainer.btnNotification.setOnClickListener {
+ val intent = Intent(requireContext(), NotificationActivity::class.java)
+ startActivity(intent)
+ }
+ }
+
private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -109,7 +151,7 @@ class HomeFragment : Fragment() {
binding.loading.root.isVisible = false
binding.error.root.isVisible = false
binding.home.isVisible = true
- productAdapter?.updateLimitedProducts(state.products) // Ensure productAdapter is initialized
+ productAdapter?.updateLimitedProducts(state.products)
}
is HomeUiState.Error -> {
binding.loading.root.isVisible = false
@@ -125,18 +167,16 @@ class HomeFragment : Fragment() {
}
}
- viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.categories.collect { categories ->
Log.d("Categories", "Updated Categories: $categories")
- categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") }
categoryAdapter?.updateLimitedCategory(categories)
}
}
}
}
-
private fun initUi() {
// For LightStatusBar
setLightStatusBar()
@@ -161,7 +201,6 @@ class HomeFragment : Fragment() {
)
}
-
private fun handleProductClick(product: ProductsItem) {
val intent = Intent(requireContext(), DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
@@ -169,7 +208,7 @@ class HomeFragment : Fragment() {
}
private fun handleCategoryProduct(category: CategoryItem) {
-
+ // Your implementation
}
override fun onDestroyView() {
@@ -179,7 +218,7 @@ class HomeFragment : Fragment() {
_binding = null
}
- private fun showLoading(isLoading: Boolean) {
- binding.progressBar.isVisible = isLoading
- }
+// private fun showLoading(isLoading: Boolean) {
+// binding.progressBar.isVisible = isLoading
+// }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt
new file mode 100644
index 0000000..a00b3d7
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt
@@ -0,0 +1,56 @@
+package com.alya.ecommerce_serang.ui.home
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.alya.ecommerce_serang.databinding.ItemRecentSearchBinding
+
+class SearchHistoryAdapter(
+ private val onItemClick: (String) -> Unit
+) : ListAdapter(DIFF_CALLBACK) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = ItemRecentSearchBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val query = getItem(position)
+ holder.bind(query)
+ }
+
+ inner class ViewHolder(private val binding: ItemRecentSearchBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ init {
+ binding.root.setOnClickListener {
+ val position = adapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ onItemClick(getItem(position))
+ }
+ }
+ }
+
+ fun bind(query: String) {
+ binding.recentSearchText.text = query
+ }
+ }
+
+ companion object {
+ private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt
new file mode 100644
index 0000000..94003c6
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt
@@ -0,0 +1,162 @@
+package com.alya.ecommerce_serang.ui.home
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.GridLayoutManager
+import com.alya.ecommerce_serang.data.api.dto.ProductsItem
+import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
+import com.alya.ecommerce_serang.data.repository.ProductRepository
+import com.alya.ecommerce_serang.databinding.FragmentSearchHomeBinding
+import com.alya.ecommerce_serang.ui.product.DetailProductActivity
+import com.alya.ecommerce_serang.utils.BaseViewModelFactory
+import com.alya.ecommerce_serang.utils.SessionManager
+
+class SearchHomeFragment : Fragment() {
+ private var _binding: FragmentSearchHomeBinding? = null
+ private val binding get() = _binding!!
+ private var searchResultsAdapter: SearchResultsAdapter? = null
+ private lateinit var sessionManager: SessionManager
+ private val args: SearchHomeFragmentArgs by navArgs()
+
+ private val viewModel: SearchHomeViewModel by viewModels {
+ BaseViewModelFactory {
+ val apiService = ApiConfig.getApiService(sessionManager)
+ val productRepository = ProductRepository(apiService)
+ SearchHomeViewModel(productRepository)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ sessionManager = SessionManager(requireContext())
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSearchHomeBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupUI()
+ setupSearchResultsRecyclerView()
+ observeData()
+
+ // Perform search with the query passed from HomeFragment
+ args.query?.let { query ->
+ // Wait until layout is done, then set query text
+ binding.searchView.post {
+ binding.searchView.setQuery(query, false) // sets "food" as text, doesn't submit
+ }
+
+ viewModel.searchProducts(query)
+ }
+ }
+
+ private fun setupUI() {
+ // Setup back button
+ binding.backButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+
+ // Setup search view
+ binding.searchView.apply {
+ setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ query?.let {
+ if (it.isNotEmpty()) {
+ viewModel.searchProducts(it)
+ hideKeyboard()
+ }
+ }
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ newText?.let {
+ if (it.isEmpty()) {
+ // Clear the search results if user clears the input
+ searchResultsAdapter?.submitList(emptyList())
+ binding.noResultsText.isVisible = false
+ return true
+ }
+
+ // Optional: do real-time search
+ if (it.length >= 2) {
+ viewModel.searchProducts(it)
+ }
+ }
+ return true
+ }
+ })
+
+ // Request focus and show keyboard
+ if (args.query.isNullOrEmpty()) {
+ requestFocus()
+ postDelayed({
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.showSoftInput(findFocus(), InputMethodManager.SHOW_IMPLICIT)
+ }, 200)
+ }
+ }
+ }
+
+ private fun setupSearchResultsRecyclerView() {
+ searchResultsAdapter = SearchResultsAdapter { product ->
+ navigateToProductDetail(product)
+ }
+
+ binding.searchResultsRecyclerView.apply {
+ adapter = searchResultsAdapter
+ layoutManager = GridLayoutManager(requireContext(), 2)
+ }
+ }
+
+ private fun observeData() {
+ viewModel.searchResults.observe(viewLifecycleOwner) { products ->
+
+ searchResultsAdapter?.submitList(products)
+ binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
+ binding.searchResultsRecyclerView.isVisible = products.isNotEmpty()
+ }
+
+ viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->
+ binding.progressBar.isVisible = isSearching
+ }
+ }
+
+ private fun navigateToProductDetail(product: ProductsItem) {
+ val intent = Intent(requireContext(), DetailProductActivity::class.java)
+ intent.putExtra("PRODUCT_ID", product.id)
+ startActivity(intent)
+ }
+
+ private fun hideKeyboard() {
+ val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ binding.searchView.let {
+ imm.hideSoftInputFromWindow(it.windowToken, 0)
+ }
+ }
+
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ searchResultsAdapter = null
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt
new file mode 100644
index 0000000..fec5c4e
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt
@@ -0,0 +1,76 @@
+package com.alya.ecommerce_serang.ui.home
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.alya.ecommerce_serang.data.api.dto.ProductsItem
+import com.alya.ecommerce_serang.data.repository.ProductRepository
+import com.alya.ecommerce_serang.data.repository.Result
+import kotlinx.coroutines.launch
+
+class SearchHomeViewModel (private val productRepository: ProductRepository) : ViewModel() {
+
+ private val _searchResults = MutableLiveData>(emptyList())
+ val searchResults: LiveData> = _searchResults
+
+ private val _searchHistory = MutableLiveData>(emptyList())
+ val searchHistory: LiveData> = _searchHistory
+
+ private val _isSearching = MutableLiveData(false)
+ val isSearching: LiveData = _isSearching
+
+ private val _isSearchActive = MutableLiveData(false)
+ val isSearchActive: LiveData = _isSearchActive
+
+ fun searchProducts(query: String) {
+ Log.d("HomeViewModel", "searchProducts called with query: '$query'")
+
+ if (query.isBlank()) {
+ Log.d("HomeViewModel", "Query is blank, clearing results")
+ _searchResults.value = emptyList()
+ _isSearchActive.value = false
+ return
+ }
+
+ _isSearching.value = true
+ _isSearchActive.value = true
+
+ viewModelScope.launch {
+ Log.d("HomeViewModel", "Starting search coroutine")
+
+ when (val result = productRepository.searchProducts(query)) {
+ is Result.Success -> {
+ Log.d("HomeViewModel", "Search successful, found ${result.data.size} products")
+ _searchResults.postValue(result.data)
+
+ // Double check the state after assignment
+ Log.d("HomeViewModel", "Updated searchResults value has ${result.data.size} items")
+ }
+ is Result.Error -> {
+ Log.e("HomeViewModel", "Search failed", result.exception)
+ _searchResults.postValue(emptyList())
+ }
+ else -> {}
+ }
+ _isSearching.postValue(false)
+ }
+ }
+
+ fun clearSearch() {
+ _isSearchActive.value = false
+ _searchResults.value = emptyList()
+ _isSearching.value = false
+ }
+
+ fun loadSearchHistory() {
+ viewModelScope.launch {
+ when (val result = productRepository.getSearchHistory()) {
+ is Result.Success -> _searchHistory.value = result.data
+ is Result.Error -> Log.e("HomeViewModel", "Failed to load search history", result.exception)
+ else -> {}
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt
new file mode 100644
index 0000000..7a95fdc
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt
@@ -0,0 +1,78 @@
+package com.alya.ecommerce_serang.ui.home
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.alya.ecommerce_serang.R
+import com.alya.ecommerce_serang.data.api.dto.ProductsItem
+import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
+import com.bumptech.glide.Glide
+
+class SearchResultsAdapter(
+ private val onItemClick: (ProductsItem) -> Unit
+) : ListAdapter(DIFF_CALLBACK) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = ItemProductGridBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val product = getItem(position)
+ holder.bind(product)
+ }
+
+ inner class ViewHolder(private val binding: ItemProductGridBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ init {
+ binding.root.setOnClickListener {
+ val position = adapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ onItemClick(getItem(position))
+ }
+ }
+ }
+
+ fun bind(product: ProductsItem) {
+ binding.productName.text = product.name
+ binding.productPrice.text = (product.price)
+
+ // Load image with Glide
+ Glide.with(binding.root.context)
+ .load(product.image)
+ .placeholder(R.drawable.placeholder_image)
+// .error(R.drawable.error_image)
+ .into(binding.productImage)
+
+ // Set store name if available
+ product.storeId?.toString().let {
+ binding.storeName.text = it
+ }
+ }
+ }
+
+ override fun submitList(list: List?) {
+ Log.d("SearchResultsAdapter", "Submitting list with ${list?.size ?: 0} items")
+ super.submitList(list)
+ }
+
+ companion object {
+ private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
+ return oldItem == newItem
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt
new file mode 100644
index 0000000..d2040c4
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt
@@ -0,0 +1,67 @@
+package com.alya.ecommerce_serang.ui.notif
+
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.alya.ecommerce_serang.data.api.dto.UserProfile
+import com.alya.ecommerce_serang.data.repository.Result
+import com.alya.ecommerce_serang.data.repository.UserRepository
+import com.alya.ecommerce_serang.utils.SessionManager
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class NotifViewModel @Inject constructor(
+ private val notificationBuilder: NotificationCompat.Builder,
+ private val notificationManager: NotificationManagerCompat,
+ @ApplicationContext private val context: Context,
+ private val userRepository: UserRepository,
+ private val webSocketManager: WebSocketManager,
+ private val sessionManager: SessionManager
+
+) : ViewModel() {
+
+ private val _userProfile = MutableStateFlow>(Result.Loading)
+ val userProfile: StateFlow> = _userProfile.asStateFlow()
+
+ init {
+ fetchUserProfile()
+ }
+
+ // Fetch user profile to get necessary data
+ fun fetchUserProfile() {
+ viewModelScope.launch {
+ _userProfile.value = Result.Loading
+ val result = userRepository.fetchUserProfile()
+ _userProfile.value = result
+
+ // If successful, save the user ID for WebSocket use
+ if (result is Result.Success && result.data != null) {
+ sessionManager.saveUserId(result.data.userId.toString())
+ }
+ }
+ }
+
+ // Start WebSocket connection
+ fun startWebSocketConnection() {
+ webSocketManager.startWebSocketConnection()
+ }
+
+ // Stop WebSocket connection
+ fun stopWebSocketConnection() {
+ webSocketManager.stopWebSocketConnection()
+ }
+
+ // Call when ViewModel is cleared (e.g., app closing)
+ override fun onCleared() {
+ super.onCleared()
+ // No need to stop here - the service will manage its own lifecycle
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt
new file mode 100644
index 0000000..1e22402
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt
@@ -0,0 +1,118 @@
+package com.alya.ecommerce_serang.ui.notif
+
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.alya.ecommerce_serang.data.repository.Result
+import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint // Required for Hilt
+class NotificationActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityNotificationBinding
+ private val viewModel: NotifViewModel by viewModels()
+
+ // Permission request code
+ private val NOTIFICATION_PERMISSION_CODE = 100
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.userProfile.collect { result ->
+ when (result) {
+ is com.alya.ecommerce_serang.data.repository.Result.Success -> {
+ // User profile loaded successfully
+ // Potentially do something with user profile
+ }
+ is com.alya.ecommerce_serang.data.repository.Result.Error -> {
+ // Handle error - show message, etc.
+ Toast.makeText(this@NotificationActivity,
+ "Failed to load profile",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ Result.Loading -> {
+ // Show loading indicator if needed
+ }
+ }
+ }
+ }
+ }
+
+ // Start WebSocket connection
+// viewModel.startWebSocketConnection()
+
+ binding = ActivityNotificationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ // Check and request notification permission for Android 13+
+ requestNotificationPermissionIfNeeded()
+
+ // Set up button click listeners
+// setupButtonListeners()
+
+
+ }
+
+// private fun setupButtonListeners() {
+// binding.simpleNotification.setOnClickListener {
+// viewModel.showSimpleNotification()
+// }
+//
+// binding.updateNotification.setOnClickListener {
+// viewModel.updateSimpleNotification()
+// }
+//
+// binding.cancelNotification.setOnClickListener {
+// viewModel.cancelSimpleNotification()
+// }
+// }
+
+ private fun requestNotificationPermissionIfNeeded() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
+ NOTIFICATION_PERMISSION_CODE
+ )
+ }
+ }
+ }
+
+ // Handle permission request result
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ if (requestCode == NOTIFICATION_PERMISSION_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission granted
+ Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
+ } else {
+ // Permission denied
+ Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
+ // You might want to show a dialog explaining why notifications are important
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt
new file mode 100644
index 0000000..49913e7
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt
@@ -0,0 +1,162 @@
+package com.alya.ecommerce_serang.ui.notif
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import com.alya.ecommerce_serang.BuildConfig
+import com.alya.ecommerce_serang.R
+import com.alya.ecommerce_serang.utils.SessionManager
+import dagger.hilt.android.AndroidEntryPoint
+import io.socket.client.IO
+import io.socket.client.Socket
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SimpleWebSocketService : Service() {
+
+ companion object {
+ private const val TAG = "SocketIOService"
+ private const val NOTIFICATION_CHANNEL_ID = "websocket_service_channel"
+ private const val FOREGROUND_SERVICE_ID = 1001
+ }
+
+ @Inject
+ lateinit var notificationBuilder: NotificationCompat.Builder
+
+ @Inject
+ lateinit var notificationManager: NotificationManagerCompat
+
+ @Inject
+ lateinit var sessionManager: SessionManager
+
+ private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private var socket: Socket? = null
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "Service created")
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
+// .setSmallIcon(R.drawable.ic_notification) // Replace with your app's icon
+ .setPriority(NotificationCompat.PRIORITY_MIN) // Set the lowest priority
+ .setSound(null) // No sound
+ .setVibrate(longArrayOf(0L)) // No vibration
+ .setContentText("") // Empty text or minimal text
+ .setOngoing(true) // Keeps it ongoing
+ .build()
+
+ startForeground(1, notification)
+
+
+ startForeground(FOREGROUND_SERVICE_ID, notification)
+ serviceScope.launch { initSocket() }
+ return START_STICKY
+ }
+
+ private suspend fun initSocket() {
+ val userId = sessionManager.getUserId() ?: run {
+ Log.e(TAG, "User ID not available")
+ stopSelf()
+ return
+ }
+
+ val options = IO.Options().apply {
+ forceNew = true
+ reconnection = true
+ reconnectionDelay = 1000 // Retry every 1 second if disconnected
+ reconnectionAttempts = Int.MAX_VALUE
+ }
+
+ socket = IO.socket(BuildConfig.BASE_URL, options)
+ socket?.apply {
+ on(Socket.EVENT_CONNECT) {
+ Log.d(TAG, "Socket.IO connected")
+ emit("joinRoom", userId)
+ }
+
+ on("notification") { args ->
+ if (args.isNotEmpty()) {
+ val data = args[0] as? JSONObject
+ val title = data?.optString("title", "New Notification") ?: "Notification"
+ val message = data?.optString("message", "") ?: ""
+ showNotification(title, message)
+ }
+ }
+
+ on(Socket.EVENT_DISCONNECT) {
+ Log.d(TAG, "Socket.IO disconnected")
+ }
+
+ on(Socket.EVENT_CONNECT_ERROR) { args ->
+ Log.e(TAG, "Socket.IO connection error: ${args.firstOrNull()}")
+ }
+
+ connect()
+ }
+ }
+
+ private fun showNotification(title: String, message: String) {
+ val notification = notificationBuilder
+ .setContentTitle(title)
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setSmallIcon(R.drawable.baseline_alarm_24)
+ .build()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManager.notify(System.currentTimeMillis().toInt(), notification)
+ } else {
+ Log.e(TAG, "Notification permission not granted")
+ }
+ } else {
+ notificationManager.notify(System.currentTimeMillis().toInt(), notification)
+ }
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ "WebSocket Service Channel",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Channel for WebSocket Service"
+ }
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Service destroyed")
+ socket?.disconnect()
+ socket?.off()
+ serviceScope.cancel()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt
new file mode 100644
index 0000000..f80f46b
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt
@@ -0,0 +1,51 @@
+package com.alya.ecommerce_serang.ui.notif
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import com.alya.ecommerce_serang.utils.SessionManager
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WebSocketManager @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val sessionManager: SessionManager
+) {
+ companion object {
+ private const val TAG = "WebSocketManager"
+
+ }
+
+ fun startWebSocketConnection() {
+ try {
+ // Only start if we have a token
+ if (sessionManager.getToken().isNullOrEmpty()) {
+ Log.d(TAG, "No auth token available, not starting WebSocket service")
+ return
+ }
+
+ Log.d(TAG, "Starting WebSocket service")
+ val serviceIntent = Intent(context, SimpleWebSocketService::class.java)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(serviceIntent)
+ } else {
+ context.startService(serviceIntent)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error starting WebSocket service: ${e.message}")
+ }
+ }
+
+ fun stopWebSocketConnection() {
+ try {
+ Log.d(TAG, "Stopping WebSocket service")
+ context.stopService(Intent(context, SimpleWebSocketService::class.java))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error stopping WebSocket service: ${e.message}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt
index 257e479..8de79a8 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt
@@ -9,8 +9,12 @@ import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
@@ -24,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
@@ -41,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)
@@ -57,6 +61,22 @@ class DetailProductActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
+ 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
+ }
+
setupUI()
setupObservers()
loadData()
@@ -199,6 +219,9 @@ class DetailProductActivity : AppCompatActivity() {
binding.tvDescription.text = product.description
+ binding.btnChat.setOnClickListener{
+ navigateToChat()
+ }
val fullImageUrl = when (val img = product.image) {
is String -> {
@@ -362,8 +385,30 @@ class DetailProductActivity : AppCompatActivity() {
)
}
+ private fun navigateToChat(){
+ val productDetail = viewModel.productDetail.value ?: return
+ val storeDetail = viewModel.storeDetail.value
+
+ if (storeDetail !is Result.Success || storeDetail.data == null) {
+ Toast.makeText(this, "Store information not available", Toast.LENGTH_SHORT).show()
+ return
+ }
+ ChatActivity.createIntent(
+ context = this,
+ storeId = productDetail.storeId,
+ productId = productDetail.productId,
+ productName = productDetail.productName,
+ productPrice = productDetail.price,
+ productImage = productDetail.image,
+ productRating = productDetail.rating,
+ storeName = storeDetail.data.storeName,
+ chatRoomId = 0
+ )
+
+ }
+
companion object {
- const val EXTRA_PRODUCT_ID = "extra_product_id"
+ private const val EXTRA_PRODUCT_ID = "extra_product_id"
fun start(context: Context, productId: Int) {
val intent = Intent(context, DetailProductActivity::class.java)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt
index b527fd9..d1921f9 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt
@@ -39,7 +39,6 @@ class ProfileFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
-
}
override fun onCreateView(
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt
index 08efbbe..af8a8c4 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt
@@ -11,7 +11,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.databinding.ActivityMyStoreBinding
-import com.alya.ecommerce_serang.ui.chat.ChatFragment
+import com.alya.ecommerce_serang.ui.chat.ChatListFragment
import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
@@ -124,7 +124,7 @@ class MyStoreActivity : AppCompatActivity() {
binding.layoutInbox.setOnClickListener {
supportFragmentManager.beginTransaction()
- .replace(android.R.id.content, ChatFragment())
+ .replace(android.R.id.content, ChatListFragment())
.addToBackStack(null)
.commit()
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/Constants.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/Constants.kt
new file mode 100644
index 0000000..02dcb5e
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/utils/Constants.kt
@@ -0,0 +1,40 @@
+package com.alya.ecommerce_serang.utils
+
+object Constants {
+ // API Endpoints
+ const val ENDPOINT_SEND_CHAT = "/sendchat"
+ const val ENDPOINT_UPDATE_CHAT_STATUS = "/chatstatus"
+ const val ENDPOINT_GET_CHAT_DETAIL = "/chatdetail"
+
+ // Shared Preferences
+ const val PREF_NAME = "app_preferences"
+ const val KEY_USER_ID = "user_id"
+ const val KEY_TOKEN = "token"
+
+ // Intent extras
+ const val EXTRA_CHAT_ROOM_ID = "chat_room_id"
+ const val EXTRA_STORE_ID = "store_id"
+ const val EXTRA_PRODUCT_ID = "product_id"
+ const val EXTRA_STORE_NAME = "store_name"
+ const val EXTRA_PRODUCT_NAME = "product_name"
+ const val EXTRA_PRODUCT_PRICE = "product_price"
+ const val EXTRA_PRODUCT_IMAGE = "product_image"
+ const val EXTRA_PRODUCT_RATING = "product_rating"
+
+ // Request codes
+ const val REQUEST_IMAGE_PICK = 1001
+ const val REQUEST_CAMERA = 1002
+ const val REQUEST_STORAGE_PERMISSION = 1003
+
+ // Socket.IO events
+ const val EVENT_JOIN_ROOM = "joinRoom"
+ const val EVENT_NEW_MESSAGE = "new_message"
+ const val EVENT_MESSAGE_DELIVERED = "message_delivered"
+ const val EVENT_MESSAGE_READ = "message_read"
+ const val EVENT_TYPING = "typing"
+
+ // Message status
+ const val STATUS_SENT = "sent"
+ const val STATUS_DELIVERED = "delivered"
+ const val STATUS_READ = "read"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt
index 6feaa5c..d3fd47b 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt
@@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.utils
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
+import androidx.core.content.edit
class SessionManager(context: Context) {
private var sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -10,23 +11,58 @@ class SessionManager(context: Context) {
companion object {
private const val PREFS_NAME = "app_prefs"
private const val USER_TOKEN = "user_token"
+ private const val USER_ID = "user_id" // New constant for storing user ID
+
}
fun saveToken(token: String) {
- val editor = sharedPreferences.edit()
- editor.putString(USER_TOKEN, token)
- editor.apply()
+ 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
}
+ fun saveUserId(userId: String) {
+ sharedPreferences.edit() {
+ putString(USER_ID, userId)
+ }
+ Log.d("SessionManager", "Saved user ID: $userId")
+ }
+
+ 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)
+ }
+ }
+
fun clearToken() {
- val editor = sharedPreferences.edit()
- editor.remove(USER_TOKEN)
- editor.apply()
+ sharedPreferences.edit() {
+ remove(USER_TOKEN)
+ }
+ }
+
+
+
+ //clear data when log out
+ fun clearAll() {
+ sharedPreferences.edit() {
+ clear()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt
deleted file mode 100644
index 394a67a..0000000
--- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ChatViewModel.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.alya.ecommerce_serang.utils.viewmodel
-
-import androidx.lifecycle.ViewModel
-
-class ChatViewModel : ViewModel() {
- // TODO: Implement the ViewModel
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt
index 0e321ba..0f0437b 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt
@@ -52,6 +52,8 @@ class HomeViewModel (
loadProducts()
loadCategories()
}
+
+
}
sealed class HomeUiState {
diff --git a/app/src/main/res/color/bottom_nav_icon_color.xml b/app/src/main/res/color/bottom_nav_icon_color.xml
new file mode 100644
index 0000000..1da8b16
--- /dev/null
+++ b/app/src/main/res/color/bottom_nav_icon_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/bottom_nav_text_color.xml b/app/src/main/res/color/bottom_nav_text_color.xml
new file mode 100644
index 0000000..481be1a
--- /dev/null
+++ b/app/src/main/res/color/bottom_nav_text_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_alarm_24.xml b/app/src/main/res/drawable/baseline_alarm_24.xml
new file mode 100644
index 0000000..59acdcb
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_alarm_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_attach_file_24.xml b/app/src/main/res/drawable/baseline_attach_file_24.xml
new file mode 100644
index 0000000..fe3f21d
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_attach_file_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_edit_text_background.xml b/app/src/main/res/drawable/bg_edit_text_background.xml
new file mode 100644
index 0000000..0cfc787
--- /dev/null
+++ b/app/src/main/res/drawable/bg_edit_text_background.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_message_received.xml b/app/src/main/res/drawable/bg_message_received.xml
new file mode 100644
index 0000000..128f72d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_message_received.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_message_sent.xml b/app/src/main/res/drawable/bg_message_sent.xml
new file mode 100644
index 0000000..f0f1d90
--- /dev/null
+++ b/app/src/main/res/drawable/bg_message_sent.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bottom_nav_background.xml b/app/src/main/res/drawable/bottom_nav_background.xml
new file mode 100644
index 0000000..68362a6
--- /dev/null
+++ b/app/src/main/res/drawable/bottom_nav_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/check_double_24.xml b/app/src/main/res/drawable/check_double_24.xml
new file mode 100644
index 0000000..06b7aff
--- /dev/null
+++ b/app/src/main/res/drawable/check_double_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/check_double_read_24.xml b/app/src/main/res/drawable/check_double_read_24.xml
new file mode 100644
index 0000000..33b9cbc
--- /dev/null
+++ b/app/src/main/res/drawable/check_double_read_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/check_single_24.xml b/app/src/main/res/drawable/check_single_24.xml
new file mode 100644
index 0000000..c3d38c5
--- /dev/null
+++ b/app/src/main/res/drawable/check_single_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_calendar_today_24.xml b/app/src/main/res/drawable/outline_calendar_today_24.xml
new file mode 100644
index 0000000..5ca556c
--- /dev/null
+++ b/app/src/main/res/drawable/outline_calendar_today_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
new file mode 100644
index 0000000..f6e2b69
--- /dev/null
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_detail_product.xml b/app/src/main/res/layout/activity_detail_product.xml
index f9182d4..65458ad 100644
--- a/app/src/main/res/layout/activity_detail_product.xml
+++ b/app/src/main/res/layout/activity_detail_product.xml
@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/white"
+ android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.product.DetailProductActivity">
@@ -147,7 +147,8 @@
android:id="@+id/recyclerViewReviews"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="1"
tools:listitem="@layout/item_review" />
@@ -392,6 +393,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
+ android:layout_marginBottom="8dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
@@ -419,7 +421,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
- android:backgroundTint="@color/white"
app:contentInsetStart="0dp">
-
+ app:itemIconSize="32dp"
+ app:itemPaddingBottom="4dp"
+ app:itemTextAppearanceActive="@style/BottomNavigationTextStyle"
+ app:itemTextAppearanceInactive="@style/BottomNavigationTextStyle"
+ android:elevation="8dp"
+ app:itemIconTint="@color/bottom_nav_icon_color"
+ app:itemTextColor="@color/bottom_nav_text_color" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_notification.xml b/app/src/main/res/layout/activity_notification.xml
new file mode 100644
index 0000000..e821fd0
--- /dev/null
+++ b/app/src/main/res/layout/activity_notification.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml
index a23ce3e..6485770 100644
--- a/app/src/main/res/layout/activity_register.xml
+++ b/app/src/main/res/layout/activity_register.xml
@@ -5,7 +5,7 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/white"
+ android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.auth.RegisterActivity">
+
+ style="@style/SharpedBorderStyleOutline"
+ app:endIconMode="custom"
+ app:endIconDrawable="@drawable/outline_calendar_today_24">
-
+ android:hint="Pilih tanggal"
+ android:focusable="false"
+ android:clickable="true"
+ android:minHeight="50dp"/>
-
+ xmlns:app="http://schemas.android.com/apk/res-auto">
-
+ android:layout_height="wrap_content"
+ android:background="#FFFFFF"
+ android:elevation="4dp"
+ app:layout_constraintTop_toTopOf="parent">
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_chat_list.xml b/app/src/main/res/layout/fragment_chat_list.xml
new file mode 100644
index 0000000..e701c83
--- /dev/null
+++ b/app/src/main/res/layout/fragment_chat_list.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 4deae79..be25e39 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -4,36 +4,31 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.home.HomeFragment">
+
+
+
+ android:layout_height="0dp"
+ app:layout_constraintTop_toBottomOf="@id/searchContainer"
+ app:layout_constraintBottom_toBottomOf="parent">
+
-
-
-
+
@@ -64,8 +61,8 @@
android:layout_marginEnd="32dp"
android:text="@string/show_all"
android:textAllCaps="false"
- android:textColor="@color/blue1"
- android:textSize="16sp"
+ android:textColor="@color/blue_600"
+ android:textSize="14sp"
app:layout_constraintBaseline_toBaselineOf="@id/categoriesText"
app:layout_constraintEnd_toEndOf="parent" />
@@ -73,7 +70,7 @@
android:id="@+id/categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="19dp"
+ android:layout_marginTop="8dp"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal"
@@ -83,16 +80,15 @@
tools:layout_editor_absoluteX="0dp"
tools:listitem="@layout/item_category_home" />
-
-
@@ -104,7 +100,7 @@
android:layout_marginEnd="32dp"
android:text="@string/show_all"
android:textAllCaps="false"
- android:textColor="@color/blue1"
+ android:textColor="@color/blue_600"
android:textSize="16sp"
app:layout_constraintBaseline_toBaselineOf="@id/new_products_text"
app:layout_constraintEnd_toEndOf="parent" />
@@ -123,6 +119,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml
index afbd7ec..22363c9 100644
--- a/app/src/main/res/layout/fragment_profile.xml
+++ b/app/src/main/res/layout/fragment_profile.xml
@@ -5,6 +5,7 @@
android:layout_height="match_parent"
android:background="@color/white"
xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.profile.ProfileFragment">
@@ -320,12 +321,5 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLogout" />
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search_home.xml b/app/src/main/res/layout/fragment_search_home.xml
new file mode 100644
index 0000000..69eb47b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search_home.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_message_received.xml b/app/src/main/res/layout/item_message_received.xml
new file mode 100644
index 0000000..0f03dd7
--- /dev/null
+++ b/app/src/main/res/layout/item_message_received.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_message_sent.xml b/app/src/main/res/layout/item_message_sent.xml
new file mode 100644
index 0000000..fd2aa49
--- /dev/null
+++ b/app/src/main/res/layout/item_message_sent.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_product_grid.xml b/app/src/main/res/layout/item_product_grid.xml
new file mode 100644
index 0000000..cebc279
--- /dev/null
+++ b/app/src/main/res/layout/item_product_grid.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_recent_search.xml b/app/src/main/res/layout/item_recent_search.xml
new file mode 100644
index 0000000..c098004
--- /dev/null
+++ b/app/src/main/res/layout/item_recent_search.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_search.xml b/app/src/main/res/layout/view_search.xml
index 2a2a3c9..3f7d5b6 100644
--- a/app/src/main/res/layout/view_search.xml
+++ b/app/src/main/res/layout/view_search.xml
@@ -12,6 +12,7 @@
android:hint="@string/fragment_home_search"
android:textColor="@color/soft_gray"
android:textSize="16sp"
+ android:fontFamily="@font/dmsans_regular"
android:layout_marginStart="8dp"
android:drawablePadding="8dp"
android:paddingHorizontal="25dp"
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 99d7a48..d9327b4 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -9,7 +9,11 @@
android:id="@+id/homeFragment"
android:name="com.alya.ecommerce_serang.ui.home.HomeFragment"
android:label="fragment_home"
- tools:layout="@layout/fragment_home" />
+ tools:layout="@layout/fragment_home">
+
+
+ tools:layout="@layout/fragment_chat_list" />
+
+
+
#E8ECF2
#7D8FAB
#489EC6
+
+ #489EC6
+ #8E8E8E
+ #489EC6
+ #8E8E8E
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 36c0bcf..499b459 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -117,5 +117,25 @@
- Other reason
+
+ Image attached
+ Tulis pesan
+ Options
+ Block User
+ Report
+ Clear Chat
+ Block user selected
+ Report selected
+ Clear chat selected
+ Permission denied
+ Take Photo
+ Choose from Gallery
+ Select Attachment
+ Image selected
+ Connecting...
+ Disconnected. Reconnecting...
+ Connection error: %1$s
+ User is typing...
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 06b23b1..dcf0226 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -25,4 +25,39 @@
- 8dp
- @color/blue_500
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 0da93ba..24782af 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,16 +1,45 @@
diff --git a/build.gradle.kts b/build.gradle.kts
index 1bd531d..0f35cfd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,7 +2,6 @@
buildscript {
dependencies {
classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1")
-// classpath("com.google.dagger:hilt-android-gradle-plugin:2.55")
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 51949c8..df89d71 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,8 +1,10 @@
[versions]
agp = "8.5.2"
glide = "4.16.0"
-hiltAndroid = "2.51"
+hiltAndroid = "2.48" # Updated from 2.44 for better compatibility
hiltLifecycleViewmodel = "1.0.0-alpha03"
+hiltCompiler = "2.48" # Added for consistency
+ksp = "1.9.0-1.0.13"
kotlin = "1.9.0"
coreKtx = "1.10.1"
@@ -45,7 +47,15 @@ androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" }
+hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
+androidx-hilt-common = { module = "androidx.hilt:hilt-common", version = "1.0.0" }
+androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version = "1.0.0" }
+androidx-hilt-navigation-fragment = { module = "androidx.hilt:hilt-navigation-fragment", version = "1.0.0" }
+androidx-hilt-work = { module = "androidx.hilt:hilt-work", version = "1.0.0" }
+
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }