guest docs : sse
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user