From 53300a6a84cdb1ecb85bb2f5639c240f5a2c0fb1 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sat, 31 Jan 2026 00:59:00 +0530 Subject: [PATCH] guest docs : sse --- app/build.gradle.kts | 1 + .../trisolarispms/data/api/ApiClient.kt | 23 +++- .../ui/guestdocs/GuestDocumentsTab.kt | 14 ++- .../ui/guestdocs/GuestDocumentsViewModel.kt | 103 ++++++++++++++++-- gradle/libs.versions.toml | 1 + 5 files changed, 124 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3a0a3db..728fe2d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(libs.retrofit.converter.gson) implementation(libs.okhttp) implementation(libs.okhttp.logging) + implementation(libs.okhttp.sse) implementation(libs.coil.compose) implementation(libs.coil.svg) implementation(libs.lottie.compose) diff --git a/app/src/main/java/com/android/trisolarispms/data/api/ApiClient.kt b/app/src/main/java/com/android/trisolarispms/data/api/ApiClient.kt index f64a725..de17e2e 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/ApiClient.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/ApiClient.kt @@ -10,11 +10,11 @@ import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object ApiClient { - fun create( + fun createOkHttpClient( auth: FirebaseAuth = FirebaseAuth.getInstance(), - baseUrl: String = ApiConstants.BASE_URL, - enableLogging: Boolean = true - ): ApiService { + enableLogging: Boolean = true, + readTimeoutSeconds: Long = 30 + ): OkHttpClient { val tokenProvider = FirebaseAuthTokenProvider(auth) val authInterceptor = Interceptor { chain -> val original = chain.request() @@ -52,14 +52,25 @@ object ApiClient { level = if (enableLogging) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE } - val client = OkHttpClient.Builder() + return OkHttpClient.Builder() .addInterceptor(authInterceptor) .authenticator(authenticator) .addInterceptor(logging) .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) + .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() + } + + fun create( + auth: FirebaseAuth = FirebaseAuth.getInstance(), + baseUrl: String = ApiConstants.BASE_URL, + enableLogging: Boolean = true + ): ApiService { + val client = createOkHttpClient( + auth = auth, + enableLogging = enableLogging + ) return Retrofit.Builder() .baseUrl(baseUrl) diff --git a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt index 0518f8c..fbdb26d 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -70,7 +71,7 @@ fun GuestDocumentsTab( guestId: String, bookingId: String, canManageDocuments: Boolean, - viewModel: GuestDocumentsViewModel = viewModel() + viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId") ) { val state by viewModel.state.collectAsState() val context = LocalContext.current @@ -101,8 +102,15 @@ fun GuestDocumentsTab( .build() } - LaunchedEffect(propertyId, guestId) { - viewModel.load(propertyId, guestId) + LaunchedEffect(propertyId, guestId, canManageDocuments) { + if (canManageDocuments) { + viewModel.startStream(propertyId, guestId) + } + } + DisposableEffect(propertyId, guestId, canManageDocuments) { + onDispose { + viewModel.stopStream() + } } val cameraLauncher = rememberLauncherForActivityResult( diff --git a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsViewModel.kt index 3c35113..4e7e29d 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsViewModel.kt @@ -6,14 +6,20 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.trisolarispms.data.api.ApiClient +import com.android.trisolarispms.data.api.ApiConstants import com.android.trisolarispms.data.api.model.GuestDocumentDto +import com.google.gson.Gson import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody +import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.sse.EventSource +import okhttp3.sse.EventSourceListener +import okhttp3.sse.EventSources import java.io.File import java.io.FileOutputStream import java.util.Locale @@ -21,6 +27,9 @@ import java.util.Locale class GuestDocumentsViewModel : ViewModel() { private val _state = MutableStateFlow(GuestDocumentsState()) val state: StateFlow = _state + private val gson = Gson() + private var eventSource: EventSource? = null + private var streamKey: String? = null fun load(propertyId: String, guestId: String) { viewModelScope.launch { @@ -56,6 +65,76 @@ class GuestDocumentsViewModel : ViewModel() { } } + fun startStream(propertyId: String, guestId: String) { + val key = "$propertyId:$guestId" + if (streamKey == key && eventSource != null) return + stopStream() + streamKey = key + _state.update { it.copy(isLoading = true, error = null, documents = emptyList()) } + val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0) + val url = "${ApiConstants.BASE_URL}properties/$propertyId/guests/$guestId/documents/stream" + val request = Request.Builder().url(url).get().build() + eventSource = EventSources.createFactory(client).newEventSource( + request, + object : EventSourceListener() { + override fun onOpen( + eventSource: EventSource, + response: okhttp3.Response + ) { + _state.update { it.copy(isLoading = false) } + } + + override fun onEvent( + eventSource: EventSource, + id: String?, + type: String?, + data: String + ) { + if (data.isBlank() || type == "ping") return + val docs = try { + gson.fromJson(data, Array::class.java)?.toList() + } catch (_: Exception) { + null + } + if (docs != null) { + _state.update { + it.copy( + isLoading = false, + documents = docs, + error = null + ) + } + } + } + + override fun onFailure( + eventSource: EventSource, + t: Throwable?, + response: okhttp3.Response? + ) { + _state.update { + it.copy( + isLoading = false, + error = t?.localizedMessage ?: "Stream disconnected" + ) + } + stopStream() + } + + override fun onClosed(eventSource: EventSource) { + stopStream() + } + } + ) + _state.update { it.copy(isLoading = false) } + } + + fun stopStream() { + eventSource?.cancel() + eventSource = null + streamKey = null + } + fun uploadFromUri( context: Context, propertyId: String, @@ -92,10 +171,10 @@ class GuestDocumentsViewModel : ViewModel() { errorMessage = e.localizedMessage ?: "Upload failed" } } - _state.update { - it.copy( + _state.update { current -> + current.copy( isUploading = false, - error = errorMessage + error = errorMessage ?: current.error ) } } @@ -111,8 +190,8 @@ class GuestDocumentsViewModel : ViewModel() { _state.update { current -> current.copy( isLoading = false, - documents = current.documents.filterNot { it.id == documentId }, - error = null + error = null, + documents = current.documents.filterNot { it.id == documentId } ) } } else { @@ -174,11 +253,12 @@ class GuestDocumentsViewModel : ViewModel() { ) val body = response.body() if (response.isSuccessful && body != null) { - _state.update { current -> - current.copy( + _state.update { it.copy(isUploading = false, error = null) } + } else if (response.code() == 409) { + _state.update { + it.copy( isUploading = false, - documents = listOf(body) + current.documents, - error = null + error = "Duplicate document" ) } } else { @@ -213,4 +293,9 @@ class GuestDocumentsViewModel : ViewModel() { } return output } + + override fun onCleared() { + super.onCleared() + stopStream() + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e502873..ea9408f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +okhttp-sse = { group = "com.squareup.okhttp3", name = "okhttp-sse", version.ref = "okhttp" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-auth-ktx = { module = "com.google.firebase:firebase-auth" } kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutinesPlayServices" }