Display error dialog if Element Call can't be joined (#4919)

This commit is contained in:
Jorge Martin Espinosa
2025-06-25 08:43:29 +02:00
committed by GitHub
parent 668d91062c
commit b500d135b0
3 changed files with 55 additions and 15 deletions

View File

@@ -30,6 +30,9 @@ data class WidgetMessage(
@Serializable
enum class Action {
@SerialName("io.element.join")
Join,
@SerialName("im.vector.hangup")
HangUp,

View File

@@ -48,9 +48,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
@@ -91,6 +88,7 @@ class CallScreenPresenter @AssistedInject constructor(
var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
DisposableEffect(Unit) {
coroutineScope.launch {
// Sets the call as joined
@@ -145,17 +143,25 @@ class CallScreenPresenter @AssistedInject constructor(
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
if (parsedMessage.action == WidgetMessage.Action.Close) {
close(callWidgetDriver.value, navigator)
} else if (parsedMessage.action == WidgetMessage.Action.SendEvent) {
// This event is received when a member joins the call, the first one will be the current one
val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull
if (type == "org.matrix.msc3401.call.member") {
isJoinedCall = true
}
} else if (parsedMessage.action == WidgetMessage.Action.Join) {
isJoinedCall = true
}
}
}
.launchIn(this)
}
LaunchedEffect(Unit) {
// Wait for the call to be joined, if it takes too long, we display an error
delay(10.seconds)
if (!isJoinedCall) {
Timber.w("The call took too long to be joined. Displaying an error before exiting.")
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
webViewError = ""
}
}
}
fun handleEvents(event: CallScreenEvents) {

View File

@@ -225,7 +225,7 @@ import kotlin.time.Duration.Companion.seconds
}
@Test
fun `present - a received room member message makes the call to be active`() = runTest {
fun `present - a received 'joined' action makes the call to be active`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
@@ -248,13 +248,10 @@ import kotlin.time.Duration.Companion.seconds
messageInterceptor.givenInterceptedMessage(
"""
{
"action":"send_event",
"action":"io.element.join",
"api":"fromWidget",
"widgetId":"1",
"requestId":"1",
"data":{
"type":"org.matrix.msc3401.call.member"
}
"requestId":"1"
}
""".trimIndent()
)
@@ -264,6 +261,40 @@ import kotlin.time.Duration.Companion.seconds
}
}
@Test
fun `present - if in room mode and no join action is received an error is displayed`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
screenTracker = FakeScreenTracker {},
)
val messageInterceptor = FakeWidgetMessageInterceptor()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.isCallActive).isFalse()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
skipItems(2)
// Wait for the timeout to trigger
advanceTimeBy(10.seconds)
val finalState = awaitItem()
assertThat(finalState.isCallActive).isFalse()
// The error dialog that will force the user to leave the call is displayed
assertThat(finalState.webViewError).isNotNull()
assertThat(finalState.webViewError).isEmpty()
}
}
@Test
fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator()