From f0dc4eeace0c8f19304fce4f701afa749809a9cd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 24 Apr 2026 11:37:00 +0200 Subject: [PATCH 01/17] feat: Update call started timeline item + declined support --- .../components/TimelineItemCallNotifyView.kt | 94 ++++++++++++------- .../timeline/components/TimelineItemRow.kt | 1 + .../event/TimelineItemContentFactory.kt | 8 +- .../TimelineItemRtcNotificationContent.kt | 21 ++++- .../DefaultMessageSummaryFormatter.kt | 8 +- .../impl/DefaultRoomLatestEventFormatter.kt | 3 +- .../impl/RtcNotificationContentFormatter.kt | 39 ++++++++ .../api/timeline/item/event/EventContent.kt | 3 +- .../item/event/TimelineEventContentMapper.kt | 3 +- 9 files changed, 138 insertions(+), 42 deletions(-) create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt 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..aa004a6259 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 @@ -22,17 +22,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 +44,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 +65,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,15 +91,51 @@ internal fun TimelineItemCallNotifyView( } } +private fun getTextRes( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): Int = if (timelineRoomInfo.isDm) { + when (content.state) { + RtcNotificationState.Declined -> CommonStrings.common_call_declined + RtcNotificationState.DeclinedByMe -> CommonStrings.common_call_you_declined + RtcNotificationState.None -> CommonStrings.common_call_started + } +} else { + // Only show declined info in DMs + CommonStrings.common_call_started +} + +@Composable +private fun getIcon( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): ImageVector { + val showAsDeclined = timelineRoomInfo.isDm && ( + content.state == RtcNotificationState.Declined || + content.state == RtcNotificationState.DeclinedByMe + ) + 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 -> + (aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.None)), + (aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None)), + (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined)), + (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined)), + (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.DeclinedByMe)), + (aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None)), + ).forEach { (info, content) -> TimelineItemCallNotifyView( + timelineRoomInfo = info, 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 e75df2f89f..7cd99651a4 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 dff195e833..64320622ba 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 = when { + itemContent.declinedBy.isEmpty().not() && itemContent.declinedBy.any { it == sessionId } -> RtcNotificationState.DeclinedByMe + itemContent.declinedBy.isEmpty().not() -> RtcNotificationState.Declined + else -> RtcNotificationState.None + } ) 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..df1c9f5693 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,24 @@ 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" +// For now this is just an enum, but could be a +// sealed class if we need the list of users who declined. +enum class RtcNotificationState { + /** Some users have declined */ + Declined, + + /** I have declined this call */ + DeclinedByMe, + + // Future sates could be `Missed`? `ongoing`... + None +} + +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..5437711bb7 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,12 @@ 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) { + RtcNotificationState.Declined -> + context.getString(CommonStrings.common_call_declined) + RtcNotificationState.DeclinedByMe -> context.getString(CommonStrings.common_call_you_declined) + RtcNotificationState.None -> context.getString(CommonStrings.common_call_started) + } } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() 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..eac0e9e88d 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 @@ -55,6 +55,7 @@ class DefaultRoomLatestEventFormatter( private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, private val permalinkParser: PermalinkParser, + private val rtcNotificationContentFormatter: RtcNotificationContentFormatter, ) : RoomLatestEventFormatter { override fun format( latestEvent: LatestEventValue.Local, @@ -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/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) ) } } From a6622c678714bec5bc8a749e8e65c1c8dbd51fbb Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Apr 2026 16:03:19 +0200 Subject: [PATCH 02/17] fix deteckt | unneeded paranthesis --- .../components/TimelineItemCallNotifyView.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 aa004a6259..c11aab08ff 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 @@ -127,12 +127,12 @@ private fun getIcon( internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { listOf( - (aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.None)), - (aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None)), - (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined)), - (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined)), - (aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.DeclinedByMe)), - (aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None)), + aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.None), + aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.DeclinedByMe), + aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None), ).forEach { (info, content) -> TimelineItemCallNotifyView( timelineRoomInfo = info, From 61548167967e18b4e217af6d75dff5bee13cf39b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Apr 2026 16:21:36 +0200 Subject: [PATCH 03/17] cleanup of the RTCNotificationState enum --- .../components/TimelineItemCallNotifyView.kt | 24 +++++++++---------- .../event/TimelineItemContentFactory.kt | 8 +++---- .../TimelineItemRtcNotificationContent.kt | 15 ++++-------- .../DefaultMessageSummaryFormatter.kt | 12 ++++++---- 4 files changed, 28 insertions(+), 31 deletions(-) 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 c11aab08ff..f4d0d97495 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 @@ -96,9 +96,10 @@ private fun getTextRes( content: TimelineItemRtcNotificationContent ): Int = if (timelineRoomInfo.isDm) { when (content.state) { - RtcNotificationState.Declined -> CommonStrings.common_call_declined - RtcNotificationState.DeclinedByMe -> CommonStrings.common_call_you_declined - RtcNotificationState.None -> CommonStrings.common_call_started + is RtcNotificationState.Declined -> { + if (content.state.byMe) CommonStrings.common_call_you_declined else CommonStrings.common_call_declined + } + RtcNotificationState.Started -> CommonStrings.common_call_started } } else { // Only show declined info in DMs @@ -110,10 +111,7 @@ private fun getIcon( timelineRoomInfo: TimelineRoomInfo, content: TimelineItemRtcNotificationContent ): ImageVector { - val showAsDeclined = timelineRoomInfo.isDm && ( - content.state == RtcNotificationState.Declined || - content.state == RtcNotificationState.DeclinedByMe - ) + val showAsDeclined = timelineRoomInfo.isDm && content.state is RtcNotificationState.Declined val icon = if (showAsDeclined) { if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallDeclinedSolid() else CompoundIcons.VideoCallDeclinedSolid() } else { @@ -127,12 +125,12 @@ private fun getIcon( internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { listOf( - aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.None), - aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.DeclinedByMe), - aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.None), + aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Started), + aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Started), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined(false)), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined(false)), + aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined(true)), + aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Started), ).forEach { (info, content) -> TimelineItemCallNotifyView( timelineRoomInfo = info, 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 64320622ba..2d884d75fd 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 @@ -106,10 +106,10 @@ class TimelineItemContentFactory( is UnableToDecryptContent -> utdFactory.create(itemContent) is CallNotifyContent -> TimelineItemRtcNotificationContent( callIntent = itemContent.callIntent, - state = when { - itemContent.declinedBy.isEmpty().not() && itemContent.declinedBy.any { it == sessionId } -> RtcNotificationState.DeclinedByMe - itemContent.declinedBy.isEmpty().not() -> RtcNotificationState.Declined - else -> RtcNotificationState.None + state = if (itemContent.declinedBy.isNotEmpty()) { + RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) + } else { + RtcNotificationState.Started } ) is UnknownContent -> TimelineItemUnknownContent 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 df1c9f5693..c09ccd1d21 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 @@ -11,17 +11,12 @@ 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 -// For now this is just an enum, but could be a -// sealed class if we need the list of users who declined. -enum class RtcNotificationState { - /** Some users have declined */ - Declined, +// State of the call, for now only isDeclined but in the future could be missed, active. +sealed class RtcNotificationState { + /** Some users have declined, byMe indicates if the current user is one of them. */ + data class Declined(val byMe: Boolean) : RtcNotificationState() - /** I have declined this call */ - DeclinedByMe, - - // Future sates could be `Missed`? `ongoing`... - None + object Started : RtcNotificationState() } class TimelineItemRtcNotificationContent( 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 5437711bb7..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 @@ -58,10 +58,14 @@ class DefaultMessageSummaryFormatter( is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) is TimelineItemRtcNotificationContent -> when (content.state) { - RtcNotificationState.Declined -> - context.getString(CommonStrings.common_call_declined) - RtcNotificationState.DeclinedByMe -> context.getString(CommonStrings.common_call_you_declined) - RtcNotificationState.None -> context.getString(CommonStrings.common_call_started) + 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 From 531d9b3d47d61b1ba82230a477f654007979332d Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Apr 2026 16:29:13 +0200 Subject: [PATCH 04/17] fix: consist, use sealed interface instead of class --- .../model/event/TimelineItemRtcNotificationContent.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 c09ccd1d21..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 @@ -12,11 +12,11 @@ import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.timeline.item.event.EventType // State of the call, for now only isDeclined but in the future could be missed, active. -sealed class RtcNotificationState { +sealed interface RtcNotificationState { /** Some users have declined, byMe indicates if the current user is one of them. */ - data class Declined(val byMe: Boolean) : RtcNotificationState() + data class Declined(val byMe: Boolean) : RtcNotificationState - object Started : RtcNotificationState() + object Started : RtcNotificationState } class TimelineItemRtcNotificationContent( From 66f68fdccb292d95425a0e1227dc61f06d836a23 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Apr 2026 17:27:51 +0200 Subject: [PATCH 05/17] fixup test compilation --- .../messages/impl/actionlist/ActionListPresenterTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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( From 795299ed0ccab9ad629f1f9d62778a066342ab90 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 30 Apr 2026 09:20:58 +0000 Subject: [PATCH 06/17] Update screenshots --- ...imeline.components_TimelineItemCallNotifyView_Day_0_en.png | 4 ++-- ...eline.components_TimelineItemCallNotifyView_Night_0_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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..13b76dc106 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:0a618360cff745818c0ad066b4c6599ff804d9c022e6b54dfec97aa152a84f29 +size 27716 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..4d1d5d8f00 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:54e56f77c535db49bb9352ca09033fc81ba21b71e7c28079aec8f9bc09dcc1ad +size 26644 From a0646717a313072ba287c70a158e84f99eaaf832 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 4 May 2026 08:59:46 +0200 Subject: [PATCH 07/17] fix test compilation --- .../eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt | 1 + 1 file changed, 1 insertion(+) 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..177cc27df2 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 @@ -75,6 +75,7 @@ class DefaultRoomLatestEventFormatterTest { profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), permalinkParser = FakePermalinkParser(), + rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider) ) } From f05ceb9f5dcfa3f396fe737f72922121cceb7967 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 10:47:17 +0200 Subject: [PATCH 08/17] review: Use @stringRes annotation for Int --- .../impl/timeline/components/TimelineItemCallNotifyView.kt | 2 ++ 1 file changed, 2 insertions(+) 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 f4d0d97495..0c3e548f4b 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 @@ -91,6 +92,7 @@ internal fun TimelineItemCallNotifyView( } } +@StringRes private fun getTextRes( timelineRoomInfo: TimelineRoomInfo, content: TimelineItemRtcNotificationContent From 885d0f2cb926532e4485fd9f7d83cdbeccd5e5cf Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 10:47:32 +0200 Subject: [PATCH 09/17] review: better comment --- .../impl/timeline/components/TimelineItemCallNotifyView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c3e548f4b..0c9c4e68c0 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 @@ -104,7 +104,7 @@ private fun getTextRes( RtcNotificationState.Started -> CommonStrings.common_call_started } } else { - // Only show declined info in DMs + // In Rooms, do not show declined info. CommonStrings.common_call_started } From 4437d4d9df8e63f8914c1a2be94e81f9d766c59f Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 10:47:46 +0200 Subject: [PATCH 10/17] review: Invert if for better readability --- .../timeline/factories/event/TimelineItemContentFactory.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 2d884d75fd..b3409aa567 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 @@ -106,10 +106,10 @@ class TimelineItemContentFactory( is UnableToDecryptContent -> utdFactory.create(itemContent) is CallNotifyContent -> TimelineItemRtcNotificationContent( callIntent = itemContent.callIntent, - state = if (itemContent.declinedBy.isNotEmpty()) { - RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) - } else { + state = if (itemContent.declinedBy.isEmpty()) { RtcNotificationState.Started + } else { + RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) } ) is UnknownContent -> TimelineItemUnknownContent From 9a8046c02d1ada564dc129d5cabf09d238734bf2 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 10:48:50 +0200 Subject: [PATCH 11/17] review: Update signature and keep formatters grouped together --- .../eventformatter/impl/DefaultRoomLatestEventFormatter.kt | 2 +- .../impl/DefaultRoomLatestEventFormatterTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 eac0e9e88d..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,8 +54,8 @@ class DefaultRoomLatestEventFormatter( private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, - private val permalinkParser: PermalinkParser, private val rtcNotificationContentFormatter: RtcNotificationContentFormatter, + private val permalinkParser: PermalinkParser, ) : RoomLatestEventFormatter { override fun format( latestEvent: LatestEventValue.Local, 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 177cc27df2..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,8 +74,8 @@ class DefaultRoomLatestEventFormatterTest { roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), - permalinkParser = FakePermalinkParser(), - rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider) + rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider), + permalinkParser = FakePermalinkParser() ) } From 9ce825308677a5f58be978e887f06aceeb5c3b61 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 11:16:45 +0200 Subject: [PATCH 12/17] review: Add unit test for notificationFormater --- .../RtcNotificationContentFormatterTest.kt | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt 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) + } +} From f0f5e8be25e642d44c6f470158a40ae4e3f3f28e Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 11:20:10 +0200 Subject: [PATCH 13/17] review: Refactor preview, show all variants --- .../components/TimelineItemCallNotifyView.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) 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 0c9c4e68c0..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 @@ -125,21 +125,23 @@ private fun getIcon( @PreviewsDayNight @Composable internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - listOf( - aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Started), - aTimelineRoomInfo() to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Started), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.AUDIO, RtcNotificationState.Declined(false)), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined(false)), - aTimelineRoomInfo(isDm = true) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Declined(true)), - aTimelineRoomInfo(isDm = false) to TimelineItemRtcNotificationContent(CallIntent.VIDEO, RtcNotificationState.Started), - ).forEach { (info, content) -> - TimelineItemCallNotifyView( - timelineRoomInfo = info, - 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 = {}, + ) + } + } } } } From c5a054b9e3d48b1df20b0f25735b7e19f0eb3de7 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 11 May 2026 09:36:54 +0000 Subject: [PATCH 14/17] Update screenshots --- ...imeline.components_TimelineItemCallNotifyView_Day_0_en.png | 4 ++-- ...eline.components_TimelineItemCallNotifyView_Night_0_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 13b76dc106..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:0a618360cff745818c0ad066b4c6599ff804d9c022e6b54dfec97aa152a84f29 -size 27716 +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 4d1d5d8f00..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:54e56f77c535db49bb9352ca09033fc81ba21b71e7c28079aec8f9bc09dcc1ad -size 26644 +oid sha256:fa29aaa82f21912dd5147ffb6fdc457fd5880e8be4abde4f0177d0ab1a412fe2 +size 48245 From 6bc8cd84e4ce625a5e2a484393cb84970dab04e0 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 16:54:30 +0200 Subject: [PATCH 15/17] fixup test compilation --- .../mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From a227b830beb62b517c651a6be76cb7cad412aa19 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 18:08:28 +0200 Subject: [PATCH 16/17] add test to MessageSummaryFormatter --- .../DefaultMessageSummaryFormatterTest.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt 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..7d0a097b3c --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt @@ -0,0 +1,105 @@ +/* + * 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 = 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")!! + ) + } +) From d299b722e3e0514708e075e4d666d2424da493a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 12 May 2026 15:50:47 +0200 Subject: [PATCH 17/17] Format code. --- .../DefaultMessageSummaryFormatterTest.kt | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) 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 index 7d0a097b3c..664d21ed64 100644 --- 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 @@ -33,33 +33,36 @@ class DefaultMessageSummaryFormatterTest { @Test @Config(qualifiers = "en") fun `format call notification started`() { - val expected = formatter.format(TimelineItemRtcNotificationContent( - callIntent = CallIntent.VIDEO, - state = RtcNotificationState.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) - )) - + 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) - )) - + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Declined(byMe = false) + ) + ) assertThat(expected).isEqualTo("Call declined") } @@ -69,7 +72,6 @@ class DefaultMessageSummaryFormatterTest { val expected = formatter.format( aLocationContent(isLive = true) ) - assertThat(expected).isEqualTo("Shared live location") } @@ -79,27 +81,26 @@ class DefaultMessageSummaryFormatterTest { val expected = formatter.format( aLocationContent(isLive = false) ) - assertThat(expected).isEqualTo("Shared location") } } -private fun aLocationContent(isLive: Boolean): TimelineItemLocationContent = TimelineItemLocationContent( +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 - ) + lastKnownLocation = Location.fromGeoUri("geo:1,5"), + isActive = true, + endsAt = "", + endTimestamp = 0, + isOwnUser = true, + ) } else { Mode.Static( - location = Location.fromGeoUri("geo:1,5")!! - ) + location = Location.fromGeoUri("geo:1,5")!!, + ) } )