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.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.okhttp.sse)
implementation(libs.coil.compose)
implementation(libs.coil.svg)
implementation(libs.lottie.compose)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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<GuestDocumentsState> = _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<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(
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()
}
}