mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-01 01:02:41 +00:00
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:
@@ -26,9 +26,10 @@ sealed interface CallType : NodeInputs, Parcelable {
|
||||
data class RoomCall(
|
||||
val sessionId: SessionId,
|
||||
val roomId: RoomId,
|
||||
val isAudioCall: Boolean
|
||||
) : CallType {
|
||||
override fun toString(): String {
|
||||
return "RoomCall(sessionId=$sessionId, roomId=$roomId)"
|
||||
return "RoomCall(sessionId=$sessionId, roomId=$roomId, isAudioCall=$isAudioCall)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ class DefaultElementCallEntryPoint(
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
notificationChannelId = notificationChannelId,
|
||||
textContent = textContent,
|
||||
audioOnly = callType.isAudioCall
|
||||
)
|
||||
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
|
||||
}
|
||||
|
||||
+1
@@ -29,4 +29,5 @@ data class CallNotificationData(
|
||||
val textContent: String?,
|
||||
// Expiration timestamp in millis since epoch
|
||||
val expirationTimestamp: Long,
|
||||
val audioOnly: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
+8
-2
@@ -69,6 +69,7 @@ class RingingCallNotificationCreator(
|
||||
timestamp: Long,
|
||||
expirationTimestamp: Long,
|
||||
textContent: String?,
|
||||
audioOnly: Boolean,
|
||||
): Notification? {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val imageLoader = imageLoaderHolder.get(matrixClient)
|
||||
@@ -88,7 +89,7 @@ class RingingCallNotificationCreator(
|
||||
.setImportant(true)
|
||||
.build()
|
||||
|
||||
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId))
|
||||
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, isAudioCall = audioOnly))
|
||||
val notificationData = CallNotificationData(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
@@ -101,6 +102,7 @@ class RingingCallNotificationCreator(
|
||||
timestamp = timestamp,
|
||||
textContent = textContent,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
audioOnly = audioOnly,
|
||||
)
|
||||
|
||||
val declineIntent = PendingIntentCompat.getBroadcast(
|
||||
@@ -127,7 +129,11 @@ class RingingCallNotificationCreator(
|
||||
.setSmallIcon(CommonDrawables.ic_notification)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true))
|
||||
.setStyle(
|
||||
NotificationCompat.CallStyle
|
||||
.forIncomingCall(caller, declineIntent, answerIntent)
|
||||
.setIsVideo(!audioOnly)
|
||||
)
|
||||
.addPerson(caller)
|
||||
.setAutoCancel(true)
|
||||
.setWhen(timestamp)
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = notificationData.sessionId,
|
||||
roomId = notificationData.roomId,
|
||||
),
|
||||
notificationData = notificationData,
|
||||
isAudioCall = notificationData.audioOnly
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -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
|
||||
)
|
||||
}
|
||||
+1
@@ -226,6 +226,7 @@ class CallScreenPresenter(
|
||||
sessionId = inputs.sessionId,
|
||||
roomId = inputs.roomId,
|
||||
clientId = UUID.randomUUID().toString(),
|
||||
isAudioCall = inputs.isAudioCall,
|
||||
languageTag = languageTag,
|
||||
theme = theme,
|
||||
).getOrThrow()
|
||||
|
||||
+7
-1
@@ -112,7 +112,13 @@ class IncomingCallActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
+6
-19
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 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.theme.components.Icon
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -103,7 +100,7 @@ internal fun IncomingCallScreen(
|
||||
ActionButton(
|
||||
size = 64.dp,
|
||||
onClick = { onAnswer(notificationData) },
|
||||
icon = CompoundIcons.VoiceCallSolid(),
|
||||
icon = if (notificationData.audioOnly) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
|
||||
title = stringResource(CommonStrings.action_accept),
|
||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
||||
borderColor = ElementTheme.colors.borderSuccessSubtle
|
||||
@@ -163,21 +160,11 @@ private fun ActionButton(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun IncomingCallScreenPreview() = ElementPreview {
|
||||
internal fun IncomingCallScreenPreview(
|
||||
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
|
||||
) = ElementPreview {
|
||||
IncomingCallScreen(
|
||||
notificationData = 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,
|
||||
),
|
||||
notificationData = state,
|
||||
onAnswer = {},
|
||||
onCancel = {},
|
||||
)
|
||||
|
||||
+2
@@ -146,6 +146,7 @@ class DefaultActiveCallManager(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = notificationData.sessionId,
|
||||
roomId = notificationData.roomId,
|
||||
isAudioCall = notificationData.audioOnly,
|
||||
),
|
||||
callState = CallState.Ringing(notificationData),
|
||||
)
|
||||
@@ -273,6 +274,7 @@ class DefaultActiveCallManager(
|
||||
timestamp = notificationData.timestamp,
|
||||
textContent = notificationData.textContent,
|
||||
expirationTimestamp = notificationData.expirationTimestamp,
|
||||
audioOnly = notificationData.audioOnly,
|
||||
) ?: return
|
||||
runCatchingExceptions {
|
||||
notificationManagerCompat.notify(
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ interface CallWidgetProvider {
|
||||
suspend fun getWidget(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
isAudioCall: Boolean,
|
||||
clientId: String,
|
||||
languageTag: String?,
|
||||
theme: String?,
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ class DefaultCallWidgetProvider(
|
||||
override suspend fun getWidget(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
isAudioCall: Boolean,
|
||||
clientId: String,
|
||||
languageTag: String?,
|
||||
theme: String?,
|
||||
@@ -50,6 +51,7 @@ class DefaultCallWidgetProvider(
|
||||
baseUrl = baseUrl,
|
||||
encrypted = isEncrypted,
|
||||
direct = room.isDm(),
|
||||
isAudioCall = isAudioCall,
|
||||
hasActiveCall = roomInfo.hasRoomCall,
|
||||
)
|
||||
val callUrl = room.generateWidgetWebViewUrl(
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest {
|
||||
@Test
|
||||
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
|
||||
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 intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
|
||||
@@ -53,7 +53,7 @@ class DefaultElementCallEntryPointTest {
|
||||
val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
|
||||
|
||||
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,
|
||||
senderId = A_USER_ID_2,
|
||||
roomName = "roomName",
|
||||
|
||||
+28
-1
@@ -65,7 +65,33 @@ class RingingCallNotificationCreatorTest {
|
||||
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,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
@@ -77,6 +103,7 @@ class RingingCallNotificationCreatorTest {
|
||||
timestamp = 0L,
|
||||
expirationTimestamp = 20L,
|
||||
textContent = "textContent",
|
||||
audioOnly = audioOnly
|
||||
)
|
||||
|
||||
private fun createRingingCallNotificationCreator(
|
||||
|
||||
+7
-7
@@ -90,7 +90,7 @@ class CallScreenPresenterTest {
|
||||
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
|
||||
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
widgetProvider = widgetProvider,
|
||||
screenTracker = FakeScreenTracker(analyticsLambda),
|
||||
@@ -123,7 +123,7 @@ class CallScreenPresenterTest {
|
||||
fun `present - set message interceptor, send and receive messages`() = runTest {
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
screenTracker = FakeScreenTracker {},
|
||||
)
|
||||
@@ -154,7 +154,7 @@ class CallScreenPresenterTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
navigator = navigator,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
@@ -188,7 +188,7 @@ class CallScreenPresenterTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
navigator = navigator,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
@@ -223,7 +223,7 @@ class CallScreenPresenterTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
navigator = navigator,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
@@ -260,7 +260,7 @@ class CallScreenPresenterTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
navigator = navigator,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
@@ -300,7 +300,7 @@ class CallScreenPresenterTest {
|
||||
val matrixClient = FakeMatrixClient(syncService = syncService)
|
||||
val appForegroundStateService = FakeAppForegroundStateService()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
|
||||
widgetDriver = widgetDriver,
|
||||
navigator = navigator,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
|
||||
+3
-2
@@ -27,6 +27,7 @@ class CallTypeTest {
|
||||
CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
isAudioCall = false,
|
||||
).getSessionId()
|
||||
).isEqualTo(A_SESSION_ID)
|
||||
}
|
||||
@@ -38,7 +39,7 @@ class CallTypeTest {
|
||||
|
||||
@Test
|
||||
fun `RoomCall stringification does not contain the URL`() {
|
||||
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString())
|
||||
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)")
|
||||
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false).toString())
|
||||
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)")
|
||||
}
|
||||
}
|
||||
|
||||
+29
-4
@@ -80,6 +80,7 @@ class DefaultActiveCallManagerTest {
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
isAudioCall = false,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
@@ -91,6 +92,28 @@ class DefaultActiveCallManagerTest {
|
||||
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)
|
||||
@Test
|
||||
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.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.activeWakeLock?.isHeld).isFalse()
|
||||
|
||||
@@ -192,7 +215,7 @@ class DefaultActiveCallManagerTest {
|
||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||
manager.registerIncomingCall(notificationData)
|
||||
|
||||
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false))
|
||||
|
||||
coVerify {
|
||||
room.declineCall(notificationEventId = notificationData.eventId)
|
||||
@@ -219,7 +242,7 @@ class DefaultActiveCallManagerTest {
|
||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||
// Do not register the incoming call, so the manager doesn't know about it
|
||||
manager.hangUpCall(
|
||||
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId),
|
||||
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false),
|
||||
notificationData = notificationData,
|
||||
)
|
||||
coVerify {
|
||||
@@ -321,12 +344,13 @@ class DefaultActiveCallManagerTest {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
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(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
isAudioCall = true,
|
||||
),
|
||||
callState = CallState.InCall,
|
||||
)
|
||||
@@ -429,6 +453,7 @@ class DefaultActiveCallManagerTest {
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
isAudioCall = false,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
|
||||
+7
-7
@@ -31,7 +31,7 @@ class DefaultCallWidgetProviderTest {
|
||||
@Test
|
||||
fun `getWidget - fails if the session does not exist`() = runTest {
|
||||
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
|
||||
@@ -40,7 +40,7 @@ class DefaultCallWidgetProviderTest {
|
||||
givenGetRoomResult(A_ROOM_ID, null)
|
||||
}
|
||||
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
|
||||
@@ -52,7 +52,7 @@ class DefaultCallWidgetProviderTest {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
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
|
||||
@@ -65,7 +65,7 @@ class DefaultCallWidgetProviderTest {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
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
|
||||
@@ -78,7 +78,7 @@ class DefaultCallWidgetProviderTest {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
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
|
||||
@@ -101,7 +101,7 @@ class DefaultCallWidgetProviderTest {
|
||||
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||
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
|
||||
@@ -122,7 +122,7 @@ class DefaultCallWidgetProviderTest {
|
||||
callWidgetSettingsProvider = settingsProvider,
|
||||
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")
|
||||
}
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ class FakeCallWidgetProvider(
|
||||
override suspend fun getWidget(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
isAudioCall: Boolean,
|
||||
clientId: String,
|
||||
languageTag: String?,
|
||||
theme: String?
|
||||
|
||||
+2
@@ -33,6 +33,7 @@ fun aCallNotificationData(
|
||||
timestamp: Long = 0L,
|
||||
expirationTimestamp: Long = 30_000L,
|
||||
textContent: String? = null,
|
||||
audioOnly: Boolean = false,
|
||||
): CallNotificationData = CallNotificationData(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
@@ -45,4 +46,5 @@ fun aCallNotificationData(
|
||||
timestamp = timestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
textContent = textContent,
|
||||
audioOnly = audioOnly
|
||||
)
|
||||
|
||||
+4
-2
@@ -272,10 +272,11 @@ class MessagesFlowNode(
|
||||
backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall(roomId: RoomId) {
|
||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||
val callType = CallType.RoomCall(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
isAudioCall = isAudioCall
|
||||
)
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
elementCallEntryPoint.startCall(callType)
|
||||
@@ -488,10 +489,11 @@ class MessagesFlowNode(
|
||||
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(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
isAudioCall = isAudioCall
|
||||
)
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
elementCallEntryPoint.startCall(callType)
|
||||
|
||||
+4
-2
@@ -125,7 +125,7 @@ class MessagesNode(
|
||||
fun navigateToSendLocation()
|
||||
fun navigateToCreatePoll()
|
||||
fun navigateToEditPoll(eventId: EventId)
|
||||
fun navigateToRoomCall(roomId: RoomId)
|
||||
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
|
||||
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
fun navigateToRoomDetails()
|
||||
fun navigateToPinnedMessagesList()
|
||||
@@ -279,7 +279,9 @@ class MessagesNode(
|
||||
},
|
||||
onSendLocationClick = callback::navigateToSendLocation,
|
||||
onCreatePollClick = callback::navigateToCreatePoll,
|
||||
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
|
||||
onJoinCallClick = { isAudioCall ->
|
||||
callback.navigateToRoomCall(room.roomId, isAudioCall)
|
||||
},
|
||||
onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList,
|
||||
modifier = modifier,
|
||||
knockRequestsBannerView = {
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ fun MessagesView(
|
||||
onLinkClick: (String, Boolean) -> Unit,
|
||||
onSendLocationClick: () -> Unit,
|
||||
onCreatePollClick: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
onViewAllPinnedMessagesClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
forceJumpToBottomVisibility: Boolean = false,
|
||||
@@ -423,7 +423,7 @@ private fun MessagesViewContent(
|
||||
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
||||
onSendLocationClick: () -> Unit,
|
||||
onCreatePollClick: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
onViewAllPinnedMessagesClick: () -> Unit,
|
||||
forceJumpToBottomVisibility: Boolean,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
|
||||
+4
-2
@@ -130,7 +130,7 @@ class ThreadedMessagesNode(
|
||||
fun navigateToSendLocation()
|
||||
fun navigateToCreatePoll()
|
||||
fun navigateToEditPoll(eventId: EventId)
|
||||
fun navigateToRoomCall(roomId: RoomId)
|
||||
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
|
||||
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
}
|
||||
|
||||
@@ -281,7 +281,9 @@ class ThreadedMessagesNode(
|
||||
},
|
||||
onSendLocationClick = callback::navigateToSendLocation,
|
||||
onCreatePollClick = callback::navigateToCreatePoll,
|
||||
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
|
||||
onJoinCallClick = { isAudioCall ->
|
||||
callback.navigateToRoomCall(room.roomId, isAudioCall)
|
||||
},
|
||||
onViewAllPinnedMessagesClick = {},
|
||||
modifier = modifier,
|
||||
knockRequestsBannerView = {},
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ fun TimelineView(
|
||||
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
forceJumpToBottomVisibility: Boolean = false,
|
||||
|
||||
+31
-13
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -35,7 +36,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
internal fun CallMenuItem(
|
||||
roomCallState: RoomCallState,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (roomCallState) {
|
||||
@@ -52,7 +53,7 @@ internal fun CallMenuItem(
|
||||
is RoomCallState.OnGoing -> {
|
||||
OnGoingCallMenuItem(
|
||||
roomCallState = roomCallState,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
onJoinCallClick = { onJoinCallClick(roomCallState.isAudioCall) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -62,18 +63,31 @@ internal fun CallMenuItem(
|
||||
@Composable
|
||||
private fun StandByCallMenuItem(
|
||||
roomCallState: RoomCallState.StandBy,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
onClick = onJoinCallClick,
|
||||
enabled = roomCallState.canStartCall,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.VideoCallSolid(),
|
||||
contentDescription = stringResource(CommonStrings.a11y_start_call),
|
||||
)
|
||||
Row(modifier = modifier) {
|
||||
// Only show voice call in DMs
|
||||
if (roomCallState.isDM) {
|
||||
IconButton(
|
||||
onClick = { onJoinCallClick(true) },
|
||||
enabled = roomCallState.canStartCall,
|
||||
) {
|
||||
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(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = CompoundIcons.VideoCallSolid(),
|
||||
imageVector = if (roomCallState.isAudioCall) {
|
||||
CompoundIcons.VoiceCallSolid()
|
||||
} else {
|
||||
CompoundIcons.VideoCallSolid()
|
||||
},
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ internal fun TimelineItemCallNotifyView(
|
||||
event: TimelineItem.Event,
|
||||
roomCallState: RoomCallState,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ internal fun TimelineItemRow(
|
||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
eventSink: (TimelineEvent.TimelineItemEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ internal fun MessagesViewTopBar(
|
||||
dmUserIdentityState: IdentityState?,
|
||||
sharedHistoryIcon: SharedHistoryIcon,
|
||||
onRoomDetailsClick: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
+21
-2
@@ -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.model.TimelineItem
|
||||
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.UserId
|
||||
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.clickOn
|
||||
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.setSafeContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -122,7 +124,7 @@ class MessagesViewTest {
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
ensureCalledOnceWithParam(false) { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
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
|
||||
fun `clicking on an Event invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
|
||||
@@ -609,7 +628,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setSafeContent {
|
||||
|
||||
+1
-2
@@ -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.user.MatrixUser
|
||||
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.EnsureNeverCalledWithTwoParams
|
||||
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(),
|
||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||
forceJumpToBottomVisibility: Boolean = false,
|
||||
) {
|
||||
setSafeContent(clearAndroidUiDispatcher = true) {
|
||||
|
||||
+2
@@ -18,10 +18,12 @@ sealed interface RoomCallState {
|
||||
|
||||
data class StandBy(
|
||||
val canStartCall: Boolean,
|
||||
val isDM: Boolean,
|
||||
) : RoomCallState
|
||||
|
||||
data class OnGoing(
|
||||
val canJoinCall: Boolean,
|
||||
val isAudioCall: Boolean,
|
||||
val isUserInTheCall: Boolean,
|
||||
val isUserLocallyInTheCall: Boolean,
|
||||
) : RoomCallState
|
||||
|
||||
+6
@@ -14,9 +14,11 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
|
||||
override val values: Sequence<RoomCallState> = sequenceOf(
|
||||
aStandByCallState(),
|
||||
aStandByCallState(canStartCall = false),
|
||||
aStandByCallState(canStartCall = false, isDM = true),
|
||||
anOngoingCallState(),
|
||||
anOngoingCallState(canJoinCall = false),
|
||||
anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
|
||||
anOngoingCallState(canJoinCall = true, isAudioCall = true),
|
||||
RoomCallState.Unavailable,
|
||||
)
|
||||
}
|
||||
@@ -25,14 +27,18 @@ fun anOngoingCallState(
|
||||
canJoinCall: Boolean = true,
|
||||
isUserInTheCall: Boolean = false,
|
||||
isUserLocallyInTheCall: Boolean = isUserInTheCall,
|
||||
isAudioCall: Boolean = false,
|
||||
) = RoomCallState.OnGoing(
|
||||
canJoinCall = canJoinCall,
|
||||
isUserInTheCall = isUserInTheCall,
|
||||
isUserLocallyInTheCall = isUserLocallyInTheCall,
|
||||
isAudioCall = isAudioCall
|
||||
)
|
||||
|
||||
fun aStandByCallState(
|
||||
canStartCall: Boolean = true,
|
||||
isDM: Boolean = false,
|
||||
) = RoomCallState.StandBy(
|
||||
canStartCall = canStartCall,
|
||||
isDM
|
||||
)
|
||||
|
||||
+7
-1
@@ -21,6 +21,7 @@ import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.permissionsAsState
|
||||
|
||||
@@ -56,8 +57,13 @@ class RoomCallStatePresenter(
|
||||
canJoinCall = canJoinCall,
|
||||
isUserInTheCall = isUserInTheCall,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -41,6 +41,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(initialState).isEqualTo(
|
||||
RoomCallState.StandBy(
|
||||
canStartCall = false,
|
||||
isDM = false
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -79,6 +80,28 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(initialState).isEqualTo(
|
||||
RoomCallState.StandBy(
|
||||
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(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = false,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = false,
|
||||
isUserLocallyInTheCall = false,
|
||||
)
|
||||
@@ -125,6 +149,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = true,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = true,
|
||||
isUserLocallyInTheCall = false,
|
||||
)
|
||||
@@ -155,6 +180,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = true,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = true,
|
||||
isUserLocallyInTheCall = true,
|
||||
)
|
||||
@@ -187,6 +213,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = true,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = true,
|
||||
isUserLocallyInTheCall = true,
|
||||
)
|
||||
@@ -195,6 +222,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = true,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = true,
|
||||
isUserLocallyInTheCall = false,
|
||||
)
|
||||
@@ -208,6 +236,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.OnGoing(
|
||||
canJoinCall = true,
|
||||
isAudioCall = false,
|
||||
isUserInTheCall = false,
|
||||
isUserLocallyInTheCall = false,
|
||||
)
|
||||
@@ -221,6 +250,7 @@ class RoomCallStatePresenterTest {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RoomCallState.StandBy(
|
||||
canStartCall = true,
|
||||
isDM = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+11
-3
@@ -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.UserId
|
||||
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.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
@@ -223,10 +224,11 @@ class RoomDetailsFlowNode(
|
||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall() {
|
||||
override fun navigateToRoomCall(callIntent: CallIntent) {
|
||||
val inputs = CallType.RoomCall(
|
||||
sessionId = room.sessionId,
|
||||
roomId = room.roomId,
|
||||
isAudioCall = callIntent == CallIntent.AUDIO
|
||||
)
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
elementCallEntryPoint.startCall(inputs)
|
||||
@@ -284,8 +286,14 @@ class RoomDetailsFlowNode(
|
||||
callback.navigateToRoom(roomId, emptyList())
|
||||
}
|
||||
|
||||
override fun startCall(dmRoomId: RoomId) {
|
||||
elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId))
|
||||
override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
|
||||
elementCallEntryPoint.startCall(
|
||||
CallType.RoomCall(
|
||||
roomId = dmRoomId,
|
||||
sessionId = room.sessionId,
|
||||
isAudioCall = callIntent == CallIntent.AUDIO
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun startVerifyUserFlow(userId: UserId) {
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
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.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -59,7 +60,7 @@ class RoomDetailsNode(
|
||||
fun navigateToKnockRequestsList()
|
||||
fun navigateToSecurityAndPrivacy()
|
||||
fun navigateToRoomMemberDetails(userId: UserId)
|
||||
fun navigateToRoomCall()
|
||||
fun navigateToRoomCall(callIntent: CallIntent)
|
||||
fun navigateToReportRoom()
|
||||
fun navigateToSelectNewOwnersWhenLeaving()
|
||||
}
|
||||
|
||||
+14
-4
@@ -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.RoomId
|
||||
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.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
@@ -105,7 +106,7 @@ fun RoomDetailsView(
|
||||
openPollHistory: () -> Unit,
|
||||
openMediaGallery: () -> Unit,
|
||||
openAdminSettings: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onJoinCallClick: (CallIntent) -> Unit,
|
||||
onPinnedMessagesClick: () -> Unit,
|
||||
onKnockRequestsClick: () -> Unit,
|
||||
onSecurityAndPrivacyClick: () -> Unit,
|
||||
@@ -327,7 +328,7 @@ private fun MainActionsSection(
|
||||
state: RoomDetailsState,
|
||||
onShareRoom: () -> Unit,
|
||||
onInvitePeople: () -> Unit,
|
||||
onCall: () -> Unit,
|
||||
onCall: (callIntent: CallIntent) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -356,10 +357,19 @@ private fun MainActionsSection(
|
||||
}
|
||||
if (state.roomCallState.hasPermissionToJoin()) {
|
||||
// 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(
|
||||
title = stringResource(CommonStrings.action_call),
|
||||
title = stringResource(CommonStrings.common_video),
|
||||
imageVector = CompoundIcons.VideoCall(),
|
||||
onClick = onCall,
|
||||
onClick = { onCall(CallIntent.VIDEO) },
|
||||
)
|
||||
}
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
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.notification.CallIntent
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@@ -67,8 +68,8 @@ class RoomMemberDetailsNode(
|
||||
callback.navigateToRoom(roomId)
|
||||
}
|
||||
|
||||
fun onStartCall(roomId: RoomId) {
|
||||
callback.startCall(roomId)
|
||||
fun onStartCall(roomId: RoomId, callIntent: CallIntent) {
|
||||
callback.startCall(roomId, callIntent)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
|
||||
+22
-3
@@ -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.userprofile.shared.aUserProfileState
|
||||
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.test.A_USER_ID
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
@@ -121,7 +122,25 @@ class RoomDetailsViewTest {
|
||||
|
||||
@Test
|
||||
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(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
@@ -129,7 +148,7 @@ class RoomDetailsViewTest {
|
||||
),
|
||||
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(),
|
||||
openMediaGallery: () -> Unit = EnsureNeverCalled(),
|
||||
openAdminSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: (CallIntent) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
|
||||
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
+9
-2
@@ -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.SessionId
|
||||
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.mediaviewer.api.MediaViewerEntryPoint
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -83,8 +84,14 @@ class UserProfileFlowNode(
|
||||
callback.navigateToRoom(roomId)
|
||||
}
|
||||
|
||||
override fun startCall(dmRoomId: RoomId) {
|
||||
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId))
|
||||
override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
|
||||
elementCallEntryPoint.startCall(
|
||||
CallType.RoomCall(
|
||||
sessionId = sessionId,
|
||||
roomId = dmRoomId,
|
||||
isAudioCall = callIntent == CallIntent.AUDIO
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun startVerifyUserFlow(userId: UserId) {
|
||||
|
||||
+26
-3
@@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
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
|
||||
|
||||
@Composable
|
||||
@@ -26,11 +29,13 @@ fun UserProfileMainActionsSection(
|
||||
canCall: Boolean,
|
||||
onShareUser: () -> Unit,
|
||||
onStartDM: () -> Unit,
|
||||
onCall: () -> Unit,
|
||||
onCall: (CallIntent) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
if (!isCurrentUser) {
|
||||
@@ -43,8 +48,14 @@ fun UserProfileMainActionsSection(
|
||||
if (canCall) {
|
||||
MainActionButton(
|
||||
title = stringResource(CommonStrings.action_call),
|
||||
imageVector = CompoundIcons.VoiceCall(),
|
||||
onClick = { onCall(CallIntent.AUDIO) },
|
||||
)
|
||||
|
||||
MainActionButton(
|
||||
title = stringResource(CommonStrings.common_video),
|
||||
imageVector = CompoundIcons.VideoCall(),
|
||||
onClick = onCall,
|
||||
onClick = { onCall(CallIntent.VIDEO) },
|
||||
)
|
||||
}
|
||||
MainActionButton(
|
||||
@@ -54,3 +65,15 @@ fun UserProfileMainActionsSection(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight()
|
||||
@Composable
|
||||
internal fun UserProfileMainActionsSectionPreview() = ElementPreview {
|
||||
UserProfileMainActionsSection(
|
||||
isCurrentUser = false,
|
||||
canCall = true,
|
||||
onShareUser = { },
|
||||
onStartDM = { },
|
||||
onCall = { }
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
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.notification.CallIntent
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
@@ -24,7 +25,7 @@ class UserProfileNodeHelper(
|
||||
interface Callback : NodeInputs {
|
||||
fun navigateToAvatarPreview(username: String, avatarUrl: String)
|
||||
fun navigateToRoom(roomId: RoomId)
|
||||
fun startCall(dmRoomId: RoomId)
|
||||
fun startCall(dmRoomId: RoomId, callIntent: CallIntent)
|
||||
fun startVerifyUserFlow(userId: UserId)
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -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.matrix.api.core.RoomId
|
||||
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.ui.strings.CommonStrings
|
||||
|
||||
@@ -52,7 +53,7 @@ fun UserProfileView(
|
||||
state: UserProfileState,
|
||||
onShareUser: () -> Unit,
|
||||
onOpenDm: (RoomId) -> Unit,
|
||||
onStartCall: (RoomId) -> Unit,
|
||||
onStartCall: (RoomId, CallIntent) -> Unit,
|
||||
goBack: () -> Unit,
|
||||
openAvatarPreview: (username: String, url: String) -> Unit,
|
||||
onVerifyClick: (UserId) -> Unit,
|
||||
@@ -90,7 +91,7 @@ fun UserProfileView(
|
||||
canCall = state.canCall,
|
||||
onShareUser = onShareUser,
|
||||
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))
|
||||
if (!state.isCurrentUser) {
|
||||
@@ -151,7 +152,7 @@ internal fun UserProfileViewPreview(
|
||||
onShareUser = {},
|
||||
goBack = {},
|
||||
onOpenDm = {},
|
||||
onStartCall = {},
|
||||
onStartCall = { _, _ -> },
|
||||
openAvatarPreview = { _, _ -> },
|
||||
onVerifyClick = {},
|
||||
)
|
||||
|
||||
+17
-2
@@ -23,6 +23,7 @@ import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.notification.CallIntent
|
||||
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_USER_ID
|
||||
@@ -105,7 +106,7 @@ class UserProfileViewTest {
|
||||
|
||||
@Test
|
||||
fun `on Call clicked - the expected callback is called`() = runTest {
|
||||
ensureCalledOnceWithParam(A_ROOM_ID) { callback ->
|
||||
ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback ->
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
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")
|
||||
@Test
|
||||
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(),
|
||||
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onStartCall: (RoomId, CallIntent) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
|
||||
+6
@@ -56,6 +56,7 @@ sealed interface NotificationContent {
|
||||
data class RtcNotification(
|
||||
val senderId: UserId,
|
||||
val type: RtcNotificationType,
|
||||
val callIntent: CallIntent,
|
||||
val expirationTimestampMillis: Long
|
||||
) : MessageLike
|
||||
|
||||
@@ -127,3 +128,8 @@ enum class RtcNotificationType {
|
||||
RING,
|
||||
NOTIFY
|
||||
}
|
||||
|
||||
enum class CallIntent {
|
||||
AUDIO,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ interface CallWidgetSettingsProvider {
|
||||
widgetId: String = UUID.randomUUID().toString(),
|
||||
encrypted: Boolean,
|
||||
direct: Boolean,
|
||||
isAudioCall: Boolean,
|
||||
hasActiveCall: Boolean,
|
||||
): MatrixWidgetSettings
|
||||
}
|
||||
|
||||
+8
@@ -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.matrix.api.core.EventId
|
||||
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.RtcNotificationType
|
||||
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.TimelineEventContent
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.RtcCallIntent as SdkRtcCallIntent
|
||||
import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType
|
||||
|
||||
class TimelineEventToNotificationContentMapper {
|
||||
@@ -83,6 +85,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
||||
is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification(
|
||||
senderId = senderId,
|
||||
type = notificationType.map(),
|
||||
callIntent = callIntent.map(),
|
||||
expirationTimestampMillis = expirationTs.toLong()
|
||||
)
|
||||
MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
|
||||
@@ -111,3 +114,8 @@ private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) {
|
||||
SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY
|
||||
SdkRtcNotificationType.RING -> RtcNotificationType.RING
|
||||
}
|
||||
|
||||
private fun SdkRtcCallIntent?.map(): CallIntent = when (this) {
|
||||
SdkRtcCallIntent.AUDIO -> CallIntent.AUDIO
|
||||
else -> CallIntent.VIDEO
|
||||
}
|
||||
|
||||
+18
-7
@@ -30,7 +30,14 @@ class DefaultCallWidgetSettingsProvider(
|
||||
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : 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 properties = VirtualElementCallWidgetProperties(
|
||||
elementCallUrl = baseUrl,
|
||||
@@ -47,14 +54,18 @@ class DefaultCallWidgetSettingsProvider(
|
||||
parentUrl = null,
|
||||
)
|
||||
val config = VirtualElementCallWidgetConfig(
|
||||
// TODO remove this once we have the next EC version
|
||||
preload = false,
|
||||
// TODO remove this once we have the next EC version
|
||||
skipLobby = null,
|
||||
// // TODO remove this once we have the next EC version
|
||||
// preload = false,
|
||||
// // TODO remove this once we have the next EC version
|
||||
// skipLobby = null,
|
||||
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
|
||||
direct -> CallIntent.START_CALL_DM
|
||||
direct -> {
|
||||
if (isAudioCall) CallIntent.START_CALL_DM_VOICE else CallIntent.START_CALL_DM
|
||||
}
|
||||
else -> CallIntent.START_CALL
|
||||
}.also {
|
||||
Timber.d("Starting/joining call with intent: $it")
|
||||
|
||||
+10
-2
@@ -12,7 +12,14 @@ import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
|
||||
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 {
|
||||
val providedBaseUrls = mutableListOf<String>()
|
||||
|
||||
@@ -21,9 +28,10 @@ class FakeCallWidgetSettingsProvider(
|
||||
widgetId: String,
|
||||
encrypted: Boolean,
|
||||
direct: Boolean,
|
||||
isAudioCall: Boolean,
|
||||
hasActiveCall: Boolean
|
||||
): MatrixWidgetSettings {
|
||||
providedBaseUrls += baseUrl
|
||||
return provideFn(baseUrl, widgetId, encrypted, direct, hasActiveCall)
|
||||
return provideFn(baseUrl, widgetId, encrypted, direct, isAudioCall, hasActiveCall)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -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.core.SessionId
|
||||
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.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
@@ -90,6 +91,7 @@ class DefaultCallNotificationEventResolver(
|
||||
|
||||
notificationData.run {
|
||||
if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) {
|
||||
Timber.d("Ringing call notification intent ${content.callIntent} in room $roomId")
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
@@ -100,10 +102,18 @@ class DefaultCallNotificationEventResolver(
|
||||
timestamp = this.timestamp,
|
||||
isRedacted = 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),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
rtcNotificationType = content.type,
|
||||
callIntent = content.callIntent,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
expirationTimestamp = content.expirationTimestampMillis,
|
||||
@@ -119,7 +129,11 @@ class DefaultCallNotificationEventResolver(
|
||||
noisy = true,
|
||||
timestamp = this.timestamp,
|
||||
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,
|
||||
roomIsDm = isDm,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
|
||||
+6
-1
@@ -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.SessionId
|
||||
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.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.onSuccess
|
||||
@@ -223,7 +224,11 @@ class DefaultNotificationResultProcessor(
|
||||
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
||||
Timber.i("## handleInternal() : Incoming call.")
|
||||
elementCallEntryPoint.handleIncomingCall(
|
||||
callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId),
|
||||
callType = CallType.RoomCall(
|
||||
notifiableEvent.sessionId,
|
||||
notifiableEvent.roomId,
|
||||
isAudioCall = notifiableEvent.callIntent == CallIntent.AUDIO
|
||||
),
|
||||
eventId = notifiableEvent.eventId,
|
||||
senderId = notifiableEvent.senderId,
|
||||
roomName = notifiableEvent.roomName,
|
||||
|
||||
+2
@@ -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.SessionId
|
||||
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
|
||||
|
||||
data class NotifiableRingingCallEvent(
|
||||
@@ -29,6 +30,7 @@ data class NotifiableRingingCallEvent(
|
||||
val senderAvatarUrl: String?,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val rtcNotificationType: RtcNotificationType,
|
||||
val callIntent: CallIntent,
|
||||
val timestamp: Long,
|
||||
val expirationTimestamp: Long,
|
||||
) : NotifiableEvent
|
||||
|
||||
+5
-3
@@ -9,6 +9,7 @@
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
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.RtcNotificationType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -64,10 +65,11 @@ class DefaultCallNotificationEventResolverTest {
|
||||
senderAvatarUrl = null,
|
||||
expirationTimestamp = 1567L,
|
||||
rtcNotificationType = RtcNotificationType.RING,
|
||||
callIntent = CallIntent.VIDEO
|
||||
)
|
||||
|
||||
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)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
@@ -111,7 +113,7 @@ class DefaultCallNotificationEventResolverTest {
|
||||
)
|
||||
|
||||
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)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
@@ -155,7 +157,7 @@ class DefaultCallNotificationEventResolverTest {
|
||||
)
|
||||
|
||||
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)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
|
||||
+2
@@ -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.exception.NotificationResolverException
|
||||
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.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
@@ -739,6 +740,7 @@ class DefaultNotifiableEventResolverTest {
|
||||
content = NotificationContent.MessageLike.RtcNotification(
|
||||
A_USER_ID_2,
|
||||
RtcNotificationType.NOTIFY,
|
||||
CallIntent.VIDEO,
|
||||
0
|
||||
),
|
||||
))
|
||||
|
||||
+3
@@ -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.ThreadId
|
||||
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.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
@@ -125,6 +126,7 @@ fun aNotifiableCallEvent(
|
||||
rtcNotificationType: RtcNotificationType = RtcNotificationType.NOTIFY,
|
||||
timestamp: Long = 0L,
|
||||
expirationTimestamp: Long = 0L,
|
||||
callIntent: CallIntent = CallIntent.VIDEO,
|
||||
) = NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
@@ -142,6 +144,7 @@ fun aNotifiableCallEvent(
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
rtcNotificationType = rtcNotificationType,
|
||||
callIntent = callIntent,
|
||||
)
|
||||
|
||||
fun aFallbackNotifiableEvent(
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6853c47a1baae81166006f3a991a8bef0ebb8615b9a6058ec93d289d64642ceb
|
||||
size 37759
|
||||
oid sha256:155ad78cfadaab78089293eca38ab8c404f227e38c451dddbbe3c59cccb82bc5
|
||||
size 51391
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b0943712beb4e68d3a6ea828aee4c95c6ec874652a7f7e04d33e615b794c8dd1
|
||||
size 37674
|
||||
oid sha256:8b54d16054565d3ba0280ff704c350227a03db0fad93750ad6d41f6e67f605f3
|
||||
size 51582
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed7e04d993469a6b7385d511bb4ccaffc8dc61001fce454716c4db584ae6e971
|
||||
size 78502
|
||||
oid sha256:7f74b2d87e7dd5e431c6d6add3131472d31c9ffcce900736ed489af2c667783c
|
||||
size 78968
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4990d60bc18f94875488edb048fda5baf6efafab6bd84341cde6aeb083b3374c
|
||||
size 42658
|
||||
oid sha256:c5ec03b736a2b1c641747a49be5417a3e9b6e034c865294bfd71a5bed8f53834
|
||||
size 42930
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fca6805fc467e91656124ed2287c170d7a4888e9c9e74c58341c44bb98767948
|
||||
size 40505
|
||||
oid sha256:0bbf72a26a32f180d1dddc276adda01fb8586e6c182b32dde35d7daca55bc6ae
|
||||
size 40765
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d08756e2c0dfe00c5239264c664c048156f3f49f18d07a4db5792e01ba78170
|
||||
size 41933
|
||||
oid sha256:5fb05bc1ca83f872c88b36097fca767c36fa0688c19bd504674846707eedc358
|
||||
size 42192
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d1d5811a68186da4ef5c1eb4b5d272571326016bbc3d29630e49efcbfaea7c92
|
||||
size 41841
|
||||
oid sha256:7fe45d21c630e31b44ae7a8cb17b9f89b0b0f67d00b3769035516f0e827f64a2
|
||||
size 42098
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0103a8787c5546373b79b9b3aa1455581b9af2e2bcc4e9b515ab6a94ba3cc6ac
|
||||
size 42392
|
||||
oid sha256:de164fd028b96c6c53ae2e33246bfb5bda1e6beba1c91cbd00111d2fbba92cd8
|
||||
size 42658
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa72f5bbb796ec897e7fffb85c4918d89a1cecbd224cdfa4edff365fc7370067
|
||||
size 42923
|
||||
oid sha256:dd7a8279d58ba0420c884cdcbf1972772d2d577024c3b766cd535c5cabfe8cba
|
||||
size 43196
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:077d29a411a1315d6706a022b8ca3509224b0d36394bd0f8609d297bfd2e978f
|
||||
size 42181
|
||||
oid sha256:59d38b1cde9e22b1c47b2c7932ba980b2c56af2fa8b6b25200cb46ee3a0cd145
|
||||
size 42440
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2446123585802cf662ecf56bd971155fc128fd96db07f33e94d43a4b9b9787c1
|
||||
size 41443
|
||||
oid sha256:de0c605c7845f2e590aba1d0484700bdecb80effc16899b6288301cd91a9d966
|
||||
size 41703
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4865d30ef99fa80c05666ef66d92777230e8e3997efb3e379a600507870ff89d
|
||||
size 38649
|
||||
oid sha256:88a2e1d2000e3034fa2d9e37b3b69aa47a7f588fee40efb8a8c74f9292315ea1
|
||||
size 39678
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fbe1e721ebc0a8fa8f4ff9a10018e2e40f17c8082404dbf30a8d48d661abe14d
|
||||
size 38605
|
||||
oid sha256:59c7e92e09d585e18f552db5e9fa55dfe79cb6af028493cafc11f0ffee8344bd
|
||||
size 39634
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e86ae772e54f80d45945426aaefc75eb4e9f300a573b80b1b25c605bcb07b63
|
||||
size 37836
|
||||
oid sha256:405b81267a5a14771a69c8b06efea888b2fdc588bec26872b9564a66bb5496cf
|
||||
size 38110
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3404c7ea44f0e508599ed5ef52931d240ace0521abf07d8200a18ad2ec746631
|
||||
size 44910
|
||||
oid sha256:b7b0f5cbfc01a4539757611a9800d18ed19cb732e44eee4f1bed41788d4b0918
|
||||
size 45142
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67c89ba55452cca28cc7a0a6f7035a1613b2c5dc35116689a00a40b66e8e9abc
|
||||
size 44745
|
||||
oid sha256:acd1c2c73c21370b816792de80d0183bacadb213cf0bf005c73695df553fad3a
|
||||
size 44979
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f99a0af76390c056389bd0559454de5f95e65a5cec8c978951956e0e9c14b8a
|
||||
size 44378
|
||||
oid sha256:0ad520c5dc49852a8934bc85fcbcd982e500e8d9818635605abacfc51f29dfde
|
||||
size 44621
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a311ba8cdc8b092f854f262e87dc2c402812eaf4ff507baf4909939a56be4343
|
||||
size 36337
|
||||
oid sha256:4bf88dda52209b3ff194c70a4c783a8e3765c352d58eb00cb0dd9aa16d3c6578
|
||||
size 36614
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8acfda3d05ce66e76577213eada50568fd5c8ab06219c5e3ec5456140e7de787
|
||||
size 42086
|
||||
oid sha256:09c7b2efe4a3dba7028810fb10dbc7a3c3be4e1fc012319bd868bb3d392487ae
|
||||
size 42344
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d5d8cb157c74b3415bc138790a57021585df7b9a48833a01e2843bf4b4a199d0
|
||||
size 41105
|
||||
oid sha256:5132f788bbfb0fe37254ebf53823f04ab0c303cc65770a3a089f8e7a3e2277e5
|
||||
size 41374
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9357e6b9fc086812a64ccbac15af5277d1be8b9d35acc94e0da855269dd8065b
|
||||
size 38293
|
||||
oid sha256:3f36006f661e4dfb2c8669983194f5b22cf54292bc975939c9a8c2832b47b799
|
||||
size 39320
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:92b4ad9b47dd662f495d0788fb28b855f723b6f12c932287060b3268d2783d85
|
||||
size 41643
|
||||
oid sha256:51100671549be5aee9e67f2e43f22015b75edb3d39fa1f900481416d1d65e8b3
|
||||
size 42689
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe11656c915a10c13b45ffce52634efd4b357136ec42d270114f688b41eb12c5
|
||||
size 42866
|
||||
oid sha256:07ec8a7ab75acad9b31b6e6352f0e79cfc473fe7756c55c3101977b680136ce9
|
||||
size 43113
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f25a62cf9d5a6929f660aaf6dc15da17686a42b07e799db0f46cc00d98b6038f
|
||||
size 41859
|
||||
oid sha256:bc85e38b6ec98b12c4fe40b0338dc80033120d5224007b1e8f641165d313ca0b
|
||||
size 42119
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df93745b7bc26a0974204af9460d246bdb27d4d99e92520abdbcd0237226033a
|
||||
size 41967
|
||||
oid sha256:2c13a57f8e460a7f6b06b1d81c10b3a2bc8dfbcda9bd2bd7c7d5224374a1b192
|
||||
size 42108
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2dc3581c56ccbfe37efb0fcae7998fb2169b57a86dc9710653f3e5fd38e60076
|
||||
size 43401
|
||||
oid sha256:cdbbba4d6ebf3eac1d096fe13e5685aa10ab90972af322975fa2b3687737afb9
|
||||
size 43656
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:318f191a6b012a445c4bc93d52aa015bfd18fff22257787dfdccff95a4a98509
|
||||
size 41223
|
||||
oid sha256:446a52c50158d9855269dbe62746d6a5fea94a6850d16fb29763b6b3bda67cd9
|
||||
size 41472
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e87bf7c44509e1df0884141a75ff8c6e7d186cd2511c8e8a8ae974a5123a51c2
|
||||
size 42662
|
||||
oid sha256:0e518928f4a23a4e72cd09e300d332ce75a700ca4baf573fe48af509fe73418a
|
||||
size 42929
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eed74faa57b8eb42b5d1a61b9cda7e9a3cad7c67e894049f8b7f17672d06f73c
|
||||
size 42594
|
||||
oid sha256:6e5cdb25bd944c13580ff2f8b5142e2c2793965bb233c7f663cdf45bde96bf26
|
||||
size 42855
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6284ae879aaed961e698854b5c7ace5221717d815aa64aa186c9560db175a16b
|
||||
size 43117
|
||||
oid sha256:a364c3b8edd68c4e4c944844db649cb22ac5db8931836a7f91d4d0f65329e4a5
|
||||
size 43374
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user