Support incoming audio only calls
This commit is contained in:
parent
ec420332c3
commit
a3dd2c78b3
22 changed files with 156 additions and 53 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,5 @@ data class CallNotificationData(
|
||||||
val textContent: String?,
|
val textContent: String?,
|
||||||
// Expiration timestamp in millis since epoch
|
// Expiration timestamp in millis since epoch
|
||||||
val expirationTimestamp: Long,
|
val expirationTimestamp: Long,
|
||||||
|
val audioOnly: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ class RingingCallNotificationCreator(
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
expirationTimestamp: Long,
|
expirationTimestamp: Long,
|
||||||
textContent: String?,
|
textContent: String?,
|
||||||
|
audioOnly: Boolean,
|
||||||
): Notification? {
|
): Notification? {
|
||||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||||
val imageLoader = imageLoaderHolder.get(matrixClient)
|
val imageLoader = imageLoaderHolder.get(matrixClient)
|
||||||
|
|
@ -88,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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -59,7 +60,7 @@ class RoomDetailsNode(
|
||||||
fun navigateToKnockRequestsList()
|
fun navigateToKnockRequestsList()
|
||||||
fun navigateToSecurityAndPrivacy()
|
fun navigateToSecurityAndPrivacy()
|
||||||
fun navigateToRoomMemberDetails(userId: UserId)
|
fun navigateToRoomMemberDetails(userId: UserId)
|
||||||
fun navigateToRoomCall()
|
fun navigateToRoomCall(callIntent: CallIntent)
|
||||||
fun navigateToReportRoom()
|
fun navigateToReportRoom()
|
||||||
fun navigateToSelectNewOwnersWhenLeaving()
|
fun navigateToSelectNewOwnersWhenLeaving()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
|
|
@ -105,7 +106,7 @@ fun RoomDetailsView(
|
||||||
openPollHistory: () -> Unit,
|
openPollHistory: () -> Unit,
|
||||||
openMediaGallery: () -> Unit,
|
openMediaGallery: () -> Unit,
|
||||||
openAdminSettings: () -> Unit,
|
openAdminSettings: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: (CallIntent) -> Unit,
|
||||||
onPinnedMessagesClick: () -> Unit,
|
onPinnedMessagesClick: () -> Unit,
|
||||||
onKnockRequestsClick: () -> Unit,
|
onKnockRequestsClick: () -> Unit,
|
||||||
onSecurityAndPrivacyClick: () -> Unit,
|
onSecurityAndPrivacyClick: () -> Unit,
|
||||||
|
|
@ -327,7 +328,7 @@ private fun MainActionsSection(
|
||||||
state: RoomDetailsState,
|
state: RoomDetailsState,
|
||||||
onShareRoom: () -> Unit,
|
onShareRoom: () -> Unit,
|
||||||
onInvitePeople: () -> Unit,
|
onInvitePeople: () -> Unit,
|
||||||
onCall: () -> Unit,
|
onCall: (callIntent: CallIntent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
|
|
@ -67,8 +68,8 @@ class RoomMemberDetailsNode(
|
||||||
callback.navigateToRoom(roomId)
|
callback.navigateToRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onStartCall(roomId: RoomId) {
|
fun onStartCall(roomId: RoomId, callIntent: CallIntent) {
|
||||||
callback.startCall(roomId)
|
callback.startCall(roomId, callIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -26,11 +29,13 @@ fun UserProfileMainActionsSection(
|
||||||
canCall: Boolean,
|
canCall: Boolean,
|
||||||
onShareUser: () -> Unit,
|
onShareUser: () -> Unit,
|
||||||
onStartDM: () -> Unit,
|
onStartDM: () -> Unit,
|
||||||
onCall: () -> Unit,
|
onCall: (CallIntent) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
if (!isCurrentUser) {
|
if (!isCurrentUser) {
|
||||||
|
|
@ -43,8 +48,14 @@ fun UserProfileMainActionsSection(
|
||||||
if (canCall) {
|
if (canCall) {
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
title = stringResource(CommonStrings.action_call),
|
title = stringResource(CommonStrings.action_call),
|
||||||
|
imageVector = CompoundIcons.VoiceCall(),
|
||||||
|
onClick = { onCall(CallIntent.AUDIO) },
|
||||||
|
)
|
||||||
|
|
||||||
|
MainActionButton(
|
||||||
|
title = stringResource(CommonStrings.common_video),
|
||||||
imageVector = CompoundIcons.VideoCall(),
|
imageVector = CompoundIcons.VideoCall(),
|
||||||
onClick = onCall,
|
onClick = { onCall(CallIntent.VIDEO) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
|
|
@ -54,3 +65,15 @@ fun UserProfileMainActionsSection(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight()
|
||||||
|
@Composable
|
||||||
|
internal fun UserProfileMainActionsSectionPreview() = ElementPreview {
|
||||||
|
UserProfileMainActionsSection(
|
||||||
|
isCurrentUser = false,
|
||||||
|
canCall = true,
|
||||||
|
onShareUser = { },
|
||||||
|
onStartDM = { },
|
||||||
|
onCall = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -24,7 +25,7 @@ class UserProfileNodeHelper(
|
||||||
interface Callback : NodeInputs {
|
interface Callback : NodeInputs {
|
||||||
fun navigateToAvatarPreview(username: String, avatarUrl: String)
|
fun navigateToAvatarPreview(username: String, avatarUrl: String)
|
||||||
fun navigateToRoom(roomId: RoomId)
|
fun navigateToRoom(roomId: RoomId)
|
||||||
fun startCall(dmRoomId: RoomId)
|
fun startCall(dmRoomId: RoomId, callIntent: CallIntent)
|
||||||
fun startVerifyUserFlow(userId: UserId)
|
fun startVerifyUserFlow(userId: UserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet
|
import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ fun UserProfileView(
|
||||||
state: UserProfileState,
|
state: UserProfileState,
|
||||||
onShareUser: () -> Unit,
|
onShareUser: () -> Unit,
|
||||||
onOpenDm: (RoomId) -> Unit,
|
onOpenDm: (RoomId) -> Unit,
|
||||||
onStartCall: (RoomId) -> Unit,
|
onStartCall: (RoomId, CallIntent) -> Unit,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
openAvatarPreview: (username: String, url: String) -> Unit,
|
openAvatarPreview: (username: String, url: String) -> Unit,
|
||||||
onVerifyClick: (UserId) -> Unit,
|
onVerifyClick: (UserId) -> Unit,
|
||||||
|
|
@ -90,7 +91,7 @@ fun UserProfileView(
|
||||||
canCall = state.canCall,
|
canCall = state.canCall,
|
||||||
onShareUser = onShareUser,
|
onShareUser = onShareUser,
|
||||||
onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
|
onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
|
||||||
onCall = { state.dmRoomId?.let { onStartCall(it) } }
|
onCall = { intent -> state.dmRoomId?.let { onStartCall(it, intent) } }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(26.dp))
|
Spacer(modifier = Modifier.height(26.dp))
|
||||||
if (!state.isCurrentUser) {
|
if (!state.isCurrentUser) {
|
||||||
|
|
@ -151,7 +152,7 @@ internal fun UserProfileViewPreview(
|
||||||
onShareUser = {},
|
onShareUser = {},
|
||||||
goBack = {},
|
goBack = {},
|
||||||
onOpenDm = {},
|
onOpenDm = {},
|
||||||
onStartCall = {},
|
onStartCall = { _, _ -> },
|
||||||
openAvatarPreview = { _, _ -> },
|
openAvatarPreview = { _, _ -> },
|
||||||
onVerifyClick = {},
|
onVerifyClick = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue