feat(jam-session): add connection state management and update UI feedback for session status

This commit is contained in:
Kingkor Roy Tirtho 2026-09-04 15:46:56 +06:00
parent 0eb07ef9b6
commit 60e38f4868
3 changed files with 56 additions and 23 deletions

View File

@ -74,6 +74,9 @@ class JamSessionService(
private val _localParticipantId = MutableStateFlow<String?>(null) private val _localParticipantId = MutableStateFlow<String?>(null)
val localParticipantId: StateFlow<String?> = _localParticipantId.asStateFlow() val localParticipantId: StateFlow<String?> = _localParticipantId.asStateFlow()
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
private val _incomingMessages = MutableSharedFlow<JamMessage>(extraBufferCapacity = 64) private val _incomingMessages = MutableSharedFlow<JamMessage>(extraBufferCapacity = 64)
val incomingMessages = _incomingMessages.asSharedFlow() val incomingMessages = _incomingMessages.asSharedFlow()
@ -174,7 +177,7 @@ class JamSessionService(
return true return true
} }
suspend fun joinSession(offerSdp: String): String { suspend fun joinSession(offerSdp: String, hostName: String? = null): String {
log.i { "Joining jam session" } log.i { "Joining jam session" }
val participantName = resolveParticipantName(defaultPrefix = "Guest") val participantName = resolveParticipantName(defaultPrefix = "Guest")
@ -186,6 +189,13 @@ class JamSessionService(
hostConnection = pc hostConnection = pc
_role.value = JamRole.Guest _role.value = JamRole.Guest
_localParticipantId.value = "guest" _localParticipantId.value = "guest"
_participants.value = listOf(
JamParticipant(
id = "host",
displayName = hostName?.ifBlank { null } ?: "Host",
isHost = true,
)
)
_isActive.value = true _isActive.value = true
// The data channel arrives in-band from the host's offer via on_data_channel; // The data channel arrives in-band from the host's offer via on_data_channel;
@ -226,6 +236,7 @@ class JamSessionService(
_role.value = null _role.value = null
_participants.value = emptyList() _participants.value = emptyList()
_isActive.value = false _isActive.value = false
_isConnected.value = false
_localParticipantId.value = null _localParticipantId.value = null
} }
@ -303,6 +314,7 @@ class JamSessionService(
override fun onDataChannelOpen(label: String) { override fun onDataChannelOpen(label: String) {
log.i { "[$guestId] Data channel '$label' open" } log.i { "[$guestId] Data channel '$label' open" }
_isConnected.value = true
} }
override fun onDataChannelMessage(label: String, data: String) { override fun onDataChannelMessage(label: String, data: String) {
@ -329,6 +341,7 @@ class JamSessionService(
override fun onDataChannelOpen(label: String) { override fun onDataChannelOpen(label: String) {
log.i { "Data channel '$label' open" } log.i { "Data channel '$label' open" }
_isConnected.value = true
} }
override fun onDataChannelMessage(label: String, data: String) { override fun onDataChannelMessage(label: String, data: String) {

View File

@ -295,13 +295,24 @@ private fun GuestSessionView(
ParticipantsSection(state.participants) ParticipantsSection(state.participants)
val answerLink = state.answerLink val answerLink = state.answerLink
if (answerLink == null) { when {
state.isConnected -> {
Text(
text = "Connected to the session",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
)
}
answerLink == null -> {
Text( Text(
text = "Connecting to the session...", text = "Connecting to the session...",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { }
else -> {
Text( Text(
text = "Almost there! Send your answer back to the host:", text = "Almost there! Send your answer back to the host:",
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
@ -313,6 +324,7 @@ private fun GuestSessionView(
onShare = { onShare(answerLink) }, onShare = { onShare(answerLink) },
) )
} }
}
LeaveButton(onLeave) LeaveButton(onLeave)
} }

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.launch
data class JamUiState( data class JamUiState(
val isActive: Boolean = false, val isActive: Boolean = false,
val isConnected: Boolean = false,
val role: JamRole? = null, val role: JamRole? = null,
val participants: List<JamParticipant> = emptyList(), val participants: List<JamParticipant> = emptyList(),
/** Host: deep link containing this session's SDP offer, ready to share. */ /** Host: deep link containing this session's SDP offer, ready to share. */
@ -69,6 +70,7 @@ class JamViewModel(
_uiState.update { _uiState.update {
it.copy( it.copy(
isActive = active, isActive = active,
isConnected = jamSession.isConnected.value,
role = jamSession.role.value, role = jamSession.role.value,
participants = jamSession.participants.value, participants = jamSession.participants.value,
inviteLink = if (!active) null else it.inviteLink, inviteLink = if (!active) null else it.inviteLink,
@ -84,6 +86,11 @@ class JamViewModel(
_uiState.update { it.copy(participants = participants) } _uiState.update { it.copy(participants = participants) }
} }
} }
viewModelScope.launch {
jamSession.isConnected.collect { connected ->
_uiState.update { it.copy(isConnected = connected) }
}
}
viewModelScope.launch { viewModelScope.launch {
deepLinks.pendingLink.collect { link -> deepLinks.pendingLink.collect { link ->
handleDeepLink(link) handleDeepLink(link)
@ -119,16 +126,17 @@ class JamViewModel(
fun joinWithIncomingInvite() { fun joinWithIncomingInvite() {
val sdp = _uiState.value.incomingOfferSdp ?: return val sdp = _uiState.value.incomingOfferSdp ?: return
join(sdp) join(sdp, _uiState.value.incomingHostName)
} }
fun joinWithPasted(input: String) { fun joinWithPasted(input: String) {
val sdp = JamInviteCodec.extractSdp(input) val parsed = JamInviteCodec.parse(input)
val sdp = parsed?.sdp ?: JamInviteCodec.extractSdp(input)
if (sdp == null) { if (sdp == null) {
_uiState.update { it.copy(error = "That doesn't look like a valid jam invite.") } _uiState.update { it.copy(error = "That doesn't look like a valid jam invite.") }
return return
} }
join(sdp) join(sdp, (parsed as? JamInviteLink.HostInvite)?.peerName)
} }
/** /**
@ -171,10 +179,10 @@ class JamViewModel(
_uiState.update { it.copy(incomingOfferSdp = null, incomingHostName = null) } _uiState.update { it.copy(incomingOfferSdp = null, incomingHostName = null) }
} }
private fun join(offerSdp: String) { private fun join(offerSdp: String, hostName: String? = null) {
viewModelScope.launch { viewModelScope.launch {
runCatching { runCatching {
val answer = jamSession.joinSession(offerSdp) val answer = jamSession.joinSession(offerSdp, hostName)
JamInviteCodec.buildGuestAnswer(localName(), answer) JamInviteCodec.buildGuestAnswer(localName(), answer)
}.onSuccess { link -> }.onSuccess { link ->
_uiState.update { _uiState.update {