mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 12:54:35 +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(
|
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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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
|
||||||
|
|||||||
+8
-2
@@ -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)
|
||||||
|
|||||||
+2
-2
@@ -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,
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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,
|
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()
|
||||||
|
|||||||
+7
-1
@@ -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() {
|
||||||
|
|||||||
+6
-19
@@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|||||||
+2
@@ -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(
|
||||||
|
|||||||
+1
@@ -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?,
|
||||||
|
|||||||
+2
@@ -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(
|
||||||
|
|||||||
+2
-2
@@ -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",
|
||||||
|
|||||||
+28
-1
@@ -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(
|
||||||
|
|||||||
+7
-7
@@ -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),
|
||||||
|
|||||||
+3
-2
@@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-4
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-7
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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?
|
||||||
|
|||||||
+2
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-2
@@ -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)
|
||||||
|
|||||||
+4
-2
@@ -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 = {
|
||||||
|
|||||||
+2
-2
@@ -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,
|
||||||
|
|||||||
+4
-2
@@ -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 = {},
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
+24
-6
@@ -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,12 +63,24 @@ 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,
|
||||||
) {
|
) {
|
||||||
|
Row(modifier = modifier) {
|
||||||
|
// Only show voice call in DMs
|
||||||
|
if (roomCallState.isDM) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = modifier,
|
onClick = { onJoinCallClick(true) },
|
||||||
onClick = onJoinCallClick,
|
enabled = roomCallState.canStartCall,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.VoiceCallSolid(),
|
||||||
|
contentDescription = stringResource(CommonStrings.a11y_start_voice_call),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { onJoinCallClick(false) },
|
||||||
enabled = roomCallState.canStartCall,
|
enabled = roomCallState.canStartCall,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -76,6 +89,7 @@ private fun StandByCallMenuItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OnGoingCallMenuItem(
|
private fun OnGoingCallMenuItem(
|
||||||
@@ -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))
|
||||||
|
|||||||
+1
-1
@@ -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(
|
||||||
|
|||||||
+1
-1
@@ -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 =
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+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.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 {
|
||||||
|
|||||||
+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.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) {
|
||||||
|
|||||||
+2
@@ -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
|
||||||
|
|||||||
+6
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+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.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+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.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) {
|
||||||
|
|||||||
+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.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()
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -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(
|
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) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.roomType is RoomDetailsType.Room) {
|
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.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()
|
||||||
|
|||||||
+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.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(),
|
||||||
|
|||||||
+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.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) {
|
||||||
|
|||||||
+26
-3
@@ -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 = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+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.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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.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 = {},
|
||||||
)
|
)
|
||||||
|
|||||||
+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.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(),
|
||||||
|
|||||||
+6
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+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.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
|
||||||
|
}
|
||||||
|
|||||||
+18
-7
@@ -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")
|
||||||
|
|||||||
+10
-2
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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.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,
|
||||||
|
|||||||
+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.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,
|
||||||
|
|||||||
+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.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
|
||||||
|
|||||||
+5
-3
@@ -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)
|
||||||
|
|||||||
+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.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
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|||||||
+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.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(
|
||||||
|
|||||||
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6853c47a1baae81166006f3a991a8bef0ebb8615b9a6058ec93d289d64642ceb
|
oid sha256:155ad78cfadaab78089293eca38ab8c404f227e38c451dddbbe3c59cccb82bc5
|
||||||
size 37759
|
size 51391
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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
Reference in New Issue
Block a user