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 @Serializable
enum class Action { enum class Action {
@SerialName("io.element.join")
Join,
@SerialName("im.vector.hangup") @SerialName("im.vector.hangup")
HangUp, HangUp,

View file

@ -48,9 +48,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -91,6 +88,7 @@ class CallScreenPresenter @AssistedInject constructor(
var webViewError by remember { mutableStateOf<String?>(null) } var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag() val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark" val theme = if (ElementTheme.isLightTheme) "light" else "dark"
DisposableEffect(Unit) { DisposableEffect(Unit) {
coroutineScope.launch { coroutineScope.launch {
// Sets the call as joined // Sets the call as joined
@ -145,17 +143,25 @@ class CallScreenPresenter @AssistedInject constructor(
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) { if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
if (parsedMessage.action == WidgetMessage.Action.Close) { if (parsedMessage.action == WidgetMessage.Action.Close) {
close(callWidgetDriver.value, navigator) close(callWidgetDriver.value, navigator)
} else if (parsedMessage.action == WidgetMessage.Action.SendEvent) { } else if (parsedMessage.action == WidgetMessage.Action.Join) {
// This event is received when a member joins the call, the first one will be the current one isJoinedCall = true
val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull
if (type == "org.matrix.msc3401.call.member") {
isJoinedCall = true
}
} }
} }
} }
.launchIn(this) .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) { fun handleEvents(event: CallScreenEvents) {

View file

@ -225,7 +225,7 @@ import kotlin.time.Duration.Companion.seconds
} }
@Test @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 navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
@ -248,13 +248,10 @@ import kotlin.time.Duration.Companion.seconds
messageInterceptor.givenInterceptedMessage( messageInterceptor.givenInterceptedMessage(
""" """
{ {
"action":"send_event", "action":"io.element.join",
"api":"fromWidget", "api":"fromWidget",
"widgetId":"1", "widgetId":"1",
"requestId":"1", "requestId":"1"
"data":{
"type":"org.matrix.msc3401.call.member"
}
} }
""".trimIndent() """.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 @Test
fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest { fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()