diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt index dfd1b2ed0e..61273ff07a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.annotation.StringRes import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -22,17 +23,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -42,6 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun TimelineItemCallNotifyView( + timelineRoomInfo: TimelineRoomInfo, event: TimelineItem.Event, content: TimelineItemRtcNotificationContent, onLongClick: (TimelineItem.Event) -> Unit, @@ -62,37 +66,22 @@ internal fun TimelineItemCallNotifyView( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Avatar( - avatarData = event.senderAvatar, - avatarType = AvatarType.User, + Icon( + modifier = Modifier.size(20.sp.toDp()), + imageVector = getIcon(timelineRoomInfo, content), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = event.safeSenderName, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier.size(20.sp.toDp()), - imageVector = - if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(), - contentDescription = null, - tint = ElementTheme.colors.iconSecondary, - ) - Text( - text = stringResource(CommonStrings.common_call_started), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } + + Text( + modifier = Modifier.weight(1f), + text = stringResource(getTextRes(timelineRoomInfo, content)), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( text = event.sentTime, style = ElementTheme.typography.fontBodyMdRegular, @@ -103,19 +92,56 @@ internal fun TimelineItemCallNotifyView( } } +@StringRes +private fun getTextRes( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): Int = if (timelineRoomInfo.isDm) { + when (content.state) { + is RtcNotificationState.Declined -> { + if (content.state.byMe) CommonStrings.common_call_you_declined else CommonStrings.common_call_declined + } + RtcNotificationState.Started -> CommonStrings.common_call_started + } +} else { + // In Rooms, do not show declined info. + CommonStrings.common_call_started +} + +@Composable +private fun getIcon( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): ImageVector { + val showAsDeclined = timelineRoomInfo.isDm && content.state is RtcNotificationState.Declined + val icon = if (showAsDeclined) { + if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallDeclinedSolid() else CompoundIcons.VideoCallDeclinedSolid() + } else { + if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid() + } + return icon +} + @PreviewsDayNight @Composable internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - listOf( - TimelineItemRtcNotificationContent(CallIntent.AUDIO), - TimelineItemRtcNotificationContent(CallIntent.VIDEO), - ).forEach { content -> - TimelineItemCallNotifyView( - event = aTimelineItemEvent(content = content), - content = content, - onLongClick = {}, - ) + Column(modifier = Modifier.padding(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { + listOf(false, true).forEach { isDm -> + listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent -> + listOf( + RtcNotificationState.Started, + RtcNotificationState.Declined(byMe = false), + RtcNotificationState.Declined(byMe = true), + ).forEach { state -> + val content = TimelineItemRtcNotificationContent(callIntent, state) + TimelineItemCallNotifyView( + timelineRoomInfo = aTimelineRoomInfo(isDm = isDm), + event = aTimelineItemEvent(content = content), + content = content, + onLongClick = {}, + ) + } + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 842b7a08f5..a02413d534 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -125,6 +125,7 @@ internal fun TimelineItemRow( is TimelineItemRtcNotificationContent -> { TimelineItemCallNotifyView( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), + timelineRoomInfo = timelineRoomInfo, event = timelineItem, content = timelineItem.content, onLongClick = onLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 11f6668dfb..3b3e5118d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.factories.event import dev.zacsweers.metro.Inject import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -104,7 +105,12 @@ class TimelineItemContentFactory( is PollContent -> pollFactory.create(eventId, isEditable, isOutgoing, itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent) is CallNotifyContent -> TimelineItemRtcNotificationContent( - itemContent.callIntent + callIntent = itemContent.callIntent, + state = if (itemContent.declinedBy.isEmpty()) { + RtcNotificationState.Started + } else { + RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) + } ) is UnknownContent -> TimelineItemUnknownContent is LiveLocationContent -> { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt index 53facfc675..2359f196a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt @@ -9,7 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.timeline.item.event.EventType -class TimelineItemRtcNotificationContent(val callIntent: CallIntent) : TimelineItemEventContent { - override val type: String = "org.matrix.msc4075.rtc.notification" +// State of the call, for now only isDeclined but in the future could be missed, active. +sealed interface RtcNotificationState { + /** Some users have declined, byMe indicates if the current user is one of them. */ + data class Declined(val byMe: Boolean) : RtcNotificationState + + object Started : RtcNotificationState +} + +class TimelineItemRtcNotificationContent( + val callIntent: CallIntent, + val state: RtcNotificationState, +) : TimelineItemEventContent { + override val type: String = EventType.RTC_NOTIFICATION } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt index c48f2dae40..210e123595 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.utils.messagesummary import android.content.Context import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent @@ -56,7 +57,16 @@ class DefaultMessageSummaryFormatter( is TimelineItemFileContent -> context.getString(CommonStrings.common_file) is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) - is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started) + is TimelineItemRtcNotificationContent -> when (content.state) { + is RtcNotificationState.Declined -> { + if (content.state.byMe) { + context.getString(CommonStrings.common_call_you_declined) + } else { + context.getString(CommonStrings.common_call_declined) + } + } + RtcNotificationState.Started -> context.getString(CommonStrings.common_call_started) + } } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 8c7f290441..20b636081a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -17,6 +17,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -1169,7 +1170,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemRtcNotificationContent(callIntent = CallIntent.VIDEO), + content = TimelineItemRtcNotificationContent(callIntent = CallIntent.VIDEO, state = RtcNotificationState.Started), ) initialState.eventSink.invoke( ActionListEvent.ComputeForMessage( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt new file mode 100644 index 0000000000..664d21ed64 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt @@ -0,0 +1,106 @@ +/* + * 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.messages.impl.utils + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent.Mode +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent +import io.element.android.features.messages.impl.utils.messagesummary.DefaultMessageSummaryFormatter +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.timeline.aProfileDetails +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +class DefaultMessageSummaryFormatterTest { + private val formatter = DefaultMessageSummaryFormatter( + RuntimeEnvironment.getApplication() as Context + ) + + @Test + @Config(qualifiers = "en") + fun `format call notification started`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Started + ) + ) + assertThat(expected).isEqualTo("Call started") + } + + @Test + @Config(qualifiers = "en") + fun `format call notification declined by me`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Declined(byMe = true) + ) + ) + assertThat(expected).isEqualTo("You declined a call") + } + + @Test + @Config(qualifiers = "en") + fun `format call notification declined`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Declined(byMe = false) + ) + ) + assertThat(expected).isEqualTo("Call declined") + } + + @Test + @Config(qualifiers = "en") + fun `format live location`() { + val expected = formatter.format( + aLocationContent(isLive = true) + ) + assertThat(expected).isEqualTo("Shared live location") + } + + @Test + @Config(qualifiers = "en") + fun `format static location`() { + val expected = formatter.format( + aLocationContent(isLive = false) + ) + assertThat(expected).isEqualTo("Shared location") + } +} + +private fun aLocationContent(isLive: Boolean) = TimelineItemLocationContent( + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + description = null, + assetType = null, + mode = if (isLive) { + Mode.Live( + lastKnownLocation = Location.fromGeoUri("geo:1,5"), + isActive = true, + endsAt = "", + endTimestamp = 0, + isOwnUser = true, + ) + } else { + Mode.Static( + location = Location.fromGeoUri("geo:1,5")!!, + ) + } +) diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt index b9b6467c92..d234e7b239 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt @@ -54,6 +54,7 @@ class DefaultRoomLatestEventFormatter( private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, + private val rtcNotificationContentFormatter: RtcNotificationContentFormatter, private val permalinkParser: PermalinkParser, ) : RoomLatestEventFormatter { override fun format( @@ -121,7 +122,7 @@ class DefaultRoomLatestEventFormatter( message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call) - is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) + is CallNotifyContent -> rtcNotificationContentFormatter.format(content, isDmRoom) }?.take(DEFAULT_SAFE_LENGTH) } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt new file mode 100644 index 0000000000..ab3fb9433d --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt @@ -0,0 +1,39 @@ +/* + * 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.libraries.eventformatter.impl + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.services.toolbox.api.strings.StringProvider + +@Inject +class RtcNotificationContentFormatter( + private val matrixClient: MatrixClient, + private val sp: StringProvider, +) { + fun format( + content: CallNotifyContent, + isDm: Boolean, + ): CharSequence { + return if (isDm) { + val isDeclined = content.declinedBy.isNotEmpty() + val isDeclinedByMe = content.declinedBy.any { matrixClient.isMe(it) } + if (isDeclinedByMe) { + sp.getString(CommonStrings.common_call_you_declined) + } else if (isDeclined) { + sp.getString(CommonStrings.common_call_declined) + } else { + sp.getString(CommonStrings.common_call_started) + } + } else { + sp.getString(CommonStrings.common_call_started) + } + } +} diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt index e1e8717c4c..e0613ed008 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt @@ -74,7 +74,8 @@ class DefaultRoomLatestEventFormatterTest { roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), - permalinkParser = FakePermalinkParser(), + rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider), + permalinkParser = FakePermalinkParser() ) } diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt new file mode 100644 index 0000000000..dca34db3cc --- /dev/null +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt @@ -0,0 +1,99 @@ +/* + * 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.libraries.eventformatter.impl + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import kotlin.toString + +@Suppress("LargeClass") +@RunWith(RobolectricTestRunner::class) +class RtcNotificationContentFormatterTest { + private lateinit var context: Context + private lateinit var fakeMatrixClient: FakeMatrixClient + private lateinit var formatter: RtcNotificationContentFormatter + + @Before + fun setup() { + context = RuntimeEnvironment.getApplication() as Context + fakeMatrixClient = FakeMatrixClient() + val stringProvider = AndroidStringProvider(context.resources) + formatter = RtcNotificationContentFormatter( + fakeMatrixClient, + stringProvider + ) + } + + @Test + @Config(qualifiers = "en") + fun `Should not display declined info in rooms`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(A_USER_ID_2, A_USER_ID_3) + ), + false + ) + val expected = "Call started" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Declined by me variant`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(fakeMatrixClient.sessionId) + ), + true + ) + val expected = "You declined a call" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Declined by other variant`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(A_USER_ID_2) + ), + true + ) + val expected = "Call declined" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Call started in DM`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.AUDIO, + declinedBy = listOf() + ), + true + ) + val expected = "Call started" + assertThat(result.toString()).isEqualTo(expected) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index d91d404a0b..f323b316f7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -119,7 +119,8 @@ data class LiveLocationContent( data object LegacyCallInviteContent : EventContent data class CallNotifyContent( - val callIntent: CallIntent + val callIntent: CallIntent, + val declinedBy: List ) : EventContent data object UnknownContent : EventContent diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index edfe9a3543..fa671bc546 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -153,7 +153,8 @@ class TimelineEventContentMapper( CallIntent.AUDIO } else { CallIntent.VIDEO - } + }, + declinedBy = it.declinedBy.map(::UserId) ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt index 6602f475eb..ab3ffa98c7 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt @@ -62,7 +62,7 @@ class DefaultEventItemFactoryTest { fun `create check all null cases`() { val factory = createEventItemFactory() val contents = listOf( - CallNotifyContent(callIntent = CallIntent.VIDEO), + CallNotifyContent(callIntent = CallIntent.VIDEO, emptyList()), FailedToParseMessageLikeContent("", ""), FailedToParseStateContent("", "", ""), LegacyCallInviteContent, diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png index afec980e10..e89697a8cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3624fe8448ae4af2481e2023a978ffab2d69f2784b7cef41e5ae2e2dbe8fdbd5 -size 17804 +oid sha256:5529e89e00208e38522f5206f5b8d304bd472b27071cab4e0d3c2daf3cb64db0 +size 49741 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png index 8f7f14e64a..37037907c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bd02d39619efbcaa6d96f1e75a0d14a572c43e60bad0c3f84d6a5a48b6fbda1 -size 17395 +oid sha256:fa29aaa82f21912dd5147ffb6fdc457fd5880e8be4abde4f0177d0ab1a412fe2 +size 48245