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