Merge pull request #5995 from element-hq/valere/rtc/voice_call

Support for Voice Call only (no video), parity with web
This commit is contained in:
Valere Fedronic 2026-03-09 17:53:38 +01:00 committed by GitHub
commit 8e13b72788
121 changed files with 599 additions and 231 deletions

View file

@ -26,9 +26,10 @@ sealed interface CallType : NodeInputs, Parcelable {
data class RoomCall( data class RoomCall(
val sessionId: SessionId, val sessionId: SessionId,
val roomId: RoomId, val roomId: RoomId,
val isAudioCall: Boolean
) : CallType { ) : CallType {
override fun toString(): String { override fun toString(): String {
return "RoomCall(sessionId=$sessionId, roomId=$roomId)" return "RoomCall(sessionId=$sessionId, roomId=$roomId, isAudioCall=$isAudioCall)"
} }
} }
} }

View file

@ -58,6 +58,7 @@ class DefaultElementCallEntryPoint(
expirationTimestamp = expirationTimestamp, expirationTimestamp = expirationTimestamp,
notificationChannelId = notificationChannelId, notificationChannelId = notificationChannelId,
textContent = textContent, textContent = textContent,
audioOnly = callType.isAudioCall
) )
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
} }

View file

@ -29,4 +29,5 @@ data class CallNotificationData(
val textContent: String?, val textContent: String?,
// Expiration timestamp in millis since epoch // Expiration timestamp in millis since epoch
val expirationTimestamp: Long, val expirationTimestamp: Long,
val audioOnly: Boolean,
) : Parcelable ) : Parcelable

View file

@ -69,6 +69,7 @@ class RingingCallNotificationCreator(
timestamp: Long, timestamp: Long,
expirationTimestamp: Long, expirationTimestamp: Long,
textContent: String?, textContent: String?,
audioOnly: Boolean,
): Notification? { ): Notification? {
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val imageLoader = imageLoaderHolder.get(matrixClient) val imageLoader = imageLoaderHolder.get(matrixClient)
@ -88,7 +89,7 @@ class RingingCallNotificationCreator(
.setImportant(true) .setImportant(true)
.build() .build()
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId)) val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, isAudioCall = audioOnly))
val notificationData = CallNotificationData( val notificationData = CallNotificationData(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -101,6 +102,7 @@ class RingingCallNotificationCreator(
timestamp = timestamp, timestamp = timestamp,
textContent = textContent, textContent = textContent,
expirationTimestamp = expirationTimestamp, expirationTimestamp = expirationTimestamp,
audioOnly = audioOnly,
) )
val declineIntent = PendingIntentCompat.getBroadcast( val declineIntent = PendingIntentCompat.getBroadcast(
@ -127,7 +129,11 @@ class RingingCallNotificationCreator(
.setSmallIcon(CommonDrawables.ic_notification) .setSmallIcon(CommonDrawables.ic_notification)
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_CALL) .setCategory(NotificationCompat.CATEGORY_CALL)
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true)) .setStyle(
NotificationCompat.CallStyle
.forIncomingCall(caller, declineIntent, answerIntent)
.setIsVideo(!audioOnly)
)
.addPerson(caller) .addPerson(caller)
.setAutoCancel(true) .setAutoCancel(true)
.setWhen(timestamp) .setWhen(timestamp)

View file

@ -45,8 +45,8 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = notificationData.sessionId, sessionId = notificationData.sessionId,
roomId = notificationData.roomId, roomId = notificationData.roomId,
), isAudioCall = notificationData.audioOnly
notificationData = notificationData, )
) )
} }
} }

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> {
override val values: Sequence<CallNotificationData>
get() = sequenceOf(
aCallNotificationData(
audioOnly = false
),
aCallNotificationData(
audioOnly = true
),
)
}
internal fun aCallNotificationData(
audioOnly: Boolean
): CallNotificationData {
return CallNotificationData(
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
audioOnly = audioOnly
)
}

View file

@ -226,6 +226,7 @@ class CallScreenPresenter(
sessionId = inputs.sessionId, sessionId = inputs.sessionId,
roomId = inputs.roomId, roomId = inputs.roomId,
clientId = UUID.randomUUID().toString(), clientId = UUID.randomUUID().toString(),
isAudioCall = inputs.isAudioCall,
languageTag = languageTag, languageTag = languageTag,
theme = theme, theme = theme,
).getOrThrow() ).getOrThrow()

View file

@ -112,7 +112,13 @@ class IncomingCallActivity : AppCompatActivity() {
} }
private fun onAnswer(notificationData: CallNotificationData) { private fun onAnswer(notificationData: CallNotificationData) {
elementCallEntryPoint.startCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) elementCallEntryPoint.startCall(
CallType.RoomCall(
notificationData.sessionId,
notificationData.roomId,
isAudioCall = notificationData.audioOnly
)
)
} }
private fun onCancel() { private fun onCancel() {

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -45,10 +46,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
/** /**
@ -103,7 +100,7 @@ internal fun IncomingCallScreen(
ActionButton( ActionButton(
size = 64.dp, size = 64.dp,
onClick = { onAnswer(notificationData) }, onClick = { onAnswer(notificationData) },
icon = CompoundIcons.VoiceCallSolid(), icon = if (notificationData.audioOnly) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
title = stringResource(CommonStrings.action_accept), title = stringResource(CommonStrings.action_accept),
backgroundColor = ElementTheme.colors.iconSuccessPrimary, backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle borderColor = ElementTheme.colors.borderSuccessSubtle
@ -163,21 +160,11 @@ private fun ActionButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun IncomingCallScreenPreview() = ElementPreview { internal fun IncomingCallScreenPreview(
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
) = ElementPreview {
IncomingCallScreen( IncomingCallScreen(
notificationData = CallNotificationData( notificationData = state,
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
),
onAnswer = {}, onAnswer = {},
onCancel = {}, onCancel = {},
) )

View file

@ -146,6 +146,7 @@ class DefaultActiveCallManager(
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = notificationData.sessionId, sessionId = notificationData.sessionId,
roomId = notificationData.roomId, roomId = notificationData.roomId,
isAudioCall = notificationData.audioOnly,
), ),
callState = CallState.Ringing(notificationData), callState = CallState.Ringing(notificationData),
) )
@ -273,6 +274,7 @@ class DefaultActiveCallManager(
timestamp = notificationData.timestamp, timestamp = notificationData.timestamp,
textContent = notificationData.textContent, textContent = notificationData.textContent,
expirationTimestamp = notificationData.expirationTimestamp, expirationTimestamp = notificationData.expirationTimestamp,
audioOnly = notificationData.audioOnly,
) ?: return ) ?: return
runCatchingExceptions { runCatchingExceptions {
notificationManagerCompat.notify( notificationManagerCompat.notify(

View file

@ -16,6 +16,7 @@ interface CallWidgetProvider {
suspend fun getWidget( suspend fun getWidget(
sessionId: SessionId, sessionId: SessionId,
roomId: RoomId, roomId: RoomId,
isAudioCall: Boolean,
clientId: String, clientId: String,
languageTag: String?, languageTag: String?,
theme: String?, theme: String?,

View file

@ -32,6 +32,7 @@ class DefaultCallWidgetProvider(
override suspend fun getWidget( override suspend fun getWidget(
sessionId: SessionId, sessionId: SessionId,
roomId: RoomId, roomId: RoomId,
isAudioCall: Boolean,
clientId: String, clientId: String,
languageTag: String?, languageTag: String?,
theme: String?, theme: String?,
@ -50,6 +51,7 @@ class DefaultCallWidgetProvider(
baseUrl = baseUrl, baseUrl = baseUrl,
encrypted = isEncrypted, encrypted = isEncrypted,
direct = room.isDm(), direct = room.isDm(),
isAudioCall = isAudioCall,
hasActiveCall = roomInfo.hasRoomCall, hasActiveCall = roomInfo.hasRoomCall,
) )
val callUrl = room.generateWidgetWebViewUrl( val callUrl = room.generateWidgetWebViewUrl(

View file

@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest {
@Test @Test
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
val entryPoint = createEntryPoint() val entryPoint = createEntryPoint()
entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false))
val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java) val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java)
val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
@ -53,7 +53,7 @@ class DefaultElementCallEntryPointTest {
val entryPoint = createEntryPoint(activeCallManager = activeCallManager) val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
entryPoint.handleIncomingCall( entryPoint.handleIncomingCall(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
roomName = "roomName", roomName = "roomName",

View file

@ -65,7 +65,33 @@ class RingingCallNotificationCreatorTest {
getUserIconLambda.assertions().isCalledOnce() getUserIconLambda.assertions().isCalledOnce()
} }
private suspend fun RingingCallNotificationCreator.createTestNotification() = createNotification( @Test
fun `createNotification - use the correct style for video call`() = runTest {
val notificationCreator = createRingingCallNotificationCreator(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
)
val notification = notificationCreator.createTestNotification()
assertThat(notification?.category).isEqualTo("call")
val acceptAction = notification?.actions?.get(1)
assertThat(acceptAction?.title?.toString()).isEqualTo("Video")
}
@Test
fun `createNotification - use the correct style for audio call`() = runTest {
val notificationCreator = createRingingCallNotificationCreator(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
)
val notification = notificationCreator.createTestNotification(audioOnly = true)
assertThat(notification?.category).isEqualTo("call")
val acceptAction = notification?.actions?.get(1)
assertThat(acceptAction?.title?.toString()).isEqualTo("Answer")
}
private suspend fun RingingCallNotificationCreator.createTestNotification(audioOnly: Boolean = false) = createNotification(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -77,6 +103,7 @@ class RingingCallNotificationCreatorTest {
timestamp = 0L, timestamp = 0L,
expirationTimestamp = 20L, expirationTimestamp = 20L,
textContent = "textContent", textContent = "textContent",
audioOnly = audioOnly
) )
private fun createRingingCallNotificationCreator( private fun createRingingCallNotificationCreator(

View file

@ -90,7 +90,7 @@ class CallScreenPresenterTest {
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {} val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
val joinedCallLambda = lambdaRecorder<CallType, Unit> {} val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
widgetProvider = widgetProvider, widgetProvider = widgetProvider,
screenTracker = FakeScreenTracker(analyticsLambda), screenTracker = FakeScreenTracker(analyticsLambda),
@ -123,7 +123,7 @@ class CallScreenPresenterTest {
fun `present - set message interceptor, send and receive messages`() = runTest { fun `present - set message interceptor, send and receive messages`() = runTest {
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
screenTracker = FakeScreenTracker {}, screenTracker = FakeScreenTracker {},
) )
@ -154,7 +154,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
navigator = navigator, navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -188,7 +188,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
navigator = navigator, navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -223,7 +223,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
navigator = navigator, navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -260,7 +260,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
navigator = navigator, navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -300,7 +300,7 @@ class CallScreenPresenterTest {
val matrixClient = FakeMatrixClient(syncService = syncService) val matrixClient = FakeMatrixClient(syncService = syncService)
val appForegroundStateService = FakeAppForegroundStateService() val appForegroundStateService = FakeAppForegroundStateService()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
navigator = navigator, navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),

View file

@ -27,6 +27,7 @@ class CallTypeTest {
CallType.RoomCall( CallType.RoomCall(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
isAudioCall = false,
).getSessionId() ).getSessionId()
).isEqualTo(A_SESSION_ID) ).isEqualTo(A_SESSION_ID)
} }
@ -38,7 +39,7 @@ class CallTypeTest {
@Test @Test
fun `RoomCall stringification does not contain the URL`() { fun `RoomCall stringification does not contain the URL`() {
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString()) assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false).toString())
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)") .isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)")
} }
} }

View file

@ -80,6 +80,7 @@ class DefaultActiveCallManagerTest {
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId, sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId, roomId = callNotificationData.roomId,
isAudioCall = false,
), ),
callState = CallState.Ringing(callNotificationData) callState = CallState.Ringing(callNotificationData)
) )
@ -91,6 +92,28 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.notify(notificationId, any()) } verify { notificationManagerCompat.notify(notificationId, any()) }
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `registerIncomingCall - sets the incoming audio call as active`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
val callNotificationData = aCallNotificationData(audioOnly = true)
manager.registerIncomingCall(callNotificationData)
assertThat(manager.activeCall.value).isEqualTo(
ActiveCall(
callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId,
isAudioCall = true,
),
callState = CallState.Ringing(callNotificationData)
)
)
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest { fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
@ -165,7 +188,7 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue() assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false))
assertThat(manager.activeCall.value).isNull() assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse() assertThat(manager.activeWakeLock?.isHeld).isFalse()
@ -192,7 +215,7 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData(roomId = A_ROOM_ID) val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData) manager.registerIncomingCall(notificationData)
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false))
coVerify { coVerify {
room.declineCall(notificationEventId = notificationData.eventId) room.declineCall(notificationEventId = notificationData.eventId)
@ -219,7 +242,7 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData(roomId = A_ROOM_ID) val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
// Do not register the incoming call, so the manager doesn't know about it // Do not register the incoming call, so the manager doesn't know about it
manager.hangUpCall( manager.hangUpCall(
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId), callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false),
notificationData = notificationData, notificationData = notificationData,
) )
coVerify { coVerify {
@ -321,12 +344,13 @@ class DefaultActiveCallManagerTest {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
assertThat(manager.activeCall.value).isNull() assertThat(manager.activeCall.value).isNull()
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, true))
assertThat(manager.activeCall.value).isEqualTo( assertThat(manager.activeCall.value).isEqualTo(
ActiveCall( ActiveCall(
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
isAudioCall = true,
), ),
callState = CallState.InCall, callState = CallState.InCall,
) )
@ -429,6 +453,7 @@ class DefaultActiveCallManagerTest {
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId, sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId, roomId = callNotificationData.roomId,
isAudioCall = false,
), ),
callState = CallState.Ringing(callNotificationData) callState = CallState.Ringing(callNotificationData)
) )

View file

@ -31,7 +31,7 @@ class DefaultCallWidgetProviderTest {
@Test @Test
fun `getWidget - fails if the session does not exist`() = runTest { fun `getWidget - fails if the session does not exist`() = runTest {
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
} }
@Test @Test
@ -40,7 +40,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, null) givenGetRoomResult(A_ROOM_ID, null)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, true, "clientId", "languageTag", "theme").isFailure).isTrue()
} }
@Test @Test
@ -52,7 +52,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
} }
@Test @Test
@ -65,7 +65,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
} }
@Test @Test
@ -78,7 +78,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
} }
@Test @Test
@ -101,7 +101,7 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
activeRoomsHolder = activeRoomsHolder activeRoomsHolder = activeRoomsHolder
) )
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue() assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isSuccess).isTrue()
} }
@Test @Test
@ -122,7 +122,7 @@ class DefaultCallWidgetProviderTest {
callWidgetSettingsProvider = settingsProvider, callWidgetSettingsProvider = settingsProvider,
appPreferencesStore = preferencesStore, appPreferencesStore = preferencesStore,
) )
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme")
assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io")
} }

View file

@ -23,6 +23,7 @@ class FakeCallWidgetProvider(
override suspend fun getWidget( override suspend fun getWidget(
sessionId: SessionId, sessionId: SessionId,
roomId: RoomId, roomId: RoomId,
isAudioCall: Boolean,
clientId: String, clientId: String,
languageTag: String?, languageTag: String?,
theme: String? theme: String?

View file

@ -33,6 +33,7 @@ fun aCallNotificationData(
timestamp: Long = 0L, timestamp: Long = 0L,
expirationTimestamp: Long = 30_000L, expirationTimestamp: Long = 30_000L,
textContent: String? = null, textContent: String? = null,
audioOnly: Boolean = false,
): CallNotificationData = CallNotificationData( ): CallNotificationData = CallNotificationData(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -45,4 +46,5 @@ fun aCallNotificationData(
timestamp = timestamp, timestamp = timestamp,
expirationTimestamp = expirationTimestamp, expirationTimestamp = expirationTimestamp,
textContent = textContent, textContent = textContent,
audioOnly = audioOnly
) )

View file

@ -272,10 +272,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId)) backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
} }
override fun navigateToRoomCall(roomId: RoomId) { override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
isAudioCall = isAudioCall
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)
@ -488,10 +489,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId)) backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId))
} }
override fun navigateToRoomCall(roomId: RoomId) { override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
isAudioCall = isAudioCall
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)

View file

@ -125,7 +125,7 @@ class MessagesNode(
fun navigateToSendLocation() fun navigateToSendLocation()
fun navigateToCreatePoll() fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId) fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId) fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToRoomDetails() fun navigateToRoomDetails()
fun navigateToPinnedMessagesList() fun navigateToPinnedMessagesList()
@ -279,7 +279,9 @@ class MessagesNode(
}, },
onSendLocationClick = callback::navigateToSendLocation, onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll, onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) }, onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList, onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList,
modifier = modifier, modifier = modifier,
knockRequestsBannerView = { knockRequestsBannerView = {

View file

@ -130,7 +130,7 @@ fun MessagesView(
onLinkClick: (String, Boolean) -> Unit, onLinkClick: (String, Boolean) -> Unit,
onSendLocationClick: () -> Unit, onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit, onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,
@ -423,7 +423,7 @@ private fun MessagesViewContent(
onMessageLongClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit, onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit, onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit,
forceJumpToBottomVisibility: Boolean, forceJumpToBottomVisibility: Boolean,
onSwipeToReply: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit,

View file

@ -130,7 +130,7 @@ class ThreadedMessagesNode(
fun navigateToSendLocation() fun navigateToSendLocation()
fun navigateToCreatePoll() fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId) fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId) fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
} }
@ -281,7 +281,9 @@ class ThreadedMessagesNode(
}, },
onSendLocationClick = callback::navigateToSendLocation, onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll, onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) }, onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = {}, onViewAllPinnedMessagesClick = {},
modifier = modifier, modifier = modifier,
knockRequestsBannerView = {}, knockRequestsBannerView = {},

View file

@ -100,7 +100,7 @@ fun TimelineView(
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -35,7 +36,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
internal fun CallMenuItem( internal fun CallMenuItem(
roomCallState: RoomCallState, roomCallState: RoomCallState,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (roomCallState) { when (roomCallState) {
@ -52,7 +53,7 @@ internal fun CallMenuItem(
is RoomCallState.OnGoing -> { is RoomCallState.OnGoing -> {
OnGoingCallMenuItem( OnGoingCallMenuItem(
roomCallState = roomCallState, roomCallState = roomCallState,
onJoinCallClick = onJoinCallClick, onJoinCallClick = { onJoinCallClick(roomCallState.isAudioCall) },
modifier = modifier, modifier = modifier,
) )
} }
@ -62,18 +63,31 @@ internal fun CallMenuItem(
@Composable @Composable
private fun StandByCallMenuItem( private fun StandByCallMenuItem(
roomCallState: RoomCallState.StandBy, roomCallState: RoomCallState.StandBy,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconButton( Row(modifier = modifier) {
modifier = modifier, // Only show voice call in DMs
onClick = onJoinCallClick, if (roomCallState.isDM) {
enabled = roomCallState.canStartCall, IconButton(
) { onClick = { onJoinCallClick(true) },
Icon( enabled = roomCallState.canStartCall,
imageVector = CompoundIcons.VideoCallSolid(), ) {
contentDescription = stringResource(CommonStrings.a11y_start_call), Icon(
) imageVector = CompoundIcons.VoiceCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_voice_call),
)
}
}
IconButton(
onClick = { onJoinCallClick(false) },
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
} }
} }
@ -96,7 +110,11 @@ private fun OnGoingCallMenuItem(
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(), imageVector = if (roomCallState.isAudioCall) {
CompoundIcons.VoiceCallSolid()
} else {
CompoundIcons.VideoCallSolid()
},
contentDescription = null contentDescription = null
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))

View file

@ -46,7 +46,7 @@ internal fun TimelineItemCallNotifyView(
event: TimelineItem.Event, event: TimelineItem.Event,
roomCallState: RoomCallState, roomCallState: RoomCallState,
onLongClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(

View file

@ -72,7 +72,7 @@ internal fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
eventSink: (TimelineEvent.TimelineItemEvent) -> Unit, eventSink: (TimelineEvent.TimelineItemEvent) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =

View file

@ -66,7 +66,7 @@ internal fun MessagesViewTopBar(
dmUserIdentityState: IdentityState?, dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon, sharedHistoryIcon: SharedHistoryIcon,
onRoomDetailsClick: () -> Unit, onRoomDetailsClick: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {

View file

@ -53,6 +53,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
@ -71,6 +72,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -122,7 +124,7 @@ class MessagesViewTest {
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView( rule.setMessagesView(
state = state, state = state,
onJoinCallClick = callback, onJoinCallClick = callback,
@ -132,6 +134,23 @@ class MessagesViewTest {
} }
} }
@Test
fun `clicking on join voice call invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder,
roomCallState = aStandByCallState(isDM = true)
)
ensureCalledOnceWithParam(true) { callback ->
rule.setMessagesView(
state = state,
onJoinCallClick = callback,
)
val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call)
rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
}
}
@Test @Test
fun `clicking on an Event invoke expected callback`() { fun `clicking on an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
@ -609,7 +628,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(), onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(), onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(), onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setSafeContent { setSafeContent {

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@ -186,7 +185,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,
) { ) {
setSafeContent(clearAndroidUiDispatcher = true) { setSafeContent(clearAndroidUiDispatcher = true) {

View file

@ -18,10 +18,12 @@ sealed interface RoomCallState {
data class StandBy( data class StandBy(
val canStartCall: Boolean, val canStartCall: Boolean,
val isDM: Boolean,
) : RoomCallState ) : RoomCallState
data class OnGoing( data class OnGoing(
val canJoinCall: Boolean, val canJoinCall: Boolean,
val isAudioCall: Boolean,
val isUserInTheCall: Boolean, val isUserInTheCall: Boolean,
val isUserLocallyInTheCall: Boolean, val isUserLocallyInTheCall: Boolean,
) : RoomCallState ) : RoomCallState

View file

@ -14,9 +14,11 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
override val values: Sequence<RoomCallState> = sequenceOf( override val values: Sequence<RoomCallState> = sequenceOf(
aStandByCallState(), aStandByCallState(),
aStandByCallState(canStartCall = false), aStandByCallState(canStartCall = false),
aStandByCallState(canStartCall = false, isDM = true),
anOngoingCallState(), anOngoingCallState(),
anOngoingCallState(canJoinCall = false), anOngoingCallState(canJoinCall = false),
anOngoingCallState(canJoinCall = true, isUserInTheCall = true), anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
anOngoingCallState(canJoinCall = true, isAudioCall = true),
RoomCallState.Unavailable, RoomCallState.Unavailable,
) )
} }
@ -25,14 +27,18 @@ fun anOngoingCallState(
canJoinCall: Boolean = true, canJoinCall: Boolean = true,
isUserInTheCall: Boolean = false, isUserInTheCall: Boolean = false,
isUserLocallyInTheCall: Boolean = isUserInTheCall, isUserLocallyInTheCall: Boolean = isUserInTheCall,
isAudioCall: Boolean = false,
) = RoomCallState.OnGoing( ) = RoomCallState.OnGoing(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall,
isAudioCall = isAudioCall
) )
fun aStandByCallState( fun aStandByCallState(
canStartCall: Boolean = true, canStartCall: Boolean = true,
isDM: Boolean = false,
) = RoomCallState.StandBy( ) = RoomCallState.StandBy(
canStartCall = canStartCall, canStartCall = canStartCall,
isDM
) )

View file

@ -21,6 +21,7 @@ import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canCall import io.element.android.libraries.matrix.api.room.powerlevels.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@ -56,8 +57,13 @@ class RoomCallStatePresenter(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall,
// TODO resolve intent while the call is ongoing
isAudioCall = false
)
else -> RoomCallState.StandBy(
canStartCall = canJoinCall,
isDM = roomInfo.isDm
) )
else -> RoomCallState.StandBy(canStartCall = canJoinCall)
} }
} }
} }

View file

@ -41,6 +41,7 @@ class RoomCallStatePresenterTest {
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = false, canStartCall = false,
isDM = false
) )
) )
} }
@ -79,6 +80,28 @@ class RoomCallStatePresenterTest {
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = true, canStartCall = true,
isDM = false
)
)
}
}
@Test
fun `present - initial state - when is direct room`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDirect = true),
roomPermissions = roomPermissions(true),
)
)
val presenter = createRoomCallStatePresenter(joinedRoom = room)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
RoomCallState.StandBy(
canStartCall = true,
isDM = true
) )
) )
} }
@ -98,6 +121,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = false, canJoinCall = false,
isAudioCall = false,
isUserInTheCall = false, isUserInTheCall = false,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -125,6 +149,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -155,6 +180,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = true, isUserLocallyInTheCall = true,
) )
@ -187,6 +213,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = true, isUserLocallyInTheCall = true,
) )
@ -195,6 +222,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -208,6 +236,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = false, isUserInTheCall = false,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -221,6 +250,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = true, canStartCall = true,
isDM = false
) )
) )
} }

View file

@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
@ -223,10 +224,11 @@ class RoomDetailsFlowNode(
backstack.push(NavTarget.RoomMemberDetails(userId)) backstack.push(NavTarget.RoomMemberDetails(userId))
} }
override fun navigateToRoomCall() { override fun navigateToRoomCall(callIntent: CallIntent) {
val inputs = CallType.RoomCall( val inputs = CallType.RoomCall(
sessionId = room.sessionId, sessionId = room.sessionId,
roomId = room.roomId, roomId = room.roomId,
isAudioCall = callIntent == CallIntent.AUDIO
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(inputs) elementCallEntryPoint.startCall(inputs)
@ -284,8 +286,14 @@ class RoomDetailsFlowNode(
callback.navigateToRoom(roomId, emptyList()) callback.navigateToRoom(roomId, emptyList())
} }
override fun startCall(dmRoomId: RoomId) { override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId)) elementCallEntryPoint.startCall(
CallType.RoomCall(
roomId = dmRoomId,
sessionId = room.sessionId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
} }
override fun startVerifyUserFlow(userId: UserId) { override fun startVerifyUserFlow(userId: UserId) {

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -59,7 +60,7 @@ class RoomDetailsNode(
fun navigateToKnockRequestsList() fun navigateToKnockRequestsList()
fun navigateToSecurityAndPrivacy() fun navigateToSecurityAndPrivacy()
fun navigateToRoomMemberDetails(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun navigateToRoomCall() fun navigateToRoomCall(callIntent: CallIntent)
fun navigateToReportRoom() fun navigateToReportRoom()
fun navigateToSelectNewOwnersWhenLeaving() fun navigateToSelectNewOwnersWhenLeaving()
} }

View file

@ -79,6 +79,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
@ -105,7 +106,7 @@ fun RoomDetailsView(
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
openMediaGallery: () -> Unit, openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit, openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (CallIntent) -> Unit,
onPinnedMessagesClick: () -> Unit, onPinnedMessagesClick: () -> Unit,
onKnockRequestsClick: () -> Unit, onKnockRequestsClick: () -> Unit,
onSecurityAndPrivacyClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit,
@ -327,7 +328,7 @@ private fun MainActionsSection(
state: RoomDetailsState, state: RoomDetailsState,
onShareRoom: () -> Unit, onShareRoom: () -> Unit,
onInvitePeople: () -> Unit, onInvitePeople: () -> Unit,
onCall: () -> Unit, onCall: (callIntent: CallIntent) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -356,10 +357,19 @@ private fun MainActionsSection(
} }
if (state.roomCallState.hasPermissionToJoin()) { if (state.roomCallState.hasPermissionToJoin()) {
// TODO Improve the view depending on all the cases here? // TODO Improve the view depending on all the cases here?
if (state.roomType is RoomDetailsType.Dm) {
// As per design, only show voice call in DM
MainActionButton(
title = stringResource(CommonStrings.action_call),
imageVector = CompoundIcons.VoiceCall(),
onClick = { onCall(CallIntent.AUDIO) },
)
}
MainActionButton( MainActionButton(
title = stringResource(CommonStrings.action_call), title = stringResource(CommonStrings.common_video),
imageVector = CompoundIcons.VideoCall(), imageVector = CompoundIcons.VideoCall(),
onClick = onCall, onClick = { onCall(CallIntent.VIDEO) },
) )
} }
if (state.roomType is RoomDetailsType.Room) { if (state.roomType is RoomDetailsType.Room) {

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -67,8 +68,8 @@ class RoomMemberDetailsNode(
callback.navigateToRoom(roomId) callback.navigateToRoom(roomId)
} }
fun onStartCall(roomId: RoomId) { fun onStartCall(roomId: RoomId, callIntent: CallIntent) {
callback.startCall(roomId) callback.startCall(roomId, callIntent)
} }
val state = presenter.present() val state = presenter.present()

View file

@ -18,6 +18,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -121,7 +122,25 @@ class RoomDetailsViewTest {
@Test @Test
fun `click on call invokes expected callback`() { fun `click on call invokes expected callback`() {
ensureCalledOnce { callback -> ensureCalledOnceWithParam(CallIntent.AUDIO) { callback ->
rule.setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
canInvite = true,
roomType = RoomDetailsType.Dm(
aRoomMember(UserId("@me:local.org")),
aRoomMember(UserId("@other:local.org"))
),
),
onJoinCallClick = callback,
)
rule.clickOn(CommonStrings.action_call)
}
}
@Test
fun `click on video call invokes expected callback`() {
ensureCalledOnceWithParam(CallIntent.VIDEO) { callback ->
rule.setRoomDetailView( rule.setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
@ -129,7 +148,7 @@ class RoomDetailsViewTest {
), ),
onJoinCallClick = callback, onJoinCallClick = callback,
) )
rule.clickOn(CommonStrings.action_call) rule.clickOn(CommonStrings.common_video)
} }
} }
@ -343,7 +362,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
openPollHistory: () -> Unit = EnsureNeverCalled(), openPollHistory: () -> Unit = EnsureNeverCalled(),
openMediaGallery: () -> Unit = EnsureNeverCalled(), openMediaGallery: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(), openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (CallIntent) -> Unit = EnsureNeverCalledWithParam(),
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),

View file

@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -83,8 +84,14 @@ class UserProfileFlowNode(
callback.navigateToRoom(roomId) callback.navigateToRoom(roomId)
} }
override fun startCall(dmRoomId: RoomId) { override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId)) elementCallEntryPoint.startCall(
CallType.RoomCall(
sessionId = sessionId,
roomId = dmRoomId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
} }
override fun startVerifyUserFlow(userId: UserId) { override fun startVerifyUserFlow(userId: UserId) {

View file

@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.button.MainActionButton import io.element.android.libraries.designsystem.components.button.MainActionButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -26,11 +29,13 @@ fun UserProfileMainActionsSection(
canCall: Boolean, canCall: Boolean,
onShareUser: () -> Unit, onShareUser: () -> Unit,
onStartDM: () -> Unit, onStartDM: () -> Unit,
onCall: () -> Unit, onCall: (CallIntent) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier.fillMaxWidth().padding(horizontal = 16.dp), modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
if (!isCurrentUser) { if (!isCurrentUser) {
@ -43,8 +48,14 @@ fun UserProfileMainActionsSection(
if (canCall) { if (canCall) {
MainActionButton( MainActionButton(
title = stringResource(CommonStrings.action_call), title = stringResource(CommonStrings.action_call),
imageVector = CompoundIcons.VoiceCall(),
onClick = { onCall(CallIntent.AUDIO) },
)
MainActionButton(
title = stringResource(CommonStrings.common_video),
imageVector = CompoundIcons.VideoCall(), imageVector = CompoundIcons.VideoCall(),
onClick = onCall, onClick = { onCall(CallIntent.VIDEO) },
) )
} }
MainActionButton( MainActionButton(
@ -54,3 +65,15 @@ fun UserProfileMainActionsSection(
) )
} }
} }
@PreviewsDayNight()
@Composable
internal fun UserProfileMainActionsSectionPreview() = ElementPreview {
UserProfileMainActionsSection(
isCurrentUser = false,
canCall = true,
onShareUser = { },
onStartDM = { },
onCall = { }
)
}

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber import timber.log.Timber
@ -24,7 +25,7 @@ class UserProfileNodeHelper(
interface Callback : NodeInputs { interface Callback : NodeInputs {
fun navigateToAvatarPreview(username: String, avatarUrl: String) fun navigateToAvatarPreview(username: String, avatarUrl: String)
fun navigateToRoom(roomId: RoomId) fun navigateToRoom(roomId: RoomId)
fun startCall(dmRoomId: RoomId) fun startCall(dmRoomId: RoomId, callIntent: CallIntent)
fun startVerifyUserFlow(userId: UserId) fun startVerifyUserFlow(userId: UserId)
} }

View file

@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -52,7 +53,7 @@ fun UserProfileView(
state: UserProfileState, state: UserProfileState,
onShareUser: () -> Unit, onShareUser: () -> Unit,
onOpenDm: (RoomId) -> Unit, onOpenDm: (RoomId) -> Unit,
onStartCall: (RoomId) -> Unit, onStartCall: (RoomId, CallIntent) -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit, openAvatarPreview: (username: String, url: String) -> Unit,
onVerifyClick: (UserId) -> Unit, onVerifyClick: (UserId) -> Unit,
@ -90,7 +91,7 @@ fun UserProfileView(
canCall = state.canCall, canCall = state.canCall,
onShareUser = onShareUser, onShareUser = onShareUser,
onStartDM = { state.eventSink(UserProfileEvents.StartDM) }, onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
onCall = { state.dmRoomId?.let { onStartCall(it) } } onCall = { intent -> state.dmRoomId?.let { onStartCall(it, intent) } }
) )
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
if (!state.isCurrentUser) { if (!state.isCurrentUser) {
@ -151,7 +152,7 @@ internal fun UserProfileViewPreview(
onShareUser = {}, onShareUser = {},
goBack = {}, goBack = {},
onOpenDm = {}, onOpenDm = {},
onStartCall = {}, onStartCall = { _, _ -> },
openAvatarPreview = { _, _ -> }, openAvatarPreview = { _, _ -> },
onVerifyClick = {}, onVerifyClick = {},
) )

View file

@ -23,6 +23,7 @@ import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
@ -105,7 +106,7 @@ class UserProfileViewTest {
@Test @Test
fun `on Call clicked - the expected callback is called`() = runTest { fun `on Call clicked - the expected callback is called`() = runTest {
ensureCalledOnceWithParam(A_ROOM_ID) { callback -> ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback ->
rule.setUserProfileView( rule.setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
dmRoomId = A_ROOM_ID, dmRoomId = A_ROOM_ID,
@ -117,6 +118,20 @@ class UserProfileViewTest {
} }
} }
@Test
fun `on Video Call clicked - the expected callback is called`() = runTest {
ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback ->
rule.setUserProfileView(
state = aUserProfileState(
dmRoomId = A_ROOM_ID,
canCall = true,
),
onStartCall = callback,
)
rule.clickOn(CommonStrings.common_video)
}
}
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest {
@ -216,7 +231,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserP
), ),
onShareUser: () -> Unit = EnsureNeverCalled(), onShareUser: () -> Unit = EnsureNeverCalled(),
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onStartCall: (RoomId, CallIntent) -> Unit = EnsureNeverCalledWithTwoParams(),
onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
goBack: () -> Unit = EnsureNeverCalled(), goBack: () -> Unit = EnsureNeverCalled(),
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(), openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(),

View file

@ -56,6 +56,7 @@ sealed interface NotificationContent {
data class RtcNotification( data class RtcNotification(
val senderId: UserId, val senderId: UserId,
val type: RtcNotificationType, val type: RtcNotificationType,
val callIntent: CallIntent,
val expirationTimestampMillis: Long val expirationTimestampMillis: Long
) : MessageLike ) : MessageLike
@ -127,3 +128,8 @@ enum class RtcNotificationType {
RING, RING,
NOTIFY NOTIFY
} }
enum class CallIntent {
AUDIO,
VIDEO
}

View file

@ -16,6 +16,7 @@ interface CallWidgetSettingsProvider {
widgetId: String = UUID.randomUUID().toString(), widgetId: String = UUID.randomUUID().toString(),
encrypted: Boolean, encrypted: Boolean,
direct: Boolean, direct: Boolean,
isAudioCall: Boolean,
hasActiveCall: Boolean, hasActiveCall: Boolean,
): MatrixWidgetSettings ): MatrixWidgetSettings
} }

View file

@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
@ -20,6 +21,7 @@ import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventContent import org.matrix.rustcomponents.sdk.TimelineEventContent
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.RtcCallIntent as SdkRtcCallIntent
import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType
class TimelineEventToNotificationContentMapper { class TimelineEventToNotificationContentMapper {
@ -83,6 +85,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification( is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification(
senderId = senderId, senderId = senderId,
type = notificationType.map(), type = notificationType.map(),
callIntent = callIntent.map(),
expirationTimestampMillis = expirationTs.toLong() expirationTimestampMillis = expirationTs.toLong()
) )
MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
@ -111,3 +114,8 @@ private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) {
SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY
SdkRtcNotificationType.RING -> RtcNotificationType.RING SdkRtcNotificationType.RING -> RtcNotificationType.RING
} }
private fun SdkRtcCallIntent?.map(): CallIntent = when (this) {
SdkRtcCallIntent.AUDIO -> CallIntent.AUDIO
else -> CallIntent.VIDEO
}

View file

@ -30,7 +30,14 @@ class DefaultCallWidgetSettingsProvider(
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider, private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : CallWidgetSettingsProvider { ) : CallWidgetSettingsProvider {
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean, hasActiveCall: Boolean): MatrixWidgetSettings { override suspend fun provide(
baseUrl: String,
widgetId: String,
encrypted: Boolean,
direct: Boolean,
isAudioCall: Boolean,
hasActiveCall: Boolean
): MatrixWidgetSettings {
val isAnalyticsEnabled = analyticsService.userConsentFlow.first() val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
val properties = VirtualElementCallWidgetProperties( val properties = VirtualElementCallWidgetProperties(
elementCallUrl = baseUrl, elementCallUrl = baseUrl,
@ -47,14 +54,18 @@ class DefaultCallWidgetSettingsProvider(
parentUrl = null, parentUrl = null,
) )
val config = VirtualElementCallWidgetConfig( val config = VirtualElementCallWidgetConfig(
// TODO remove this once we have the next EC version // // TODO remove this once we have the next EC version
preload = false, // preload = false,
// TODO remove this once we have the next EC version // // TODO remove this once we have the next EC version
skipLobby = null, // skipLobby = null,
intent = when { intent = when {
direct && hasActiveCall -> CallIntent.JOIN_EXISTING_DM direct && hasActiveCall -> {
if (isAudioCall) CallIntent.JOIN_EXISTING_DM_VOICE else CallIntent.JOIN_EXISTING_DM
}
hasActiveCall -> CallIntent.JOIN_EXISTING hasActiveCall -> CallIntent.JOIN_EXISTING
direct -> CallIntent.START_CALL_DM direct -> {
if (isAudioCall) CallIntent.START_CALL_DM_VOICE else CallIntent.START_CALL_DM
}
else -> CallIntent.START_CALL else -> CallIntent.START_CALL
}.also { }.also {
Timber.d("Starting/joining call with intent: $it") Timber.d("Starting/joining call with intent: $it")

View file

@ -12,7 +12,14 @@ import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
class FakeCallWidgetSettingsProvider( class FakeCallWidgetSettingsProvider(
private val provideFn: (String, String, Boolean, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _, _ -> MatrixWidgetSettings("id", true, "url") } private val provideFn: (
String,
String,
Boolean,
Boolean,
Boolean,
Boolean
) -> MatrixWidgetSettings = { _, _, _, _, _, _ -> MatrixWidgetSettings("id", true, "url") }
) : CallWidgetSettingsProvider { ) : CallWidgetSettingsProvider {
val providedBaseUrls = mutableListOf<String>() val providedBaseUrls = mutableListOf<String>()
@ -21,9 +28,10 @@ class FakeCallWidgetSettingsProvider(
widgetId: String, widgetId: String,
encrypted: Boolean, encrypted: Boolean,
direct: Boolean, direct: Boolean,
isAudioCall: Boolean,
hasActiveCall: Boolean hasActiveCall: Boolean
): MatrixWidgetSettings { ): MatrixWidgetSettings {
providedBaseUrls += baseUrl providedBaseUrls += baseUrl
return provideFn(baseUrl, widgetId, encrypted, direct, hasActiveCall) return provideFn(baseUrl, widgetId, encrypted, direct, isAudioCall, hasActiveCall)
} }
} }

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.exception.NotificationResolverException import io.element.android.libraries.matrix.api.exception.NotificationResolverException
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
@ -90,6 +91,7 @@ class DefaultCallNotificationEventResolver(
notificationData.run { notificationData.run {
if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) { if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) {
Timber.d("Ringing call notification intent ${content.callIntent} in room $roomId")
NotifiableRingingCallEvent( NotifiableRingingCallEvent(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -100,10 +102,18 @@ class DefaultCallNotificationEventResolver(
timestamp = this.timestamp, timestamp = this.timestamp,
isRedacted = false, isRedacted = false,
isUpdated = false, isUpdated = false,
description = stringProvider.getString(R.string.notification_incoming_call), description = if (content.callIntent ==
CallIntent.AUDIO) {
stringProvider.getString(R.string.notification_incoming_audio_call)
} else {
stringProvider.getString(
R.string.notification_incoming_call
)
},
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
roomAvatarUrl = roomAvatarUrl, roomAvatarUrl = roomAvatarUrl,
rtcNotificationType = content.type, rtcNotificationType = content.type,
callIntent = content.callIntent,
senderId = content.senderId, senderId = content.senderId,
senderAvatarUrl = senderAvatarUrl, senderAvatarUrl = senderAvatarUrl,
expirationTimestamp = content.expirationTimestampMillis, expirationTimestamp = content.expirationTimestampMillis,
@ -119,7 +129,11 @@ class DefaultCallNotificationEventResolver(
noisy = true, noisy = true,
timestamp = this.timestamp, timestamp = this.timestamp,
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
body = stringProvider.getString(R.string.notification_incoming_call), body = if (content.callIntent == CallIntent.VIDEO) {
stringProvider.getString(R.string.notification_incoming_call)
} else {
stringProvider.getString(R.string.notification_incoming_audio_call)
},
roomName = roomDisplayName, roomName = roomDisplayName,
roomIsDm = isDm, roomIsDm = isDm,
roomAvatarPath = roomAvatarUrl, roomAvatarPath = roomAvatarUrl,

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.exception.NotificationResolverException import io.element.android.libraries.matrix.api.exception.NotificationResolverException
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.push.impl.db.PushRequest import io.element.android.libraries.push.impl.db.PushRequest
import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.history.onSuccess import io.element.android.libraries.push.impl.history.onSuccess
@ -223,7 +224,11 @@ class DefaultNotificationResultProcessor(
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
Timber.i("## handleInternal() : Incoming call.") Timber.i("## handleInternal() : Incoming call.")
elementCallEntryPoint.handleIncomingCall( elementCallEntryPoint.handleIncomingCall(
callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId), callType = CallType.RoomCall(
notifiableEvent.sessionId,
notifiableEvent.roomId,
isAudioCall = notifiableEvent.callIntent == CallIntent.AUDIO
),
eventId = notifiableEvent.eventId, eventId = notifiableEvent.eventId,
senderId = notifiableEvent.senderId, senderId = notifiableEvent.senderId,
roomName = notifiableEvent.roomName, roomName = notifiableEvent.roomName,

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
data class NotifiableRingingCallEvent( data class NotifiableRingingCallEvent(
@ -29,6 +30,7 @@ data class NotifiableRingingCallEvent(
val senderAvatarUrl: String?, val senderAvatarUrl: String?,
val roomAvatarUrl: String? = null, val roomAvatarUrl: String? = null,
val rtcNotificationType: RtcNotificationType, val rtcNotificationType: RtcNotificationType,
val callIntent: CallIntent,
val timestamp: Long, val timestamp: Long,
val expirationTimestamp: Long, val expirationTimestamp: Long,
) : NotifiableEvent ) : NotifiableEvent

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
@ -64,10 +65,11 @@ class DefaultCallNotificationEventResolverTest {
senderAvatarUrl = null, senderAvatarUrl = null,
expirationTimestamp = 1567L, expirationTimestamp = 1567L,
rtcNotificationType = RtcNotificationType.RING, rtcNotificationType = RtcNotificationType.RING,
callIntent = CallIntent.VIDEO
) )
val notificationData = aNotificationData( val notificationData = aNotificationData(
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 1567) content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, CallIntent.VIDEO, 1567)
) )
val result = resolver.resolveEvent(A_SESSION_ID, notificationData) val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getOrNull()).isEqualTo(expectedResult)
@ -111,7 +113,7 @@ class DefaultCallNotificationEventResolverTest {
) )
val notificationData = aNotificationData( val notificationData = aNotificationData(
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.NOTIFY, 0) content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.NOTIFY, CallIntent.AUDIO, 0)
) )
val result = resolver.resolveEvent(A_SESSION_ID, notificationData) val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getOrNull()).isEqualTo(expectedResult)
@ -155,7 +157,7 @@ class DefaultCallNotificationEventResolverTest {
) )
val notificationData = aNotificationData( val notificationData = aNotificationData(
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 0) content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, CallIntent.VIDEO, 0)
) )
val result = resolver.resolveEvent(A_SESSION_ID, notificationData) val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getOrNull()).isEqualTo(expectedResult)

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.exception.NotificationResolverException import io.element.android.libraries.matrix.api.exception.NotificationResolverException
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
@ -739,6 +740,7 @@ class DefaultNotifiableEventResolverTest {
content = NotificationContent.MessageLike.RtcNotification( content = NotificationContent.MessageLike.RtcNotification(
A_USER_ID_2, A_USER_ID_2,
RtcNotificationType.NOTIFY, RtcNotificationType.NOTIFY,
CallIntent.VIDEO,
0 0
), ),
)) ))

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.notification.RtcNotificationType
import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -125,6 +126,7 @@ fun aNotifiableCallEvent(
rtcNotificationType: RtcNotificationType = RtcNotificationType.NOTIFY, rtcNotificationType: RtcNotificationType = RtcNotificationType.NOTIFY,
timestamp: Long = 0L, timestamp: Long = 0L,
expirationTimestamp: Long = 0L, expirationTimestamp: Long = 0L,
callIntent: CallIntent = CallIntent.VIDEO,
) = NotifiableRingingCallEvent( ) = NotifiableRingingCallEvent(
sessionId = sessionId, sessionId = sessionId,
eventId = eventId, eventId = eventId,
@ -142,6 +144,7 @@ fun aNotifiableCallEvent(
roomAvatarUrl = roomAvatarUrl, roomAvatarUrl = roomAvatarUrl,
senderAvatarUrl = senderAvatarUrl, senderAvatarUrl = senderAvatarUrl,
rtcNotificationType = rtcNotificationType, rtcNotificationType = rtcNotificationType,
callIntent = callIntent,
) )
fun aFallbackNotifiableEvent( fun aFallbackNotifiableEvent(

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4dac0f93eb31b26fa32173fbd834c7f661e4f47c79db66fa4d1536d938a4585d oid sha256:409723f9bf78cc7af140ab5798036fb17097bfdcb7e6e4d736de95a4e781015d
size 66108 size 65778

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4dac0f93eb31b26fa32173fbd834c7f661e4f47c79db66fa4d1536d938a4585d
size 66108

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4c3e5ef9368d68f661350a7a31b98b3ae3fbf975bc11d6f1b9e5ac908e6699dc oid sha256:cf3c2e90c55f3e47a93c7e86aec979b2aeb3660688962d4b7e77e0878974cf76
size 58355 size 58125

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c3e5ef9368d68f661350a7a31b98b3ae3fbf975bc11d6f1b9e5ac908e6699dc
size 58355

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:435bb5f6ffa507744590e0dc8c2d4ef82dc8afa8487263a3a47a66beaf008dd2 oid sha256:d78a84c0839258704c596870129fc20fb87d51cd3cc9617262cfd93c9b6f61fc
size 5801 size 4549

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1265548ecc92fd1071b0b57b8ded488e13f346acaf28c586328b887a170deb6b oid sha256:435bb5f6ffa507744590e0dc8c2d4ef82dc8afa8487263a3a47a66beaf008dd2
size 5350 size 5801

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 oid sha256:1265548ecc92fd1071b0b57b8ded488e13f346acaf28c586328b887a170deb6b
size 3657 size 5350

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9eb227cae4fd1ab48ea957fa72cd2dd78dd4c5228ee01254eb33a7d936763802
size 5989

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
size 3657

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2496d499e15b138ad95ee9fdcc8c06cad5c755028918fabdb70a5be7300b2bc4 oid sha256:2235c88d591b890cd208499d60a7d395d4b0926eee804257c20b634bbf83354f
size 5538 size 4485

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:087c7ec1aead0e7aa5b4cebd14bddb52e17ae1f9aeb51e7c00aab8a409b52922 oid sha256:2496d499e15b138ad95ee9fdcc8c06cad5c755028918fabdb70a5be7300b2bc4
size 5365 size 5538

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 oid sha256:087c7ec1aead0e7aa5b4cebd14bddb52e17ae1f9aeb51e7c00aab8a409b52922
size 3657 size 5365

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:013bb9352e9885a79781c25dcd864234478b1e5ce669b4c95765c25fd7af0424
size 5674

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
size 3657

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6853c47a1baae81166006f3a991a8bef0ebb8615b9a6058ec93d289d64642ceb oid sha256:155ad78cfadaab78089293eca38ab8c404f227e38c451dddbbe3c59cccb82bc5
size 37759 size 51391

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b0943712beb4e68d3a6ea828aee4c95c6ec874652a7f7e04d33e615b794c8dd1 oid sha256:8b54d16054565d3ba0280ff704c350227a03db0fad93750ad6d41f6e67f605f3
size 37674 size 51582

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ed7e04d993469a6b7385d511bb4ccaffc8dc61001fce454716c4db584ae6e971 oid sha256:7f74b2d87e7dd5e431c6d6add3131472d31c9ffcce900736ed489af2c667783c
size 78502 size 78968

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4990d60bc18f94875488edb048fda5baf6efafab6bd84341cde6aeb083b3374c oid sha256:c5ec03b736a2b1c641747a49be5417a3e9b6e034c865294bfd71a5bed8f53834
size 42658 size 42930

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fca6805fc467e91656124ed2287c170d7a4888e9c9e74c58341c44bb98767948 oid sha256:0bbf72a26a32f180d1dddc276adda01fb8586e6c182b32dde35d7daca55bc6ae
size 40505 size 40765

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9d08756e2c0dfe00c5239264c664c048156f3f49f18d07a4db5792e01ba78170 oid sha256:5fb05bc1ca83f872c88b36097fca767c36fa0688c19bd504674846707eedc358
size 41933 size 42192

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d1d5811a68186da4ef5c1eb4b5d272571326016bbc3d29630e49efcbfaea7c92 oid sha256:7fe45d21c630e31b44ae7a8cb17b9f89b0b0f67d00b3769035516f0e827f64a2
size 41841 size 42098

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0103a8787c5546373b79b9b3aa1455581b9af2e2bcc4e9b515ab6a94ba3cc6ac oid sha256:de164fd028b96c6c53ae2e33246bfb5bda1e6beba1c91cbd00111d2fbba92cd8
size 42392 size 42658

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:aa72f5bbb796ec897e7fffb85c4918d89a1cecbd224cdfa4edff365fc7370067 oid sha256:dd7a8279d58ba0420c884cdcbf1972772d2d577024c3b766cd535c5cabfe8cba
size 42923 size 43196

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:077d29a411a1315d6706a022b8ca3509224b0d36394bd0f8609d297bfd2e978f oid sha256:59d38b1cde9e22b1c47b2c7932ba980b2c56af2fa8b6b25200cb46ee3a0cd145
size 42181 size 42440

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2446123585802cf662ecf56bd971155fc128fd96db07f33e94d43a4b9b9787c1 oid sha256:de0c605c7845f2e590aba1d0484700bdecb80effc16899b6288301cd91a9d966
size 41443 size 41703

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4865d30ef99fa80c05666ef66d92777230e8e3997efb3e379a600507870ff89d oid sha256:88a2e1d2000e3034fa2d9e37b3b69aa47a7f588fee40efb8a8c74f9292315ea1
size 38649 size 39678

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fbe1e721ebc0a8fa8f4ff9a10018e2e40f17c8082404dbf30a8d48d661abe14d oid sha256:59c7e92e09d585e18f552db5e9fa55dfe79cb6af028493cafc11f0ffee8344bd
size 38605 size 39634

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9e86ae772e54f80d45945426aaefc75eb4e9f300a573b80b1b25c605bcb07b63 oid sha256:405b81267a5a14771a69c8b06efea888b2fdc588bec26872b9564a66bb5496cf
size 37836 size 38110

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3404c7ea44f0e508599ed5ef52931d240ace0521abf07d8200a18ad2ec746631 oid sha256:b7b0f5cbfc01a4539757611a9800d18ed19cb732e44eee4f1bed41788d4b0918
size 44910 size 45142

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:67c89ba55452cca28cc7a0a6f7035a1613b2c5dc35116689a00a40b66e8e9abc oid sha256:acd1c2c73c21370b816792de80d0183bacadb213cf0bf005c73695df553fad3a
size 44745 size 44979

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6f99a0af76390c056389bd0559454de5f95e65a5cec8c978951956e0e9c14b8a oid sha256:0ad520c5dc49852a8934bc85fcbcd982e500e8d9818635605abacfc51f29dfde
size 44378 size 44621

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a311ba8cdc8b092f854f262e87dc2c402812eaf4ff507baf4909939a56be4343 oid sha256:4bf88dda52209b3ff194c70a4c783a8e3765c352d58eb00cb0dd9aa16d3c6578
size 36337 size 36614

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8acfda3d05ce66e76577213eada50568fd5c8ab06219c5e3ec5456140e7de787 oid sha256:09c7b2efe4a3dba7028810fb10dbc7a3c3be4e1fc012319bd868bb3d392487ae
size 42086 size 42344

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d5d8cb157c74b3415bc138790a57021585df7b9a48833a01e2843bf4b4a199d0 oid sha256:5132f788bbfb0fe37254ebf53823f04ab0c303cc65770a3a089f8e7a3e2277e5
size 41105 size 41374

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9357e6b9fc086812a64ccbac15af5277d1be8b9d35acc94e0da855269dd8065b oid sha256:3f36006f661e4dfb2c8669983194f5b22cf54292bc975939c9a8c2832b47b799
size 38293 size 39320

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:92b4ad9b47dd662f495d0788fb28b855f723b6f12c932287060b3268d2783d85 oid sha256:51100671549be5aee9e67f2e43f22015b75edb3d39fa1f900481416d1d65e8b3
size 41643 size 42689

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fe11656c915a10c13b45ffce52634efd4b357136ec42d270114f688b41eb12c5 oid sha256:07ec8a7ab75acad9b31b6e6352f0e79cfc473fe7756c55c3101977b680136ce9
size 42866 size 43113

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f25a62cf9d5a6929f660aaf6dc15da17686a42b07e799db0f46cc00d98b6038f oid sha256:bc85e38b6ec98b12c4fe40b0338dc80033120d5224007b1e8f641165d313ca0b
size 41859 size 42119

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:df93745b7bc26a0974204af9460d246bdb27d4d99e92520abdbcd0237226033a oid sha256:2c13a57f8e460a7f6b06b1d81c10b3a2bc8dfbcda9bd2bd7c7d5224374a1b192
size 41967 size 42108

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2dc3581c56ccbfe37efb0fcae7998fb2169b57a86dc9710653f3e5fd38e60076 oid sha256:cdbbba4d6ebf3eac1d096fe13e5685aa10ab90972af322975fa2b3687737afb9
size 43401 size 43656

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:318f191a6b012a445c4bc93d52aa015bfd18fff22257787dfdccff95a4a98509 oid sha256:446a52c50158d9855269dbe62746d6a5fea94a6850d16fb29763b6b3bda67cd9
size 41223 size 41472

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e87bf7c44509e1df0884141a75ff8c6e7d186cd2511c8e8a8ae974a5123a51c2 oid sha256:0e518928f4a23a4e72cd09e300d332ce75a700ca4baf573fe48af509fe73418a
size 42662 size 42929

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:eed74faa57b8eb42b5d1a61b9cda7e9a3cad7c67e894049f8b7f17672d06f73c oid sha256:6e5cdb25bd944c13580ff2f8b5142e2c2793965bb233c7f663cdf45bde96bf26
size 42594 size 42855

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6284ae879aaed961e698854b5c7ace5221717d815aa64aa186c9560db175a16b oid sha256:a364c3b8edd68c4e4c944844db649cb22ac5db8931836a7f91d4d0f65329e4a5
size 43117 size 43374

Some files were not shown because too many files have changed in this diff Show more