Support incoming audio only calls

This commit is contained in:
Valere 2026-03-04 08:56:33 +01:00
parent ec420332c3
commit a3dd2c78b3
22 changed files with 156 additions and 53 deletions

View file

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

View file

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

View file

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

View file

@ -45,8 +45,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
callType = CallType.RoomCall( callType = CallType.RoomCall(
sessionId = notificationData.sessionId, sessionId = notificationData.sessionId,
roomId = notificationData.roomId, roomId = notificationData.roomId,
// TODO voiceIntent = notificationData.audioOnly
voiceIntent = false
) )
) )
} }

View file

@ -116,8 +116,7 @@ class IncomingCallActivity : AppCompatActivity() {
CallType.RoomCall( CallType.RoomCall(
notificationData.sessionId, notificationData.sessionId,
notificationData.roomId, notificationData.roomId,
// TODO voiceIntent = notificationData.audioOnly
voiceIntent = false
) )
) )
} }

View file

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

View file

@ -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 IncomingCallScreenProvider : PreviewParameterProvider<CallNotificationData> {
override val values: Sequence<CallNotificationData>
get() = sequenceOf(
aIncomingCallScreenState(
audioOnly = false
),
aIncomingCallScreenState(
audioOnly = true
),
)
}
internal fun aIncomingCallScreenState(
audioOnly: Boolean
): CallNotificationData {
return CallNotificationData(
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
audioOnly = audioOnly
)
}

View file

@ -147,7 +147,7 @@ class DefaultActiveCallManager(
sessionId = notificationData.sessionId, sessionId = notificationData.sessionId,
roomId = notificationData.roomId, roomId = notificationData.roomId,
// TODO // TODO
voiceIntent = false, voiceIntent = notificationData.audioOnly,
), ),
callState = CallState.Ringing(notificationData), callState = CallState.Ringing(notificationData),
) )
@ -275,6 +275,7 @@ class DefaultActiveCallManager(
timestamp = notificationData.timestamp, timestamp = notificationData.timestamp,
textContent = notificationData.textContent, textContent = notificationData.textContent,
expirationTimestamp = notificationData.expirationTimestamp, expirationTimestamp = notificationData.expirationTimestamp,
audioOnly = notificationData.audioOnly,
) ?: return ) ?: return
runCatchingExceptions { runCatchingExceptions {
notificationManagerCompat.notify( notificationManagerCompat.notify(

View file

@ -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,12 +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,
// TODO voiceIntent = callIntent == CallIntent.AUDIO
voiceIntent = false
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(inputs) elementCallEntryPoint.startCall(inputs)
@ -286,13 +286,12 @@ class RoomDetailsFlowNode(
callback.navigateToRoom(roomId, emptyList()) callback.navigateToRoom(roomId, emptyList())
} }
override fun startCall(dmRoomId: RoomId) { override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall( elementCallEntryPoint.startCall(
CallType.RoomCall( CallType.RoomCall(
roomId = dmRoomId, roomId = dmRoomId,
sessionId = room.sessionId, sessionId = room.sessionId,
// TODO voiceIntent = callIntent == CallIntent.AUDIO
voiceIntent = false
) )
) )
} }

View file

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

View file

@ -79,6 +79,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
@ -105,7 +106,7 @@ fun RoomDetailsView(
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
openMediaGallery: () -> Unit, openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit, openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (CallIntent) -> Unit,
onPinnedMessagesClick: () -> Unit, onPinnedMessagesClick: () -> Unit,
onKnockRequestsClick: () -> Unit, onKnockRequestsClick: () -> Unit,
onSecurityAndPrivacyClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit,
@ -327,7 +328,7 @@ private fun MainActionsSection(
state: RoomDetailsState, state: RoomDetailsState,
onShareRoom: () -> Unit, onShareRoom: () -> Unit,
onInvitePeople: () -> Unit, onInvitePeople: () -> Unit,
onCall: () -> Unit, onCall: (callIntent: CallIntent) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -358,8 +359,14 @@ private fun MainActionsSection(
// TODO Improve the view depending on all the cases here? // TODO Improve the view depending on all the cases here?
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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,8 +14,8 @@ import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManag
fun AccountManagementAction.toRustAction(): RustAccountManagementAction { fun AccountManagementAction.toRustAction(): RustAccountManagementAction {
return when (this) { return when (this) {
AccountManagementAction.Profile -> RustAccountManagementAction.Profile AccountManagementAction.Profile -> RustAccountManagementAction.Profile
is AccountManagementAction.SessionEnd -> RustAccountManagementAction.SessionEnd(deviceId.value) is AccountManagementAction.SessionEnd -> RustAccountManagementAction.DeviceDelete(deviceId.value)
is AccountManagementAction.SessionView -> RustAccountManagementAction.SessionView(deviceId.value) is AccountManagementAction.SessionView -> RustAccountManagementAction.DeviceView(deviceId.value)
AccountManagementAction.SessionsList -> RustAccountManagementAction.SessionsList AccountManagementAction.SessionsList -> RustAccountManagementAction.DevicesList
} }
} }

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
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.api.push.NotificationEventRequest import io.element.android.libraries.push.api.push.NotificationEventRequest
import io.element.android.libraries.push.api.push.SyncOnNotifiableEvent import io.element.android.libraries.push.api.push.SyncOnNotifiableEvent
import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService
@ -303,8 +304,7 @@ class DefaultPushHandler(
callType = CallType.RoomCall( callType = CallType.RoomCall(
notifiableEvent.sessionId, notifiableEvent.sessionId,
notifiableEvent.roomId, notifiableEvent.roomId,
// TODO voiceIntent = notifiableEvent.callIntent == CallIntent.AUDIO
voiceIntent = false
), ),
eventId = notifiableEvent.eventId, eventId = notifiableEvent.eventId,
senderId = notifiableEvent.senderId, senderId = notifiableEvent.senderId,

View file

@ -19,6 +19,7 @@
<item quantity="one">"You have %d new message."</item> <item quantity="one">"You have %d new message."</item>
<item quantity="other">"You have %d new messages."</item> <item quantity="other">"You have %d new messages."</item>
</plurals> </plurals>
<string name="notification_incoming_audio_call">"📞 Incoming call"</string>
<string name="notification_incoming_call">"📹 Incoming call"</string> <string name="notification_incoming_call">"📹 Incoming call"</string>
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string> <string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
<string name="notification_invitation_action_join">"Join"</string> <string name="notification_invitation_action_join">"Join"</string>