guest docs : sse

This commit is contained in:
androidlover5842
2026-01-31 00:59:00 +05:30
parent 9ac0b55b89
commit 53300a6a84
5 changed files with 124 additions and 18 deletions

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(libs.retrofit.converter.gson) implementation(libs.retrofit.converter.gson)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.okhttp.logging) implementation(libs.okhttp.logging)
implementation(libs.okhttp.sse)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.svg) implementation(libs.coil.svg)
implementation(libs.lottie.compose) implementation(libs.lottie.compose)

View File

@@ -10,11 +10,11 @@ import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
fun create( fun createOkHttpClient(
auth: FirebaseAuth = FirebaseAuth.getInstance(), auth: FirebaseAuth = FirebaseAuth.getInstance(),
baseUrl: String = ApiConstants.BASE_URL, enableLogging: Boolean = true,
enableLogging: Boolean = true readTimeoutSeconds: Long = 30
): ApiService { ): OkHttpClient {
val tokenProvider = FirebaseAuthTokenProvider(auth) val tokenProvider = FirebaseAuthTokenProvider(auth)
val authInterceptor = Interceptor { chain -> val authInterceptor = Interceptor { chain ->
val original = chain.request() val original = chain.request()
@@ -52,14 +52,25 @@ object ApiClient {
level = if (enableLogging) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE level = if (enableLogging) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
} }
val client = OkHttpClient.Builder() return OkHttpClient.Builder()
.addInterceptor(authInterceptor) .addInterceptor(authInterceptor)
.authenticator(authenticator) .authenticator(authenticator)
.addInterceptor(logging) .addInterceptor(logging)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS)
.build() .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() return Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)

View File

@@ -30,6 +30,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -70,7 +71,7 @@ fun GuestDocumentsTab(
guestId: String, guestId: String,
bookingId: String, bookingId: String,
canManageDocuments: Boolean, canManageDocuments: Boolean,
viewModel: GuestDocumentsViewModel = viewModel() viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId")
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@@ -101,8 +102,15 @@ fun GuestDocumentsTab(
.build() .build()
} }
LaunchedEffect(propertyId, guestId) { LaunchedEffect(propertyId, guestId, canManageDocuments) {
viewModel.load(propertyId, guestId) if (canManageDocuments) {
viewModel.startStream(propertyId, guestId)
}
}
DisposableEffect(propertyId, guestId, canManageDocuments) {
onDispose {
viewModel.stopStream()
}
} }
val cameraLauncher = rememberLauncherForActivityResult( val cameraLauncher = rememberLauncherForActivityResult(

View File

@@ -6,14 +6,20 @@ import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.model.GuestDocumentDto import com.android.trisolarispms.data.api.model.GuestDocumentDto
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Locale import java.util.Locale
@@ -21,6 +27,9 @@ import java.util.Locale
class GuestDocumentsViewModel : ViewModel() { class GuestDocumentsViewModel : ViewModel() {
private val _state = MutableStateFlow(GuestDocumentsState()) private val _state = MutableStateFlow(GuestDocumentsState())
val state: StateFlow<GuestDocumentsState> = _state val state: StateFlow<GuestDocumentsState> = _state
private val gson = Gson()
private var eventSource: EventSource? = null
private var streamKey: String? = null
fun load(propertyId: String, guestId: String) { fun load(propertyId: String, guestId: String) {
viewModelScope.launch { 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<GuestDocumentDto>::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( fun uploadFromUri(
context: Context, context: Context,
propertyId: String, propertyId: String,
@@ -92,10 +171,10 @@ class GuestDocumentsViewModel : ViewModel() {
errorMessage = e.localizedMessage ?: "Upload failed" errorMessage = e.localizedMessage ?: "Upload failed"
} }
} }
_state.update { _state.update { current ->
it.copy( current.copy(
isUploading = false, isUploading = false,
error = errorMessage error = errorMessage ?: current.error
) )
} }
} }
@@ -111,8 +190,8 @@ class GuestDocumentsViewModel : ViewModel() {
_state.update { current -> _state.update { current ->
current.copy( current.copy(
isLoading = false, isLoading = false,
documents = current.documents.filterNot { it.id == documentId }, error = null,
error = null documents = current.documents.filterNot { it.id == documentId }
) )
} }
} else { } else {
@@ -174,11 +253,12 @@ class GuestDocumentsViewModel : ViewModel() {
) )
val body = response.body() val body = response.body()
if (response.isSuccessful && body != null) { if (response.isSuccessful && body != null) {
_state.update { current -> _state.update { it.copy(isUploading = false, error = null) }
current.copy( } else if (response.code() == 409) {
_state.update {
it.copy(
isUploading = false, isUploading = false,
documents = listOf(body) + current.documents, error = "Duplicate document"
error = null
) )
} }
} else { } else {
@@ -213,4 +293,9 @@ class GuestDocumentsViewModel : ViewModel() {
} }
return output return output
} }
override fun onCleared() {
super.onCleared()
stopStream()
}
} }

View File

@@ -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" } retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", 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-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-auth-ktx = { module = "com.google.firebase:firebase-auth" } 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" } kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutinesPlayServices" }