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:
commit
8e13b72788
121 changed files with 599 additions and 231 deletions
|
|
@ -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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4dac0f93eb31b26fa32173fbd834c7f661e4f47c79db66fa4d1536d938a4585d
|
||||||
|
size 66108
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4c3e5ef9368d68f661350a7a31b98b3ae3fbf975bc11d6f1b9e5ac908e6699dc
|
||||||
|
size 58355
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9eb227cae4fd1ab48ea957fa72cd2dd78dd4c5228ee01254eb33a7d936763802
|
||||||
|
size 5989
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
|
||||||
|
size 3657
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:013bb9352e9885a79781c25dcd864234478b1e5ce669b4c95765c25fd7af0424
|
||||||
|
size 5674
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
|
||||||
|
size 3657
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue