mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 17:32:22 +00:00
Merge branch 'master' into gracia
This commit is contained in:
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -11,8 +11,12 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".app.App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
@ -34,6 +38,22 @@
|
||||
<activity
|
||||
android:name=".ui.profile.mystore.sells.order.DetailOrderActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.chat.ChatActivity"
|
||||
android:exported="false" />
|
||||
<!-- <provider -->
|
||||
<!-- android:name="androidx.startup.InitializationProvider" -->
|
||||
<!-- android:authorities="${applicationId}.androidx-startup" -->
|
||||
<!-- tools:node="remove" /> -->
|
||||
<service
|
||||
android:name=".ui.notif.SimpleWebSocketService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.notif.NotificationActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.order.detail.AddEvidencePaymentActivity"
|
||||
android:exported="false" />
|
||||
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.alya.ecommerce_serang.data.api.dto
|
||||
|
||||
class ChatRequest {
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package com.alya.ecommerce_serang.data.api.dto
|
||||
|
||||
data class UpdateChatRequest (
|
||||
val id: Int,
|
||||
val status: String
|
||||
)
|
@ -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<ChatItem>,
|
||||
|
||||
@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
|
||||
)
|
@ -0,0 +1,42 @@
|
||||
package com.alya.ecommerce_serang.data.api.response.chat
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ChatListResponse(
|
||||
|
||||
@field:SerializedName("chat")
|
||||
val chat: List<ChatItemList>,
|
||||
|
||||
@field:SerializedName("message")
|
||||
val message: String
|
||||
)
|
||||
|
||||
data class ChatItemList(
|
||||
|
||||
@field:SerializedName("store_id")
|
||||
val storeId: Int,
|
||||
|
||||
@field:SerializedName("user_id")
|
||||
val userId: Int,
|
||||
|
||||
@field:SerializedName("user_image")
|
||||
val userImage: String? = null,
|
||||
|
||||
@field:SerializedName("user_name")
|
||||
val userName: String,
|
||||
|
||||
@field:SerializedName("chat_room_id")
|
||||
val chatRoomId: Int,
|
||||
|
||||
@field:SerializedName("latest_message_time")
|
||||
val latestMessageTime: String,
|
||||
|
||||
@field:SerializedName("store_name")
|
||||
val storeName: String,
|
||||
|
||||
@field:SerializedName("message")
|
||||
val message: String,
|
||||
|
||||
@field:SerializedName("store_image")
|
||||
val storeImage: String? = null
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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<DataItem>
|
||||
)
|
||||
|
||||
data class DataItem(
|
||||
|
||||
@field:SerializedName("search_query")
|
||||
val searchQuery: String
|
||||
)
|
@ -1,5 +1,6 @@
|
||||
package com.alya.ecommerce_serang.data.api.retrofit
|
||||
|
||||
|
||||
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
|
||||
import com.alya.ecommerce_serang.data.api.dto.CartItem
|
||||
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
|
||||
@ -10,20 +11,19 @@ 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 okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
|
||||
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
|
||||
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.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.order.AddEvidenceResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
|
||||
@ -39,8 +39,17 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.StoreRespons
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
|
||||
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
|
||||
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
|
||||
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.store.product.ViewStoreProductsResponse
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
@ -226,4 +235,34 @@ interface ApiService {
|
||||
@Part complaintimg: MultipartBody.Part
|
||||
): Response<ComplaintResponse>
|
||||
|
||||
@POST("search")
|
||||
suspend fun saveSearchQuery(
|
||||
@Body searchRequest: SearchRequest
|
||||
): Response<CreateSearchResponse>
|
||||
|
||||
@GET("search")
|
||||
suspend fun getSearchHistory(): Response<SearchHistoryResponse>
|
||||
|
||||
@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<SendChatResponse>
|
||||
|
||||
@PUT("chatstatus")
|
||||
suspend fun updateChatStatus(
|
||||
@Body request: UpdateChatRequest
|
||||
): Response<UpdateChatResponse>
|
||||
|
||||
@GET("chat/{chatRoomId}")
|
||||
suspend fun getChatDetail(
|
||||
@Path("chatRoomId") chatRoomId: Int
|
||||
): Response<ChatHistoryResponse>
|
||||
|
||||
@GET("chat")
|
||||
suspend fun getChatList(
|
||||
): Response<ChatListResponse>
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
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.ChatItemList
|
||||
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.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class ChatRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
private val TAG = "ChatRepository"
|
||||
|
||||
suspend fun fetchUserProfile(): Result<UserProfile?> {
|
||||
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? = null,
|
||||
imageFile: File? = null,
|
||||
chatRoomId: Int? = null // Not used in the actual API call but kept for compatibility
|
||||
): Result<SendChatResponse> {
|
||||
return try {
|
||||
// Create multipart request parts
|
||||
val storeIdPart = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val messagePart = message.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
|
||||
// Add product ID part if provided
|
||||
val productIdPart = if (productId != null && productId > 0) {
|
||||
productId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Create image part if file is provided
|
||||
val imagePart = if (imageFile != null && imageFile.exists()) {
|
||||
val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
|
||||
MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Debug log the request parameters
|
||||
Log.d("ChatRepository", "Sending chat with: storeId=$storeId, productId=$productId, " +
|
||||
"message length=${message.length}, hasImage=${imageFile != null}")
|
||||
|
||||
// Make API call using your actual endpoint and parameter names
|
||||
val response = apiService.sendChatLine(
|
||||
storeId = storeIdPart,
|
||||
message = messagePart,
|
||||
productId = productIdPart,
|
||||
chatimg = imagePart
|
||||
)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body()
|
||||
if (body != null) {
|
||||
Result.Success(body)
|
||||
} else {
|
||||
Result.Error(Exception("Empty response body"))
|
||||
}
|
||||
} else {
|
||||
val errorBody = response.errorBody()?.string() ?: "{}"
|
||||
Log.e("ChatRepository", "API Error: ${response.code()} - $errorBody")
|
||||
Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChatRepository", "Exception sending message", e)
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMessageStatus(
|
||||
messageId: Int,
|
||||
status: String
|
||||
): Result<UpdateChatResponse> {
|
||||
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<ChatHistoryResponse> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getListChat(): Result<List<ChatItemList>> {
|
||||
return try {
|
||||
val response = apiService.getChatList()
|
||||
|
||||
if (response.isSuccessful){
|
||||
val chat = response.body()?.chat ?: emptyList()
|
||||
Result.Success(chat)
|
||||
} else {
|
||||
Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception){
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<List<ProductsItem>> =
|
||||
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<Search?> =
|
||||
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<List<String>> =
|
||||
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<String, Any?>) : UpdateProductResponse {
|
||||
// Build the request with the updated fields
|
||||
val response = apiService.updateProduct(productId, updatedProduct)
|
||||
|
@ -56,6 +56,102 @@ class UserRepository(private val apiService: ApiService) {
|
||||
}
|
||||
}
|
||||
|
||||
// suspend fun sendChatMessage(
|
||||
// storeId: Int,
|
||||
// message: String,
|
||||
// productId: Int,
|
||||
// imageFile: File? = null
|
||||
// ): Result<SendChatResponse> {
|
||||
// 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<UpdateChatResponse> {
|
||||
// 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<ChatHistoryResponse> {
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
35
app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt
Normal file
35
app/src/main/java/com/alya/ecommerce_serang/di/ChatModule.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<out String>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -0,0 +1,520 @@
|
||||
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()}'")
|
||||
|
||||
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) ?: ""
|
||||
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
||||
|
||||
// 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()
|
||||
|
||||
// If opened from ChatListFragment with a valid chatRoomId
|
||||
if (chatRoomId > 0) {
|
||||
// Directly set the chatRoomId and load chat history
|
||||
viewModel._chatRoomId.value = chatRoomId
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (!state.productName.isNullOrEmpty()) {
|
||||
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.isNullOrEmpty()) {
|
||||
Glide.with(this@ChatActivity)
|
||||
.load(BASE_URL + state.productImageUrl)
|
||||
.centerCrop()
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.error(R.drawable.placeholder_image)
|
||||
.into(binding.imgProduct)
|
||||
}
|
||||
|
||||
// Make sure the product section is visible
|
||||
binding.productContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
// Hide the product section if info is missing
|
||||
binding.productContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
Log.d(TAG, "Processing selected image: $uri")
|
||||
|
||||
// First try the direct approach to get the file path
|
||||
var filePath: String? = null
|
||||
|
||||
// For newer Android versions, we need to handle content URIs properly
|
||||
if (uri.scheme == "content") {
|
||||
val cursor = contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val columnIndex = it.getColumnIndex(MediaStore.Images.Media.DATA)
|
||||
if (columnIndex != -1) {
|
||||
filePath = it.getString(columnIndex)
|
||||
Log.d(TAG, "Found file path from cursor: $filePath")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't get the path directly, create a copy in our cache directory
|
||||
if (filePath == null) {
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
val fileName = "img_${System.currentTimeMillis()}.jpg"
|
||||
val outputFile = File(cacheDir, fileName)
|
||||
|
||||
outputFile.outputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
|
||||
filePath = outputFile.absolutePath
|
||||
Log.d(TAG, "Created temp file from input stream: $filePath")
|
||||
}
|
||||
}
|
||||
} else if (uri.scheme == "file") {
|
||||
// Direct file URI
|
||||
filePath = uri.path
|
||||
Log.d(TAG, "Got file path directly from URI: $filePath")
|
||||
}
|
||||
|
||||
// Process the file path
|
||||
if (filePath != null) {
|
||||
val file = File(filePath)
|
||||
if (file.exists()) {
|
||||
// Check file size (limit to 5MB)
|
||||
if (file.length() > 5 * 1024 * 1024) {
|
||||
Toast.makeText(this, "Image too large (max 5MB), please select a smaller image", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Set the file to the ViewModel
|
||||
viewModel.setSelectedImageFile(file)
|
||||
Toast.makeText(this, R.string.image_selected, Toast.LENGTH_SHORT).show()
|
||||
Log.d(TAG, "Successfully set image file: ${file.absolutePath}, size: ${file.length()} bytes")
|
||||
} else {
|
||||
Log.e(TAG, "File does not exist: $filePath")
|
||||
Toast.makeText(this, "Could not access the selected image", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Could not get file path from URI: $uri")
|
||||
Toast.makeText(this, "Could not process the selected image", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling selected image", e)
|
||||
Toast.makeText(this, "Error processing image: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
showImagePickerOptions()
|
||||
} else {
|
||||
Toast.makeText(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 = 0,
|
||||
productName: String? = null,
|
||||
productPrice: String = "",
|
||||
productImage: String? = null,
|
||||
productRating: String? = null,
|
||||
storeName: String? = null,
|
||||
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)
|
||||
|
||||
// Convert productRating string to float if provided
|
||||
if (productRating != null) {
|
||||
try {
|
||||
putExtra(Constants.EXTRA_PRODUCT_RATING, productRating.toFloat())
|
||||
} catch (e: NumberFormatException) {
|
||||
putExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
|
||||
}
|
||||
} else {
|
||||
putExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
|
||||
}
|
||||
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -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<ChatUiMessage, RecyclerView.ViewHolder>(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<ChatUiMessage>() {
|
||||
override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
//package com.alya.ecommerce_serang.ui.chat
|
||||
//
|
||||
//import android.Manifest
|
||||
//import android.app.Activity
|
||||
//import android.content.Intent
|
||||
//import android.content.pm.PackageManager
|
||||
//import android.net.Uri
|
||||
//import android.os.Bundle
|
||||
//import android.provider.MediaStore
|
||||
//import android.text.Editable
|
||||
//import android.text.TextWatcher
|
||||
//import androidx.fragment.app.Fragment
|
||||
//import android.view.LayoutInflater
|
||||
//import android.view.View
|
||||
//import android.view.ViewGroup
|
||||
//import android.widget.Toast
|
||||
//import androidx.activity.result.contract.ActivityResultContracts
|
||||
//import androidx.core.app.ActivityCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.content.FileProvider
|
||||
//import androidx.fragment.app.viewModels
|
||||
//import androidx.lifecycle.lifecycleScope
|
||||
//import androidx.navigation.fragment.navArgs
|
||||
//import androidx.recyclerview.widget.LinearLayoutManager
|
||||
//import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
//import com.alya.ecommerce_serang.R
|
||||
//import com.alya.ecommerce_serang.databinding.FragmentChatBinding
|
||||
//import com.alya.ecommerce_serang.utils.Constants
|
||||
//import com.bumptech.glide.Glide
|
||||
//import dagger.hilt.android.AndroidEntryPoint
|
||||
//import kotlinx.coroutines.launch
|
||||
//import java.io.File
|
||||
//import java.text.SimpleDateFormat
|
||||
//import java.util.Locale
|
||||
//
|
||||
//@AndroidEntryPoint
|
||||
//class ChatFragment : Fragment() {
|
||||
//
|
||||
// private var _binding: FragmentChatBinding? = null
|
||||
// private val binding get() = _binding!!
|
||||
//
|
||||
// private val viewModel: ChatViewModel by viewModels()
|
||||
//// private val args: ChatFragmentArgs by navArgs()
|
||||
//
|
||||
// private lateinit var chatAdapter: ChatAdapter
|
||||
//
|
||||
// // For image attachment
|
||||
// private var tempImageUri: Uri? = null
|
||||
//
|
||||
// // Typing indicator handler
|
||||
// private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
// private val stopTypingRunnable = Runnable {
|
||||
// viewModel.sendTypingStatus(false)
|
||||
// }
|
||||
//
|
||||
// // Activity Result Launchers
|
||||
// private val pickImageLauncher = registerForActivityResult(
|
||||
// ActivityResultContracts.StartActivityForResult()
|
||||
// ) { result ->
|
||||
// if (result.resultCode == Activity.RESULT_OK) {
|
||||
// result.data?.data?.let { uri ->
|
||||
// handleSelectedImage(uri)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private val takePictureLauncher = registerForActivityResult(
|
||||
// ActivityResultContracts.StartActivityForResult()
|
||||
// ) { result ->
|
||||
// if (result.resultCode == Activity.RESULT_OK) {
|
||||
// tempImageUri?.let { uri ->
|
||||
// handleSelectedImage(uri)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onCreateView(
|
||||
// inflater: LayoutInflater,
|
||||
// container: ViewGroup?,
|
||||
// savedInstanceState: Bundle?
|
||||
// ): View {
|
||||
// _binding = FragmentChatBinding.inflate(inflater, container, false)
|
||||
// return binding.root
|
||||
// }
|
||||
//
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated(view, savedInstanceState)
|
||||
//
|
||||
// setupRecyclerView()
|
||||
// setupListeners()
|
||||
// setupTypingIndicator()
|
||||
// observeViewModel()
|
||||
// }
|
||||
//
|
||||
// private fun setupRecyclerView() {
|
||||
// chatAdapter = ChatAdapter()
|
||||
// binding.recyclerChat.apply {
|
||||
// adapter = chatAdapter
|
||||
// layoutManager = LinearLayoutManager(requireContext()).apply {
|
||||
// stackFromEnd = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setupListeners() {
|
||||
// // Back button
|
||||
// binding.btnBack.setOnClickListener {
|
||||
// requireActivity().onBackPressed()
|
||||
// }
|
||||
//
|
||||
// // Options button
|
||||
// binding.btnOptions.setOnClickListener {
|
||||
// showOptionsMenu()
|
||||
// }
|
||||
//
|
||||
// // Send button
|
||||
// binding.btnSend.setOnClickListener {
|
||||
// val message = binding.editTextMessage.text.toString().trim()
|
||||
// if (message.isNotEmpty() || viewModel.state.value.hasAttachment) {
|
||||
// viewModel.sendMessage(message)
|
||||
// binding.editTextMessage.text.clear()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Attachment button
|
||||
// binding.btnAttachment.setOnClickListener {
|
||||
// checkPermissionsAndShowImagePicker()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setupTypingIndicator() {
|
||||
// binding.editTextMessage.addTextChangedListener(object : TextWatcher {
|
||||
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
//
|
||||
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// viewModel.sendTypingStatus(true)
|
||||
//
|
||||
// // Reset the timer
|
||||
// typingHandler.removeCallbacks(stopTypingRunnable)
|
||||
// typingHandler.postDelayed(stopTypingRunnable, 1000)
|
||||
// }
|
||||
//
|
||||
// override fun afterTextChanged(s: Editable?) {}
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// private fun observeViewModel() {
|
||||
// viewLifecycleOwner.lifecycleScope.launch {
|
||||
// viewModel.state.collectLatest { state ->
|
||||
// // Update messages
|
||||
// chatAdapter.submitList(state.messages)
|
||||
//
|
||||
// // Scroll to bottom if new message
|
||||
// if (state.messages.isNotEmpty()) {
|
||||
// binding.recyclerChat.scrollToPosition(state.messages.size - 1)
|
||||
// }
|
||||
//
|
||||
// // Update product info
|
||||
// binding.tvProductName.text = state.productName
|
||||
// binding.tvProductPrice.text = state.productPrice
|
||||
// binding.ratingBar.rating = state.productRating
|
||||
// binding.tvRating.text = state.productRating.toString()
|
||||
// binding.tvSellerName.text = state.storeName
|
||||
//
|
||||
// // Load product image
|
||||
// if (state.productImageUrl.isNotEmpty()) {
|
||||
// Glide.with(requireContext())
|
||||
// .load(BASE_URL + state.productImageUrl)
|
||||
// .centerCrop()
|
||||
// .placeholder(R.drawable.placeholder_image)
|
||||
// .error(R.drawable.placeholder_image)
|
||||
// .into(binding.imgProduct)
|
||||
// }
|
||||
//
|
||||
// // Show/hide loading indicators
|
||||
// binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
|
||||
// binding.btnSend.isEnabled = !state.isSending
|
||||
//
|
||||
// // Update attachment hint
|
||||
// if (state.hasAttachment) {
|
||||
// binding.editTextMessage.hint = getString(R.string.image_attached)
|
||||
// } else {
|
||||
// binding.editTextMessage.hint = getString(R.string.write_message)
|
||||
// }
|
||||
//
|
||||
// // Show typing indicator
|
||||
// binding.tvTypingIndicator.visibility =
|
||||
// if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||
//
|
||||
// // Handle connection state
|
||||
// handleConnectionState(state.connectionState)
|
||||
//
|
||||
// // Show error if any
|
||||
// state.error?.let { error ->
|
||||
// Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
|
||||
// viewModel.clearError()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun handleConnectionState(state: ConnectionState) {
|
||||
// when (state) {
|
||||
// is ConnectionState.Connected -> {
|
||||
// binding.tvConnectionStatus.visibility = View.GONE
|
||||
// }
|
||||
// is ConnectionState.Connecting -> {
|
||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||
// binding.tvConnectionStatus.text = getString(R.string.connecting)
|
||||
// }
|
||||
// is ConnectionState.Disconnected -> {
|
||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||
// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
|
||||
// }
|
||||
// is ConnectionState.Error -> {
|
||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
||||
// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun showOptionsMenu() {
|
||||
// val options = arrayOf(
|
||||
// getString(R.string.block_user),
|
||||
// getString(R.string.report),
|
||||
// getString(R.string.clear_chat),
|
||||
// getString(R.string.cancel)
|
||||
// )
|
||||
//
|
||||
// androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
// .setTitle(getString(R.string.options))
|
||||
// .setItems(options) { dialog, which ->
|
||||
// when (which) {
|
||||
// 0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show()
|
||||
// 1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show()
|
||||
// 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// dialog.dismiss()
|
||||
// }
|
||||
// .show()
|
||||
// }
|
||||
//
|
||||
// private fun checkPermissionsAndShowImagePicker() {
|
||||
// if (ContextCompat.checkSelfPermission(
|
||||
// requireContext(),
|
||||
// Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
// ) != PackageManager.PERMISSION_GRANTED
|
||||
// ) {
|
||||
// ActivityCompat.requestPermissions(
|
||||
// requireActivity(),
|
||||
// arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA),
|
||||
// Constants.REQUEST_STORAGE_PERMISSION
|
||||
// )
|
||||
// } else {
|
||||
// showImagePickerOptions()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun showImagePickerOptions() {
|
||||
// val options = arrayOf(
|
||||
// getString(R.string.take_photo),
|
||||
// getString(R.string.choose_from_gallery),
|
||||
// getString(R.string.cancel)
|
||||
// )
|
||||
//
|
||||
// androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
// .setTitle(getString(R.string.select_attachment))
|
||||
// .setItems(options) { dialog, which ->
|
||||
// when (which) {
|
||||
// 0 -> openCamera()
|
||||
// 1 -> openGallery()
|
||||
// }
|
||||
// dialog.dismiss()
|
||||
// }
|
||||
// .show()
|
||||
// }
|
||||
//
|
||||
// private fun openCamera() {
|
||||
// val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
// val imageFileName = "IMG_${timeStamp}.jpg"
|
||||
// val storageDir = requireContext().getExternalFilesDir(null)
|
||||
// val imageFile = File(storageDir, imageFileName)
|
||||
//
|
||||
// tempImageUri = FileProvider.getUriForFile(
|
||||
// requireContext(),
|
||||
// "${requireContext().packageName}.fileprovider",
|
||||
// imageFile
|
||||
// )
|
||||
//
|
||||
// val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
// putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri)
|
||||
// }
|
||||
//
|
||||
// takePictureLauncher.launch(intent)
|
||||
// }
|
||||
//
|
||||
// private fun openGallery() {
|
||||
// val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
||||
// pickImageLauncher.launch(intent)
|
||||
// }
|
||||
//
|
||||
// private fun handleSelectedImage(uri: Uri) {
|
||||
// // Get the file from Uri
|
||||
// val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
|
||||
// val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null)
|
||||
// cursor?.moveToFirst()
|
||||
// val columnIndex = cursor?.getColumnIndex(filePathColumn[0])
|
||||
// val filePath = cursor?.getString(columnIndex ?: 0)
|
||||
// cursor?.close()
|
||||
//
|
||||
// if (filePath != null) {
|
||||
// viewModel.setSelectedImageFile(File(filePath))
|
||||
// Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onRequestPermissionsResult(
|
||||
// requestCode: Int,
|
||||
// permissions: Array<out String>,
|
||||
// grantResults: IntArray
|
||||
// ) {
|
||||
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
|
||||
// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// showImagePickerOptions()
|
||||
// } else {
|
||||
// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onDestroyView() {
|
||||
// super.onDestroyView()
|
||||
// typingHandler.removeCallbacks(stopTypingRunnable)
|
||||
// _binding = null
|
||||
// }
|
||||
//}
|
@ -0,0 +1,69 @@
|
||||
package com.alya.ecommerce_serang.ui.chat
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
|
||||
import com.alya.ecommerce_serang.databinding.ItemChatBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class ChatListAdapter(
|
||||
private val chatList: List<ChatItemList>,
|
||||
private val onClick: (ChatItemList) -> Unit
|
||||
) : RecyclerView.Adapter<ChatListAdapter.ChatViewHolder>() {
|
||||
|
||||
inner class ChatViewHolder(private val binding: ItemChatBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chat: ChatItemList) {
|
||||
binding.txtStoreName.text = chat.storeName
|
||||
binding.txtMessage.text = chat.message
|
||||
binding.txtTime.text = formatTime(chat.latestMessageTime)
|
||||
|
||||
// Process image URL properly
|
||||
val imageUrl = chat.storeImage?.let {
|
||||
if (it.startsWith("/")) BASE_URL + it else it
|
||||
}
|
||||
|
||||
Glide.with(binding.imgStore.context)
|
||||
.load(imageUrl)
|
||||
.placeholder(R.drawable.ic_person)
|
||||
.error(R.drawable.placeholder_image)
|
||||
.into(binding.imgStore)
|
||||
|
||||
// Handle click event
|
||||
binding.root.setOnClickListener {
|
||||
onClick(chat)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(isoTime: String): String {
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val date = inputFormat.parse(isoTime)
|
||||
|
||||
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
outputFormat.format(date ?: Date())
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
|
||||
val binding = ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ChatViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = chatList.size
|
||||
|
||||
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
|
||||
holder.bind(chatList[position])
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package com.alya.ecommerce_serang.ui.chat
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
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.data.repository.Result
|
||||
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())
|
||||
socketService = SocketIOService(sessionManager)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
viewModel.getChatList()
|
||||
observeChatList()
|
||||
}
|
||||
|
||||
private fun observeChatList() {
|
||||
viewModel.chatList.observe(viewLifecycleOwner) { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val adapter = ChatListAdapter(result.data) { chatItem ->
|
||||
// Use the ChatActivity.createIntent factory method for proper navigation
|
||||
ChatActivity.createIntent(
|
||||
context = requireActivity(),
|
||||
storeId = chatItem.storeId,
|
||||
productId = 0, // Default value since we don't have it in ChatListItem
|
||||
productName = null, // Null is acceptable as per ChatActivity
|
||||
productPrice = "",
|
||||
productImage = null,
|
||||
productRating = null,
|
||||
storeName = chatItem.storeName,
|
||||
chatRoomId = chatItem.chatRoomId
|
||||
)
|
||||
}
|
||||
binding.chatListRecyclerView.adapter = adapter
|
||||
}
|
||||
is Result.Error -> {
|
||||
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Result.Loading -> {
|
||||
// Optional: show progress bar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
@ -0,0 +1,509 @@
|
||||
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.ChatItemList
|
||||
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<ChatUiState> = _state
|
||||
|
||||
val _chatRoomId = MutableLiveData<Int>(0)
|
||||
val chatRoomId: LiveData<Int> = _chatRoomId
|
||||
|
||||
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
|
||||
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
|
||||
|
||||
// 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? = 0,
|
||||
productName: String? = null,
|
||||
productPrice: String? = null,
|
||||
productImage: String? = null,
|
||||
productRating: Float? = 0f,
|
||||
storeName: String
|
||||
) {
|
||||
this.storeId = storeId
|
||||
this.productId = productId!!
|
||||
this.productName = productName.toString()
|
||||
this.productPrice = productPrice.toString()
|
||||
this.productImage = productImage.toString()
|
||||
this.productRating = productRating!!
|
||||
this.storeName = storeName
|
||||
|
||||
// Update state with product info
|
||||
updateState {
|
||||
it.copy(
|
||||
productName = productName.toString(),
|
||||
productPrice = productPrice.toString(),
|
||||
productImageUrl = productImage.toString(),
|
||||
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() && selectedImageFile == null) {
|
||||
Log.e(TAG, "Cannot send message: Both message and image are empty")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have the necessary parameters
|
||||
if (storeId <= 0) {
|
||||
Log.e(TAG, "Cannot send message: Store ID is invalid")
|
||||
updateState { it.copy(error = "Cannot send message. Invalid store ID.") }
|
||||
return
|
||||
}
|
||||
|
||||
// Get the existing chatRoomId (not used in API but may be needed for Socket.IO)
|
||||
val existingChatRoomId = _chatRoomId.value ?: 0
|
||||
|
||||
// Log debug information
|
||||
Log.d(TAG, "Sending message with params: storeId=$storeId, productId=$productId")
|
||||
Log.d(TAG, "Current user ID: $currentUserId")
|
||||
Log.d(TAG, "Has attachment: ${selectedImageFile != null}")
|
||||
|
||||
// Check image file size if present
|
||||
selectedImageFile?.let { file ->
|
||||
if (file.exists() && file.length() > 5 * 1024 * 1024) { // 5MB limit
|
||||
updateState { it.copy(error = "Image file is too large. Please select a smaller image.") }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
updateState { it.copy(isSending = true) }
|
||||
|
||||
try {
|
||||
// Send the message using the repository
|
||||
// Note: We keep the chatRoomId parameter for compatibility with the repository method signature,
|
||||
// but it's not actually used in the API call
|
||||
val result = chatRepository.sendChatMessage(
|
||||
storeId = storeId,
|
||||
message = message,
|
||||
productId = productId,
|
||||
imageFile = selectedImageFile,
|
||||
chatRoomId = existingChatRoomId
|
||||
)
|
||||
|
||||
when (result) {
|
||||
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
|
||||
val newChatRoomId = chatLine.chatRoomId
|
||||
if (existingChatRoomId == 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) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception in sendMessage", e)
|
||||
updateState {
|
||||
it.copy(
|
||||
isSending = false,
|
||||
error = "An unexpected error occurred: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
}
|
||||
|
||||
fun getChatList() {
|
||||
viewModelScope.launch {
|
||||
_chatList.value = com.alya.ecommerce_serang.data.repository.Result.Loading
|
||||
_chatList.value = chatRepository.getListChat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing the UI state for the chat screen
|
||||
*/
|
||||
data class ChatUiState(
|
||||
val messages: List<ChatUiMessage> = 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
|
||||
)
|
@ -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>(ConnectionState.Disconnected())
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
private val _newMessages = MutableStateFlow<ChatLine?>(null)
|
||||
val newMessages: StateFlow<ChatLine?> = _newMessages
|
||||
|
||||
private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
|
||||
val typingStatus: StateFlow<TypingStatus?> = _typingStatus
|
||||
|
||||
// LiveData for Activity/Fragment observing
|
||||
private val _connectionStateLiveData = MutableLiveData<ConnectionState>(ConnectionState.Disconnected())
|
||||
val connectionStateLiveData: LiveData<ConnectionState> = _connectionStateLiveData
|
||||
|
||||
private val _newMessagesLiveData = MutableLiveData<ChatLine?>()
|
||||
val newMessagesLiveData: LiveData<ChatLine?> = _newMessagesLiveData
|
||||
|
||||
private val _typingStatusLiveData = MutableLiveData<TypingStatus?>()
|
||||
val typingStatusLiveData: LiveData<TypingStatus?> = _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
|
||||
)
|
@ -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
|
||||
// }
|
||||
}
|
@ -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<String, SearchHistoryAdapter.ViewHolder>(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<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<List<ProductsItem>>(emptyList())
|
||||
val searchResults: LiveData<List<ProductsItem>> = _searchResults
|
||||
|
||||
private val _searchHistory = MutableLiveData<List<String>>(emptyList())
|
||||
val searchHistory: LiveData<List<String>> = _searchHistory
|
||||
|
||||
private val _isSearching = MutableLiveData(false)
|
||||
val isSearching: LiveData<Boolean> = _isSearching
|
||||
|
||||
private val _isSearchActive = MutableLiveData(false)
|
||||
val isSearchActive: LiveData<Boolean> = _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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ProductsItem, SearchResultsAdapter.ViewHolder>(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<ProductsItem>?) {
|
||||
Log.d("SearchResultsAdapter", "Submitting list with ${list?.size ?: 0} items")
|
||||
super.submitList(list)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ProductsItem>() {
|
||||
override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<UserProfile?>>(Result.Loading)
|
||||
val userProfile: StateFlow<Result<UserProfile?>> = _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
|
||||
}
|
||||
}
|
@ -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<out String>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -39,7 +39,6 @@ class ProfileFragment : Fragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sessionManager = SessionManager(requireContext())
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -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
|
||||
@ -109,7 +109,7 @@ class MyStoreActivity : AppCompatActivity() {
|
||||
|
||||
binding.layoutInbox.setOnClickListener {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, ChatFragment())
|
||||
.replace(android.R.id.content, ChatListFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.alya.ecommerce_serang.utils.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
// TODO: Implement the ViewModel
|
||||
}
|
@ -52,6 +52,8 @@ class HomeViewModel (
|
||||
loadProducts()
|
||||
loadCategories()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
sealed class HomeUiState {
|
||||
|
5
app/src/main/res/color/bottom_nav_icon_color.xml
Normal file
5
app/src/main/res/color/bottom_nav_icon_color.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/bottom_navigation_icon_color_active" android:state_checked="true"/>
|
||||
<item android:color="@color/bottom_navigation_icon_color_inactive" android:state_checked="false"/>
|
||||
</selector>
|
5
app/src/main/res/color/bottom_nav_text_color.xml
Normal file
5
app/src/main/res/color/bottom_nav_text_color.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/bottom_navigation_text_color_active" android:state_checked="true"/>
|
||||
<item android:color="@color/bottom_navigation_text_color_inactive" android:state_checked="false"/>
|
||||
</selector>
|
5
app/src/main/res/drawable/baseline_alarm_24.xml
Normal file
5
app/src/main/res/drawable/baseline_alarm_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M22,5.72l-4.6,-3.86 -1.29,1.53 4.6,3.86L22,5.72zM7.88,3.39L6.6,1.86 2,5.71l1.29,1.53 4.59,-3.85zM12.5,8L11,8v6l4.75,2.85 0.75,-1.23 -4,-2.37L12.5,8zM12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_attach_file_24.xml
Normal file
5
app/src/main/res/drawable/baseline_attach_file_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
|
||||
|
||||
</vector>
|
11
app/src/main/res/drawable/bg_edit_text_background.xml
Normal file
11
app/src/main/res/drawable/bg_edit_text_background.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#F5F5F5" />
|
||||
<corners android:radius="20dp" />
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="8dp" />
|
||||
</shape>
|
11
app/src/main/res/drawable/bg_message_received.xml
Normal file
11
app/src/main/res/drawable/bg_message_received.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0" />
|
||||
<corners android:radius="16dp" />
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="8dp" />
|
||||
</shape>
|
11
app/src/main/res/drawable/bg_message_sent.xml
Normal file
11
app/src/main/res/drawable/bg_message_sent.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#E1F5FE" />
|
||||
<corners android:radius="16dp" />
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="8dp" />
|
||||
</shape>
|
10
app/src/main/res/drawable/bottom_nav_background.xml
Normal file
10
app/src/main/res/drawable/bottom_nav_background.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@android:color/white"/>
|
||||
<corners
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="20dp"/>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#E0E0E0"/>
|
||||
</shape>
|
5
app/src/main/res/drawable/check_double_24.xml
Normal file
5
app/src/main/res/drawable/check_double_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/check_double_read_24.xml
Normal file
5
app/src/main/res/drawable/check_double_read_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#489EC6" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/check_single_24.xml
Normal file
5
app/src/main/res/drawable/check_single_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/outline_calendar_today_24.xml
Normal file
5
app/src/main/res/drawable/outline_calendar_today_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,10h16v11zM20,8L4,8L4,5h16v3z"/>
|
||||
|
||||
</vector>
|
242
app/src/main/res/layout/activity_chat.xml
Normal file
242
app/src/main/res/layout/activity_chat.xml
Normal file
@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Theme.Ecommerce_serang"
|
||||
tools:context=".ui.chat.ChatActivity">
|
||||
|
||||
<!-- Top Toolbar -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/chatToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FFFFFF"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Back"
|
||||
android:src="@drawable/ic_back_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/imgProfile"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/ic_person"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnBack"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStoreName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="SnackEnak"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
||||
app:layout_constraintTop_toTopOf="@+id/imgProfile"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOptions" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLastActive"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Aktif 3 jam lalu"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvStoreName"
|
||||
app:layout_constraintEnd_toEndOf="@+id/tvStoreName" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnOptions"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Options"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<!-- Product Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardProduct"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chatToolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/product_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgProduct"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Keripik Balado"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imgProduct" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Rp65.000"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvProductName" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/ratingBar"
|
||||
style="?android:attr/ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:isIndicator="true"
|
||||
android:numStars="5"
|
||||
android:rating="5.0"
|
||||
android:stepSize="0.1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvProductPrice" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="5.0"
|
||||
android:textColor="#F9A825"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ratingBar"
|
||||
app:layout_constraintStart_toEndOf="@+id/ratingBar"
|
||||
app:layout_constraintTop_toTopOf="@+id/ratingBar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSellerName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="SnackEnak"
|
||||
android:textColor="#666666"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ratingBar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Chat messages -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerChat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tvTypingIndicator"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardProduct" />
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<TextView
|
||||
android:id="@+id/tvTypingIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:text="User is typing..."
|
||||
android:textColor="#666666"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Chat input area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutChatInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FFFFFF"
|
||||
android:elevation="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnAttachment"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Attachment"
|
||||
android:src="@drawable/baseline_attach_file_24" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editTextMessage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_edit_text_background"
|
||||
android:hint="Tulis pesan"
|
||||
android:fontFamily="@font/dmsans_regular"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="4"
|
||||
android:minHeight="40dp"
|
||||
android:padding="8dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSend"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Send"
|
||||
android:src="@drawable/baseline_attach_file_24" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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">
|
||||
|
||||
<!-- Main Content -->
|
||||
@ -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">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -6,38 +6,38 @@
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Theme.Ecommerce_serang"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<!-- NavHostFragment -->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
app:dividerColor="@color/gray_1"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation" />
|
||||
<!-- BottomNavigationView -->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bottom_nav_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
style="@style/Widget.Material3.BottomNavigationView"
|
||||
app:menu="@menu/bottom_navigation_menu"
|
||||
app:itemIconTint="#489EC6"
|
||||
app:itemIconSize="@dimen/m3_comp_navigation_bar_active_indicator_height" />
|
||||
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" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
29
app/src/main/res/layout/activity_notification.xml
Normal file
29
app/src/main/res/layout/activity_notification.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.notif.NotificationActivity">
|
||||
|
||||
<Button
|
||||
android:id="@+id/simple_notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="simple notificaton"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/update_notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="update notificaton"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="cancel notificaton"/>
|
||||
|
||||
</LinearLayout>
|
@ -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">
|
||||
|
||||
<LinearLayout
|
||||
@ -144,18 +144,24 @@
|
||||
android:textSize="18sp"
|
||||
android:text="@string/birth_date"/>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
|
||||
style="@style/SharpedBorderStyleOutline"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/outline_calendar_today_24">
|
||||
|
||||
<AutoCompleteTextView
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_birth_date"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_birth_date"
|
||||
android:inputType="date"/>
|
||||
android:hint="Pilih tanggal"
|
||||
android:focusable="false"
|
||||
android:clickable="true"
|
||||
android:minHeight="50dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
|
@ -1,13 +1,264 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.chat.ChatFragment">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/chatToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="Hello" />
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FFFFFF"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
</FrameLayout>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Back"
|
||||
android:src="@drawable/ic_back_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/imgProfile"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnBack"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStoreName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="SnackEnak"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
||||
app:layout_constraintTop_toTopOf="@+id/imgProfile"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOptions" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLastActive"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Aktif 3 jam lalu"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvStoreName"
|
||||
app:layout_constraintEnd_toEndOf="@+id/tvStoreName" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnOptions"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Options"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<!-- Connection Status -->
|
||||
<TextView
|
||||
android:id="@+id/tvConnectionStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FFC107"
|
||||
android:padding="4dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#000000"
|
||||
android:text="Connecting..."
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chatToolbar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Product Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardProduct"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvConnectionStatus">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgProduct"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Keripik Balado"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imgProduct" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Rp65.000"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvProductName" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/ratingBar"
|
||||
style="?android:attr/ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:isIndicator="true"
|
||||
android:numStars="5"
|
||||
android:rating="5.0"
|
||||
android:stepSize="0.1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvProductPrice" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="5.0"
|
||||
android:textColor="#F9A825"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ratingBar"
|
||||
app:layout_constraintStart_toEndOf="@+id/ratingBar"
|
||||
app:layout_constraintTop_toTopOf="@+id/ratingBar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSellerName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="SnackEnak"
|
||||
android:textColor="#666666"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ratingBar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Chat messages -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerChat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tvTypingIndicator"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardProduct" />
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<TextView
|
||||
android:id="@+id/tvTypingIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:text="User is typing..."
|
||||
android:textColor="#666666"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Chat input area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutChatInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FFFFFF"
|
||||
android:elevation="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnAttachment"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Attachment"
|
||||
android:src="@drawable/baseline_attach_file_24" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editTextMessage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_edit_text_background"
|
||||
android:hint="Tulis pesan"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="4"
|
||||
android:minHeight="40dp"
|
||||
android:padding="8dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSend"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Send"
|
||||
android:src="@drawable/baseline_attach_file_24" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
36
app/src/main/res/layout/fragment_chat_list.xml
Normal file
36
app/src/main/res/layout/fragment_chat_list.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.chat.ChatListFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chatHeaderTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Pesan"
|
||||
android:textSize="24sp"
|
||||
android:padding="16dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:fontFamily="@font/dmsans_bold" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:dividerColor="@color/black_100"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/chatListRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/item_chat"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
</LinearLayout>
|
@ -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">
|
||||
|
||||
<include
|
||||
android:id="@+id/searchContainer"
|
||||
layout="@layout/view_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Home content in ScrollView -->
|
||||
<ScrollView
|
||||
android:id="@+id/home"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- Your existing home content here -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/searchContainer"
|
||||
layout="@layout/view_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
<!-- Remove searchContainer from here, it's now at the top level -->
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/banners"
|
||||
@ -41,7 +36,8 @@
|
||||
android:layout_height="132dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:background="@drawable/banner_default"
|
||||
tools:layout_editor_absoluteX="16dp" />
|
||||
|
||||
<TextView
|
||||
@ -49,9 +45,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/fragment_home_categories"
|
||||
android:textColor="@color/black"
|
||||
android:textColor="@color/blue_500"
|
||||
android:fontFamily="@font/dmsans_bold"
|
||||
android:textSize="22sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/banners" />
|
||||
@ -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" />
|
||||
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/new_products_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/sold_product_text"
|
||||
android:textColor="@color/black"
|
||||
android:textColor="@color/blue_500"
|
||||
android:fontFamily="@font/dmsans_bold"
|
||||
android:textSize="22sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/categories" />
|
||||
@ -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 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchResultsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- All other search-related elements at the top level -->
|
||||
<LinearLayout
|
||||
android:id="@+id/searchHistoryHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer">
|
||||
<!-- ... -->
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchHistoryRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchHistoryHeader"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noResultsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="No results found"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/searchProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer" />
|
||||
|
||||
<include
|
||||
android:id="@+id/loading"
|
||||
layout="@layout/view_loading"/>
|
||||
|
@ -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">
|
||||
|
||||
<!-- Profile Header -->
|
||||
@ -320,12 +321,5 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tvLogout" />
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottomNavigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
76
app/src/main/res/layout/fragment_search_home.xml
Normal file
76
app/src/main/res/layout/fragment_search_home.xml
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".ui.home.SearchHomeFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/searchToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Kembali"
|
||||
android:src="@drawable/ic_back_24" />
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/searchView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/search_background"
|
||||
android:iconifiedByDefault="false"
|
||||
android:queryHint="Search products..." />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/light_gray"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchToolbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchResultsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
||||
app:spanCount="2"
|
||||
tools:listitem="@layout/item_product_grid" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noResultsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="No results found"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
60
app/src/main/res/layout/item_chat.xml
Normal file
60
app/src/main/res/layout/item_chat.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:elevation="2dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgStore"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/circle_background"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_person" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtStoreName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Store Name"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Last message"
|
||||
android:textColor="#666" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="09.30"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#999" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
64
app/src/main/res/layout/item_message_received.xml
Normal file
64
app/src/main/res/layout/item_message_received.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="60dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/imgAvatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_person"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/layoutMessage" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/bg_message_received"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgAvatar"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="270dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="14sp"
|
||||
tools:text="Boleh banget teh. Teteh mau nawar berapa?" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgAttachment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:maxWidth="220dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/placeholder_image"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTimestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="#888888"
|
||||
android:textSize="10sp"
|
||||
app:layout_constraintStart_toStartOf="@+id/layoutMessage"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layoutMessage"
|
||||
tools:text="12:30" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
65
app/src/main/res/layout/item_message_sent.xml
Normal file
65
app/src/main/res/layout/item_message_sent.xml
Normal file
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_message_sent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="270dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="14sp"
|
||||
tools:text="Beli 1, 60 rb bisa teh?" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgAttachment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:maxWidth="220dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/placeholder_image"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTimestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:textColor="#888888"
|
||||
android:textSize="10sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/imgStatus"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layoutMessage"
|
||||
tools:text="12:30" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgStatus"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/tvTimestamp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/layoutMessage"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvTimestamp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
67
app/src/main/res/layout/item_product_grid.xml
Normal file
67
app/src/main/res/layout/item_product_grid.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/productImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/placeholder_image" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/productName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/productImage"
|
||||
tools:text="Product Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/storeName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="12sp"
|
||||
android:fontFamily="@font/dmsans_medium"
|
||||
app:layout_constraintTop_toBottomOf="@id/productName"
|
||||
tools:text="Store Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/productPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="@color/blue1"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/storeName"
|
||||
tools:text="Rp 150.000" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
32
app/src/main/res/layout/item_recent_search.xml
Normal file
32
app/src/main/res/layout/item_recent_search.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/historyIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/outline_home_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recentSearchText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp"
|
||||
android:text="Cari Produk"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/historyIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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"
|
||||
|
@ -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">
|
||||
<action
|
||||
android:id="@+id/action_homeFragment_to_searchHomeFragment"
|
||||
app:destination="@id/searchHomeFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/profileFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.profile.ProfileFragment"
|
||||
@ -17,9 +21,19 @@
|
||||
tools:layout="@layout/fragment_profile" />
|
||||
<fragment
|
||||
android:id="@+id/chatFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.chat.ChatFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.chat.ChatListFragment"
|
||||
android:label="fragment_chat"
|
||||
tools:layout="@layout/fragment_chat" />
|
||||
tools:layout="@layout/fragment_chat_list" />
|
||||
<fragment
|
||||
android:id="@+id/searchHomeFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.home.SearchHomeFragment"
|
||||
android:label="Search"
|
||||
tools:layout="@layout/fragment_search_home">
|
||||
<argument
|
||||
android:name="query"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
<activity
|
||||
android:id="@+id/registerActivity"
|
||||
android:name="com.alya.ecommerce_serang.ui.auth.RegisterActivity"
|
||||
|
@ -41,4 +41,9 @@
|
||||
<color name="gray_1">#E8ECF2</color>
|
||||
<color name="soft_gray">#7D8FAB</color>
|
||||
<color name="blue1">#489EC6</color>
|
||||
|
||||
<color name="bottom_navigation_icon_color_active">#489EC6</color>
|
||||
<color name="bottom_navigation_icon_color_inactive">#8E8E8E</color>
|
||||
<color name="bottom_navigation_text_color_active">#489EC6</color>
|
||||
<color name="bottom_navigation_text_color_inactive">#8E8E8E</color>
|
||||
</resources>
|
@ -117,5 +117,25 @@
|
||||
<item>Other reason</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Chat Activity -->
|
||||
<string name="image_attached">Image attached</string>
|
||||
<string name="write_message">Tulis pesan</string>
|
||||
<string name="options">Options</string>
|
||||
<string name="block_user">Block User</string>
|
||||
<string name="report">Report</string>
|
||||
<string name="clear_chat">Clear Chat</string>
|
||||
<string name="block_user_selected">Block user selected</string>
|
||||
<string name="report_selected">Report selected</string>
|
||||
<string name="clear_chat_selected">Clear chat selected</string>
|
||||
<string name="permission_denied">Permission denied</string>
|
||||
<string name="take_photo">Take Photo</string>
|
||||
<string name="choose_from_gallery">Choose from Gallery</string>
|
||||
<string name="select_attachment">Select Attachment</string>
|
||||
<string name="image_selected">Image selected</string>
|
||||
<string name="connecting">Connecting...</string>
|
||||
<string name="disconnected_reconnecting">Disconnected. Reconnecting...</string>
|
||||
<string name="connection_error">Connection error: %1$s</string>
|
||||
<string name="typing">User is typing...</string>
|
||||
|
||||
|
||||
</resources>
|
@ -25,4 +25,39 @@
|
||||
<item name="cornerRadius">8dp</item>
|
||||
<item name="backgroundTint">@color/blue_500</item>
|
||||
</style>
|
||||
|
||||
<style name="SharpedBorderStyleOutline">
|
||||
<item name="boxStrokeColor">@color/black_300</item>
|
||||
<item name="boxStrokeWidth">1dp</item>
|
||||
<item name="boxCornerRadiusTopStart">4dp</item>
|
||||
<item name="boxCornerRadiusTopEnd">4dp</item>
|
||||
<item name="boxCornerRadiusBottomStart">4dp</item>
|
||||
<item name="boxCornerRadiusBottomEnd">4dp</item>
|
||||
<item name="android:textColorHint">@color/black_300</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomBottomNavActiveIndicator" parent="@style/Widget.Material3.BottomNavigationView.ActiveIndicator">
|
||||
<item name="android:width">40dp</item>
|
||||
<item name="android:height">4dp</item>
|
||||
<item name="shapeAppearanceOverlay">@style/CustomActiveIndicatorShape</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomActiveIndicatorShape">
|
||||
<item name="cornerSize">2dp</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomNavigationTextStyle" parent="TextAppearance.MaterialComponents.Caption">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="fontFamily">@font/dmsans_semibold</item>
|
||||
<item name="android:paddingTop">8dp</item>
|
||||
<item name="android:layout_marginTop">4dp</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomAppBar">
|
||||
<item name="strokeColor">@color/light_gray</item>
|
||||
<item name="strokeWidth">2dp</item>
|
||||
<item name="color">@color/white</item>
|
||||
<item name="cornerRadius">8dp</item>
|
||||
<item name="backgroundTint">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,16 +1,45 @@
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Ecommerce_serang" parent="Theme.Material3.Light.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- Primary Color Customization -->
|
||||
<item name="colorPrimary">@color/blue_500</item>
|
||||
<item name="colorPrimaryDark">@color/white</item>
|
||||
<item name="colorAccent">@color/black</item>
|
||||
<item name="colorPrimaryVariant">@color/blue_600</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
|
||||
<!-- Secondary Color Customization -->
|
||||
<item name="colorSecondary">@color/blue_500</item>
|
||||
<item name="colorSecondaryVariant">@color/blue_600</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
|
||||
<!-- Surface and Background Colors -->
|
||||
<item name="colorSurface">@color/white</item>
|
||||
<item name="colorOnSurface">@color/black</item>
|
||||
<item name="colorPrimaryContainer">@color/blue_500</item>
|
||||
<item name="colorOnPrimaryContainer">@color/white</item>
|
||||
<item name="android:colorBackground">@color/white</item>
|
||||
<!-- <item name="colorBackground">@color/white</item>-->
|
||||
|
||||
<!-- Container Colors -->
|
||||
<item name="colorPrimaryContainer">@color/blue_50</item>
|
||||
<item name="colorOnPrimaryContainer">@color/blue_500</item>
|
||||
|
||||
<!-- Status Bar and Navigation Bar -->
|
||||
|
||||
<!-- Remove Content Insets -->
|
||||
<item name="android:contentInsetStart">0dp</item>
|
||||
<item name="android:contentInsetLeft">0dp</item>
|
||||
|
||||
<!-- Bottom Navigation Specific -->
|
||||
<item name="bottomNavigationStyle">@style/Widget.MaterialComponents.BottomNavigationView.Colored</item>
|
||||
<item name="bottomAppBarStyle">@style/BottomAppBar</item>
|
||||
|
||||
|
||||
<!-- Remove Purple Accent Color -->
|
||||
<item name="colorAccent">@color/blue_500</item>
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<!-- <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>-->
|
||||
</style>
|
||||
|
||||
<!-- Text Styles -->
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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" }
|
||||
|
||||
|
Reference in New Issue
Block a user