From a3dd2c78b39c2bfe1f705c77ba6c3485eb49f361 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 4 Mar 2026 08:56:33 +0100 Subject: [PATCH] Support incoming audio only calls --- .../call/impl/DefaultElementCallEntryPoint.kt | 1 + .../notifications/CallNotificationData.kt | 1 + .../RingingCallNotificationCreator.kt | 11 +++-- .../receivers/DeclineCallBroadcastReceiver.kt | 3 +- .../call/impl/ui/IncomingCallActivity.kt | 3 +- .../call/impl/ui/IncomingCallScreen.kt | 25 +++------- .../impl/ui/IncomingCallScreenProvider.kt | 46 +++++++++++++++++++ .../call/impl/utils/ActiveCallManager.kt | 3 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 11 ++--- .../roomdetails/impl/RoomDetailsNode.kt | 3 +- .../roomdetails/impl/RoomDetailsView.kt | 13 ++++-- .../members/details/RoomMemberDetailsNode.kt | 5 +- .../shared/UserProfileMainActionsSection.kt | 29 ++++++++++-- .../shared/UserProfileNodeHelper.kt | 3 +- .../userprofile/shared/UserProfileView.kt | 7 +-- .../api/notification/NotificationData.kt | 6 +++ ...imelineEventToNotificationContentMapper.kt | 8 ++++ .../impl/oidc/AccountManagementAction.kt | 6 +-- .../CallNotificationEventResolver.kt | 18 +++++++- .../model/NotifiableRingingCallEvent.kt | 2 + .../push/impl/push/DefaultPushHandler.kt | 4 +- .../impl/src/main/res/values/localazy.xml | 1 + 22 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreenProvider.kt diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt index 9ed479f5b8..bd78f7cc6a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt @@ -58,6 +58,7 @@ class DefaultElementCallEntryPoint( expirationTimestamp = expirationTimestamp, notificationChannelId = notificationChannelId, textContent = textContent, + audioOnly = callType.voiceIntent ) activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt index dcc434e84d..9206de0de8 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -29,4 +29,5 @@ data class CallNotificationData( val textContent: String?, // Expiration timestamp in millis since epoch val expirationTimestamp: Long, + val audioOnly: Boolean, ) : Parcelable diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt index 89ca335344..15af1a2c5e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt @@ -69,6 +69,7 @@ class RingingCallNotificationCreator( timestamp: Long, expirationTimestamp: Long, textContent: String?, + audioOnly: Boolean, ): Notification? { val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val imageLoader = imageLoaderHolder.get(matrixClient) @@ -88,8 +89,7 @@ class RingingCallNotificationCreator( .setImportant(true) .build() - // TODO - val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, voiceIntent = false)) + val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, voiceIntent = audioOnly)) val notificationData = CallNotificationData( sessionId = sessionId, roomId = roomId, @@ -102,6 +102,7 @@ class RingingCallNotificationCreator( timestamp = timestamp, textContent = textContent, expirationTimestamp = expirationTimestamp, + audioOnly = audioOnly, ) val declineIntent = PendingIntentCompat.getBroadcast( @@ -128,7 +129,11 @@ class RingingCallNotificationCreator( .setSmallIcon(CommonDrawables.ic_notification) .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_CALL) - .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true)) + .setStyle( + NotificationCompat.CallStyle + .forIncomingCall(caller, declineIntent, answerIntent) + .setIsVideo(!audioOnly) + ) .addPerson(caller) .setAutoCancel(true) .setWhen(timestamp) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt index 56f2f34da5..54fb41d864 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt @@ -45,8 +45,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() { callType = CallType.RoomCall( sessionId = notificationData.sessionId, roomId = notificationData.roomId, - // TODO - voiceIntent = false + voiceIntent = notificationData.audioOnly ) ) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt index 5dea00a337..d6c85c66c5 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt @@ -116,8 +116,7 @@ class IncomingCallActivity : AppCompatActivity() { CallType.RoomCall( notificationData.sessionId, notificationData.roomId, - // TODO - voiceIntent = false + voiceIntent = notificationData.audioOnly ) ) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt index 5ec3689b44..1ce2acee08 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -45,10 +46,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.ui.strings.CommonStrings /** @@ -103,7 +100,7 @@ internal fun IncomingCallScreen( ActionButton( size = 64.dp, onClick = { onAnswer(notificationData) }, - icon = CompoundIcons.VoiceCallSolid(), + icon = if (notificationData.audioOnly) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(), title = stringResource(CommonStrings.action_accept), backgroundColor = ElementTheme.colors.iconSuccessPrimary, borderColor = ElementTheme.colors.borderSuccessSubtle @@ -163,21 +160,11 @@ private fun ActionButton( @PreviewsDayNight @Composable -internal fun IncomingCallScreenPreview() = ElementPreview { +internal fun IncomingCallScreenPreview( + @PreviewParameter(IncomingCallScreenProvider::class) state: CallNotificationData, +) = ElementPreview { IncomingCallScreen( - notificationData = CallNotificationData( - sessionId = SessionId("@alice:matrix.org"), - roomId = RoomId("!1234:matrix.org"), - eventId = EventId("\$asdadadsad:matrix.org"), - senderId = UserId("@bob:matrix.org"), - roomName = "A room", - senderName = "Bob", - avatarUrl = null, - notificationChannelId = "incoming_call", - timestamp = 0L, - textContent = null, - expirationTimestamp = 1000L, - ), + notificationData = state, onAnswer = {}, onCancel = {}, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreenProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreenProvider.kt new file mode 100644 index 0000000000..287a57db1a --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreenProvider.kt @@ -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 { + override val values: Sequence + 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 + ) +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index 40f66db623..5899214773 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -147,7 +147,7 @@ class DefaultActiveCallManager( sessionId = notificationData.sessionId, roomId = notificationData.roomId, // TODO - voiceIntent = false, + voiceIntent = notificationData.audioOnly, ), callState = CallState.Ringing(notificationData), ) @@ -275,6 +275,7 @@ class DefaultActiveCallManager( timestamp = notificationData.timestamp, textContent = notificationData.textContent, expirationTimestamp = notificationData.expirationTimestamp, + audioOnly = notificationData.audioOnly, ) ?: return runCatchingExceptions { notificationManagerCompat.notify( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index c157618a59..047733687f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -223,12 +224,11 @@ class RoomDetailsFlowNode( backstack.push(NavTarget.RoomMemberDetails(userId)) } - override fun navigateToRoomCall() { + override fun navigateToRoomCall(callIntent: CallIntent) { val inputs = CallType.RoomCall( sessionId = room.sessionId, roomId = room.roomId, - // TODO - voiceIntent = false + voiceIntent = callIntent == CallIntent.AUDIO ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) elementCallEntryPoint.startCall(inputs) @@ -286,13 +286,12 @@ class RoomDetailsFlowNode( 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, - // TODO - voiceIntent = false + voiceIntent = callIntent == CallIntent.AUDIO ) ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index d7b4e0d813..e9aad5b1d2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope @@ -59,7 +60,7 @@ class RoomDetailsNode( fun navigateToKnockRequestsList() fun navigateToSecurityAndPrivacy() fun navigateToRoomMemberDetails(userId: UserId) - fun navigateToRoomCall() + fun navigateToRoomCall(callIntent: CallIntent) fun navigateToReportRoom() fun navigateToSelectNewOwnersWhenLeaving() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 3ec9b0a1da..9adb51bd6b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -79,6 +79,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.getBestName @@ -105,7 +106,7 @@ fun RoomDetailsView( openPollHistory: () -> Unit, openMediaGallery: () -> Unit, openAdminSettings: () -> Unit, - onJoinCallClick: () -> Unit, + onJoinCallClick: (CallIntent) -> Unit, onPinnedMessagesClick: () -> Unit, onKnockRequestsClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit, @@ -327,7 +328,7 @@ private fun MainActionsSection( state: RoomDetailsState, onShareRoom: () -> Unit, onInvitePeople: () -> Unit, - onCall: () -> Unit, + onCall: (callIntent: CallIntent) -> Unit, ) { Row( modifier = Modifier @@ -358,8 +359,14 @@ private fun MainActionsSection( // TODO Improve the view depending on all the cases here? MainActionButton( title = stringResource(CommonStrings.action_call), + imageVector = CompoundIcons.VoiceCall(), + onClick = { onCall(CallIntent.AUDIO) }, + ) + + MainActionButton( + title = stringResource(CommonStrings.common_video), imageVector = CompoundIcons.VideoCall(), - onClick = onCall, + onClick = { onCall(CallIntent.VIDEO) }, ) } if (state.roomType is RoomDetailsType.Room) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index ce798e5da9..ccf2f28ab5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.services.analytics.api.AnalyticsService @@ -67,8 +68,8 @@ class RoomMemberDetailsNode( callback.navigateToRoom(roomId) } - fun onStartCall(roomId: RoomId) { - callback.startCall(roomId) + fun onStartCall(roomId: RoomId, callIntent: CallIntent) { + callback.startCall(roomId, callIntent) } val state = presenter.present() diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt index 0ed6d14aca..05938820b5 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt @@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.button.MainActionButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -26,11 +29,13 @@ fun UserProfileMainActionsSection( canCall: Boolean, onShareUser: () -> Unit, onStartDM: () -> Unit, - onCall: () -> Unit, + onCall: (CallIntent) -> Unit, modifier: Modifier = Modifier ) { Row( - modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceEvenly, ) { if (!isCurrentUser) { @@ -43,8 +48,14 @@ fun UserProfileMainActionsSection( if (canCall) { MainActionButton( title = stringResource(CommonStrings.action_call), + imageVector = CompoundIcons.VoiceCall(), + onClick = { onCall(CallIntent.AUDIO) }, + ) + + MainActionButton( + title = stringResource(CommonStrings.common_video), imageVector = CompoundIcons.VideoCall(), - onClick = onCall, + onClick = { onCall(CallIntent.VIDEO) }, ) } MainActionButton( @@ -54,3 +65,15 @@ fun UserProfileMainActionsSection( ) } } + +@PreviewsDayNight() +@Composable +internal fun UserProfileMainActionsSectionPreview() = ElementPreview { + UserProfileMainActionsSection( + isCurrentUser = false, + canCall = true, + onShareUser = { }, + onStartDM = { }, + onCall = { } + ) +} diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt index 6f2266eef2..6699183f69 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.ui.strings.CommonStrings import timber.log.Timber @@ -24,7 +25,7 @@ class UserProfileNodeHelper( interface Callback : NodeInputs { fun navigateToAvatarPreview(username: String, avatarUrl: String) fun navigateToRoom(roomId: RoomId) - fun startCall(dmRoomId: RoomId) + fun startCall(dmRoomId: RoomId, callIntent: CallIntent) fun startVerifyUserFlow(userId: UserId) } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 48f89390da..380bb006ab 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet import io.element.android.libraries.ui.strings.CommonStrings @@ -52,7 +53,7 @@ fun UserProfileView( state: UserProfileState, onShareUser: () -> Unit, onOpenDm: (RoomId) -> Unit, - onStartCall: (RoomId) -> Unit, + onStartCall: (RoomId, CallIntent) -> Unit, goBack: () -> Unit, openAvatarPreview: (username: String, url: String) -> Unit, onVerifyClick: (UserId) -> Unit, @@ -90,7 +91,7 @@ fun UserProfileView( canCall = state.canCall, onShareUser = onShareUser, onStartDM = { state.eventSink(UserProfileEvents.StartDM) }, - onCall = { state.dmRoomId?.let { onStartCall(it) } } + onCall = { intent -> state.dmRoomId?.let { onStartCall(it, intent) } } ) Spacer(modifier = Modifier.height(26.dp)) if (!state.isCurrentUser) { @@ -151,7 +152,7 @@ internal fun UserProfileViewPreview( onShareUser = {}, goBack = {}, onOpenDm = {}, - onStartCall = {}, + onStartCall = { _, _ -> }, openAvatarPreview = { _, _ -> }, onVerifyClick = {}, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 02e40813b0..5dce175237 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -56,6 +56,7 @@ sealed interface NotificationContent { data class RtcNotification( val senderId: UserId, val type: RtcNotificationType, + val callIntent: CallIntent, val expirationTimestampMillis: Long ) : MessageLike @@ -127,3 +128,8 @@ enum class RtcNotificationType { RING, NOTIFY } + +enum class CallIntent { + AUDIO, + VIDEO +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index 450216b452..96c4bdf3c4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.notification import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper @@ -20,6 +21,7 @@ import org.matrix.rustcomponents.sdk.StateEventContent import org.matrix.rustcomponents.sdk.TimelineEvent import org.matrix.rustcomponents.sdk.TimelineEventContent import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.RtcCallIntent as SdkRtcCallIntent import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType class TimelineEventToNotificationContentMapper { @@ -83,6 +85,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification( senderId = senderId, type = notificationType.map(), + callIntent = callIntent.map(), expirationTimestampMillis = expirationTs.toLong() ) MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept @@ -111,3 +114,8 @@ private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) { SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY SdkRtcNotificationType.RING -> RtcNotificationType.RING } + +private fun SdkRtcCallIntent?.map(): CallIntent = when (this) { + SdkRtcCallIntent.AUDIO -> CallIntent.AUDIO + else -> CallIntent.VIDEO +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt index f998126e17..76e60e90f4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt @@ -14,8 +14,8 @@ import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManag fun AccountManagementAction.toRustAction(): RustAccountManagementAction { return when (this) { AccountManagementAction.Profile -> RustAccountManagementAction.Profile - is AccountManagementAction.SessionEnd -> RustAccountManagementAction.SessionEnd(deviceId.value) - is AccountManagementAction.SessionView -> RustAccountManagementAction.SessionView(deviceId.value) - AccountManagementAction.SessionsList -> RustAccountManagementAction.SessionsList + is AccountManagementAction.SessionEnd -> RustAccountManagementAction.DeviceDelete(deviceId.value) + is AccountManagementAction.SessionView -> RustAccountManagementAction.DeviceView(deviceId.value) + AccountManagementAction.SessionsList -> RustAccountManagementAction.DevicesList } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt index 444d0c725a..d32816d634 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.exception.NotificationResolverException +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.RtcNotificationType @@ -90,6 +91,7 @@ class DefaultCallNotificationEventResolver( notificationData.run { if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) { + Timber.d("Ringing call notification intent ${content.callIntent} in room $roomId") NotifiableRingingCallEvent( sessionId = sessionId, roomId = roomId, @@ -100,10 +102,18 @@ class DefaultCallNotificationEventResolver( timestamp = this.timestamp, isRedacted = false, isUpdated = false, - description = stringProvider.getString(R.string.notification_incoming_call), + description = if (content.callIntent == + CallIntent.AUDIO) { + stringProvider.getString(R.string.notification_incoming_audio_call) + } else { + stringProvider.getString( + R.string.notification_incoming_call + ) + }, senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), roomAvatarUrl = roomAvatarUrl, rtcNotificationType = content.type, + callIntent = content.callIntent, senderId = content.senderId, senderAvatarUrl = senderAvatarUrl, expirationTimestamp = content.expirationTimestampMillis, @@ -119,7 +129,11 @@ class DefaultCallNotificationEventResolver( noisy = true, timestamp = this.timestamp, senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), - body = stringProvider.getString(R.string.notification_incoming_call), + body = if (content.callIntent == CallIntent.VIDEO) { + stringProvider.getString(R.string.notification_incoming_call) + } else { + stringProvider.getString(R.string.notification_incoming_audio_call) + }, roomName = roomDisplayName, roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt index 35432e972d..eef39eea93 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.RtcNotificationType data class NotifiableRingingCallEvent( @@ -29,6 +30,7 @@ data class NotifiableRingingCallEvent( val senderAvatarUrl: String?, val roomAvatarUrl: String? = null, val rtcNotificationType: RtcNotificationType, + val callIntent: CallIntent, val timestamp: Long, val expirationTimestamp: Long, ) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index ee50e4cad1..8fea261d8b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -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.FeatureFlags 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.SyncOnNotifiableEvent import io.element.android.libraries.push.impl.history.PushHistoryService @@ -303,8 +304,7 @@ class DefaultPushHandler( callType = CallType.RoomCall( notifiableEvent.sessionId, notifiableEvent.roomId, - // TODO - voiceIntent = false + voiceIntent = notifiableEvent.callIntent == CallIntent.AUDIO ), eventId = notifiableEvent.eventId, senderId = notifiableEvent.senderId, diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 15afb93d3f..d680ffeb5a 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -19,6 +19,7 @@ "You have %d new message." "You have %d new messages." + "📞 Incoming call" "📹 Incoming call" "** Failed to send - please open room" "Join"