android: show connecting status in chats header via realtime state
This commit is contained in:
@@ -514,3 +514,10 @@
|
||||
- Removed `Archived` top tab from chats list UI.
|
||||
- Added search action in top app bar and unified single search field with leading search icon.
|
||||
- Kept archive as dedicated row inside chats list; opening archive now happens from that row and back navigation appears in app bar while archive is active.
|
||||
|
||||
### Step 83 - Chats header realtime connection status
|
||||
- Added realtime connection state stream (`Disconnected/Connecting/Reconnecting/Connected`) to `RealtimeManager`.
|
||||
- Wired websocket lifecycle into that state in `WsRealtimeManager`.
|
||||
- Bound chats top bar title to realtime state:
|
||||
- shows `Connecting...` while reconnect/initial connect is in progress,
|
||||
- shows regular page title once connected.
|
||||
|
||||
@@ -8,6 +8,8 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -19,6 +21,7 @@ import ru.daemonlord.messenger.BuildConfig
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.di.RefreshClient
|
||||
import ru.daemonlord.messenger.domain.realtime.RealtimeManager
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeConnectionState
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
@@ -42,16 +45,20 @@ class WsRealtimeManager @Inject constructor(
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
override val events: Flow<RealtimeEvent> = eventFlow.asSharedFlow()
|
||||
private val _connectionState = MutableStateFlow(RealtimeConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<RealtimeConnectionState> = _connectionState
|
||||
|
||||
override fun connect() {
|
||||
if (isConnected.get()) return
|
||||
manualDisconnect.set(false)
|
||||
_connectionState.value = RealtimeConnectionState.Connecting
|
||||
scope.launch { openSocket() }
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
manualDisconnect.set(true)
|
||||
isConnected.set(false)
|
||||
_connectionState.value = RealtimeConnectionState.Disconnected
|
||||
heartbeatJob?.cancel()
|
||||
heartbeatJob = null
|
||||
socket?.close(1000, "Client disconnect")
|
||||
@@ -59,7 +66,10 @@ class WsRealtimeManager @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun openSocket() {
|
||||
val accessToken = tokenRepository.getTokens()?.accessToken ?: return
|
||||
val accessToken = tokenRepository.getTokens()?.accessToken ?: run {
|
||||
_connectionState.value = RealtimeConnectionState.Disconnected
|
||||
return
|
||||
}
|
||||
val wsUrl = BuildConfig.API_BASE_URL
|
||||
.replace("http://", "ws://")
|
||||
.replace("https://", "wss://")
|
||||
@@ -72,6 +82,7 @@ class WsRealtimeManager @Inject constructor(
|
||||
|
||||
private fun scheduleReconnect() {
|
||||
if (manualDisconnect.get()) return
|
||||
_connectionState.value = RealtimeConnectionState.Reconnecting
|
||||
scope.launch {
|
||||
delay(reconnectDelayMs)
|
||||
reconnectDelayMs = (reconnectDelayMs * 2).coerceAtMost(MAX_RECONNECT_MS)
|
||||
@@ -98,6 +109,7 @@ class WsRealtimeManager @Inject constructor(
|
||||
private val listener = object : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
isConnected.set(true)
|
||||
_connectionState.value = RealtimeConnectionState.Connected
|
||||
reconnectDelayMs = INITIAL_RECONNECT_MS
|
||||
startHeartbeat(webSocket)
|
||||
}
|
||||
@@ -111,18 +123,27 @@ class WsRealtimeManager @Inject constructor(
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
isConnected.set(false)
|
||||
if (!manualDisconnect.get()) {
|
||||
_connectionState.value = RealtimeConnectionState.Reconnecting
|
||||
}
|
||||
heartbeatJob?.cancel()
|
||||
webSocket.close(code, reason)
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
isConnected.set(false)
|
||||
if (!manualDisconnect.get()) {
|
||||
_connectionState.value = RealtimeConnectionState.Reconnecting
|
||||
}
|
||||
heartbeatJob?.cancel()
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
isConnected.set(false)
|
||||
if (!manualDisconnect.get()) {
|
||||
_connectionState.value = RealtimeConnectionState.Reconnecting
|
||||
}
|
||||
heartbeatJob?.cancel()
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package ru.daemonlord.messenger.domain.realtime
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeConnectionState
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeEvent
|
||||
|
||||
interface RealtimeManager {
|
||||
val events: Flow<RealtimeEvent>
|
||||
val connectionState: StateFlow<RealtimeConnectionState>
|
||||
fun connect()
|
||||
fun disconnect()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.daemonlord.messenger.domain.realtime.model
|
||||
|
||||
enum class RealtimeConnectionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Reconnecting,
|
||||
Connected,
|
||||
}
|
||||
@@ -190,7 +190,13 @@ fun ChatListScreen(
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(if (state.selectedTab == ChatTab.ARCHIVED) "Archived" else "Chats")
|
||||
Text(
|
||||
when {
|
||||
state.isConnecting -> "Connecting..."
|
||||
state.selectedTab == ChatTab.ARCHIVED -> "Archived"
|
||||
else -> "Chats"
|
||||
}
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { searchExpanded = !searchExpanded }) {
|
||||
|
||||
@@ -13,6 +13,7 @@ data class ChatListUiState(
|
||||
val searchQuery: String = "",
|
||||
val isLoading: Boolean = true,
|
||||
val isRefreshing: Boolean = false,
|
||||
val isConnecting: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val chats: List<ChatItem> = emptyList(),
|
||||
val archivedChatsCount: Int = 0,
|
||||
|
||||
@@ -22,6 +22,8 @@ import ru.daemonlord.messenger.domain.chat.usecase.RefreshChatsUseCase
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import ru.daemonlord.messenger.domain.message.repository.MessageRepository
|
||||
import ru.daemonlord.messenger.domain.realtime.RealtimeManager
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeConnectionState
|
||||
import ru.daemonlord.messenger.domain.realtime.usecase.HandleRealtimeEventsUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -32,6 +34,7 @@ class ChatListViewModel @Inject constructor(
|
||||
private val refreshChatsUseCase: RefreshChatsUseCase,
|
||||
private val joinByInviteUseCase: JoinByInviteUseCase,
|
||||
private val handleRealtimeEventsUseCase: HandleRealtimeEventsUseCase,
|
||||
private val realtimeManager: RealtimeManager,
|
||||
private val chatRepository: ChatRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val messageRepository: MessageRepository,
|
||||
@@ -46,6 +49,7 @@ class ChatListViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
handleRealtimeEventsUseCase.start()
|
||||
observeConnectionState()
|
||||
observeChatStream()
|
||||
}
|
||||
|
||||
@@ -314,6 +318,19 @@ class ChatListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeConnectionState() {
|
||||
viewModelScope.launch {
|
||||
realtimeManager.connectionState.collectLatest { state ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isConnecting = state == RealtimeConnectionState.Connecting ||
|
||||
state == RealtimeConnectionState.Reconnecting,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatInternal(
|
||||
type: String,
|
||||
title: String,
|
||||
|
||||
Reference in New Issue
Block a user