Introduce LiveLocationContent for the timeline (needs sdk)

This commit is contained in:
ganfra 2026-03-03 22:22:02 +01:00
parent f4bf596e3b
commit 046d135e4b
20 changed files with 235 additions and 82 deletions

View file

@ -32,6 +32,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.features.location.api.internal.StaticMapPlaceholder import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.StaticMapUrlBuilder import io.element.android.features.location.api.internal.StaticMapUrlBuilder
import io.element.android.features.location.api.internal.centerBottomEdge import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.libraries.designsystem.components.LocationPinMarker
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.preview.ElementPreview 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
@ -45,6 +47,7 @@ fun StaticMapView(
lat: Double, lat: Double,
lon: Double, lon: Double,
zoom: Double, zoom: Double,
pinVariant: PinVariant,
contentDescription: String?, contentDescription: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
darkMode: Boolean = !ElementTheme.isLightTheme, darkMode: Boolean = !ElementTheme.isLightTheme,
@ -95,12 +98,7 @@ fun StaticMapView(
// We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case. // We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case.
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
Icon( LocationPinMarker(variant = pinVariant, modifier = Modifier.centerBottomEdge(this))
resourceId = CommonDrawables.pin,
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.centerBottomEdge(this),
)
} else { } else {
StaticMapPlaceholder( StaticMapPlaceholder(
showProgress = collectedState.value.isLoading(), showProgress = collectedState.value.isLoading(),
@ -127,6 +125,7 @@ internal fun StaticMapViewPreview() = ElementPreview {
lon = 0.0, lon = 0.0,
zoom = 0.0, zoom = 0.0,
contentDescription = null, contentDescription = null,
pinVariant = PinVariant.PinnedLocation,
modifier = Modifier.size(400.dp), modifier = Modifier.size(400.dp),
) )
} }

View file

@ -58,7 +58,7 @@ internal class MapTilerStaticMapUrlBuilder(
// image smaller than the available space in pixels. // image smaller than the available space in pixels.
// The resulting image will have to be scaled to fit the available space in order // The resulting image will have to be scaled to fit the available space in order
// to keep the perceived content size constant at the expense of sharpness. // to keep the perceived content size constant at the expense of sharpness.
return "$baseUrl/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft" return "$baseUrl/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=topright"
} }
override fun isServiceAvailable() = apiKey.isNotEmpty() override fun isServiceAvailable() = apiKey.isNotEmpty()

View file

@ -39,7 +39,7 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -166,7 +166,7 @@ internal fun aTimelineItemEvent(
isMine = isMine, isMine = isMine,
isEditable = isEditable, isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo, canBeRepliedTo = canBeRepliedTo,
senderProfile = aProfileTimelineDetailsReady( senderProfile = aProfileDetailsReady(
displayName = senderDisplayName, displayName = senderDisplayName,
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
), ),

View file

@ -8,10 +8,8 @@
package io.element.android.features.messages.impl.timeline.components.event package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -19,33 +17,28 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.location.api.StaticMapView import io.element.android.features.location.api.StaticMapView
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview 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.Text
@Composable @Composable
fun TimelineItemLocationView( fun TimelineItemLocationView(
content: TimelineItemLocationContent, content: TimelineItemLocationContent,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier.fillMaxWidth()) { StaticMapView(
content.description?.let { modifier = modifier
Text( .fillMaxWidth()
text = it, .heightIn(max = 188.dp),
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp), pinVariant = content.pinVariant,
) lat = content.location.lat,
} lon = content.location.lon,
zoom = 15.0,
StaticMapView( contentDescription = content.body
modifier = Modifier )
.fillMaxWidth()
.heightIn(max = 188.dp),
lat = content.location.lat,
lon = content.location.lon,
zoom = 15.0,
contentDescription = content.body
)
}
} }
@PreviewsDayNight @PreviewsDayNight

View file

@ -9,8 +9,10 @@
package io.element.android.features.messages.impl.timeline.factories.event package io.element.android.features.messages.impl.timeline.factories.event
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.location.api.Location
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent 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.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -22,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
@ -70,10 +73,10 @@ class TimelineItemContentFactory(
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
is MessageContent -> { is MessageContent -> {
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
messageFactory.create( messageFactory.create(
senderId = sender,
senderProfile = senderProfile,
content = itemContent, content = itemContent,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
eventId = eventId, eventId = eventId,
) )
} }
@ -96,6 +99,24 @@ class TimelineItemContentFactory(
is UnableToDecryptContent -> utdFactory.create(itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent)
is CallNotifyContent -> TimelineItemRtcNotificationContent() is CallNotifyContent -> TimelineItemRtcNotificationContent()
is UnknownContent -> TimelineItemUnknownContent is UnknownContent -> TimelineItemUnknownContent
is LiveLocationContent -> {
val lastKnownLocation = itemContent.locations.mapNotNull { beacon ->
Location.fromGeoUri(beacon.geoUri)
}.lastOrNull()
if (lastKnownLocation != null) {
TimelineItemLocationContent(
body = itemContent.body.trimEnd(),
description = itemContent.description?.trimEnd(),
assetType = itemContent.assetType,
senderId = sender,
senderProfile = senderProfile,
location = lastKnownLocation,
mode = TimelineItemLocationContent.Mode.Live(isActive = itemContent.isLive)
)
} else {
TimelineItemUnknownContent
}
}
} }
} }
} }

View file

@ -29,8 +29,13 @@ import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.text.safeLinkify import io.element.android.libraries.androidutils.text.safeLinkify
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@ -39,10 +44,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.messages.toHtmlDocument import io.element.android.libraries.matrix.ui.messages.toHtmlDocument
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -65,11 +73,13 @@ class TimelineItemContentMessageFactory(
) { ) {
fun create( fun create(
content: MessageContent, content: MessageContent,
senderDisambiguatedDisplayName: String, senderId: UserId,
senderProfile: ProfileDetails,
eventId: EventId?, eventId: EventId?,
): TimelineItemEventContent { ): TimelineItemEventContent {
return when (val messageType = content.type) { return when (val messageType = content.type) {
is EmoteMessageType -> { is EmoteMessageType -> {
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(senderId)
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}" val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
val dom = messageType.formatted?.toHtmlDocument( val dom = messageType.formatted?.toHtmlDocument(
permalinkParser = permalinkParser, permalinkParser = permalinkParser,
@ -135,8 +145,8 @@ class TimelineItemContentMessageFactory(
} }
is LocationMessageType -> { is LocationMessageType -> {
val location = Location.fromGeoUri(messageType.geoUri) val location = Location.fromGeoUri(messageType.geoUri)
val body = messageType.body.trimEnd()
if (location == null) { if (location == null) {
val body = messageType.body.trimEnd()
TimelineItemTextContent( TimelineItemTextContent(
body = body, body = body,
htmlDocument = null, htmlDocument = null,
@ -145,9 +155,13 @@ class TimelineItemContentMessageFactory(
) )
} else { } else {
TimelineItemLocationContent( TimelineItemLocationContent(
body = messageType.body.trimEnd(), body = body,
location = location, location = location,
description = messageType.description description = messageType.description,
senderId = senderId,
senderProfile = senderProfile,
assetType = messageType.assetType,
mode = TimelineItemLocationContent.Mode.Static
) )
} }
} }

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyCon
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
@ -81,7 +82,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
RedactedContent, RedactedContent,
is StickerContent, is StickerContent,
is PollContent, is PollContent,
is UnableToDecryptContent -> true is UnableToDecryptContent,
is LiveLocationContent -> true
// Can't be grouped // Can't be grouped
is FailedToParseStateContent, is FailedToParseStateContent,
is ProfileChangeContent, is ProfileChangeContent,

View file

@ -28,7 +28,8 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
aTimelineItemAudioContent("An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"), aTimelineItemAudioContent("An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"),
aTimelineItemVoiceContent(), aTimelineItemVoiceContent(),
aTimelineItemLocationContent(), aTimelineItemLocationContent(),
aTimelineItemLocationContent("Location description"), aTimelineItemLocationContent(),
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
aTimelineItemPollContent(), aTimelineItemPollContent(),
aTimelineItemNoticeContent(), aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(), aTimelineItemRedactedContent(),

View file

@ -9,13 +9,55 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
data class TimelineItemLocationContent( data class TimelineItemLocationContent(
val body: String, val body: String,
val senderId: UserId,
val senderProfile: ProfileDetails,
val location: Location, val location: Location,
val description: String? = null, val description: String? = null,
val assetType: AssetType? = null, val assetType: AssetType? = null,
val mode: Mode,
) : TimelineItemEventContent { ) : TimelineItemEventContent {
val pinVariant = when (mode) {
is Mode.Live -> {
if (mode.isActive) {
PinVariant.UserLocation(avatarData = senderAvatar(), isLive = true)
} else {
PinVariant.StaleLocation
}
}
Mode.Static -> {
when (assetType) {
AssetType.PIN -> PinVariant.PinnedLocation
AssetType.SENDER,
null -> PinVariant.UserLocation(avatarData = senderAvatar(), isLive = false)
}
}
}
private fun senderAvatar() = AvatarData(
senderId.value,
name = senderProfile.getDisplayName(),
url = senderProfile.getAvatarUrl(),
// Size is irrelevant as the PinMarker will override anyway.
size = AvatarSize.TimelineSender
)
sealed interface Mode {
data object Static : Mode
data class Live(val isActive: Boolean) : Mode
}
override val type: String = "TimelineItemLocationContent" override val type: String = "TimelineItemLocationContent"
} }

View file

@ -10,21 +10,34 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
open class TimelineItemLocationContentProvider : PreviewParameterProvider<TimelineItemLocationContent> { open class TimelineItemLocationContentProvider : PreviewParameterProvider<TimelineItemLocationContent> {
override val values: Sequence<TimelineItemLocationContent> override val values: Sequence<TimelineItemLocationContent>
get() = sequenceOf( get() = sequenceOf(
aTimelineItemLocationContent(), aTimelineItemLocationContent(),
aTimelineItemLocationContent("This is a description!"), aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = false)),
) )
} }
fun aTimelineItemLocationContent(description: String? = null) = TimelineItemLocationContent( fun aTimelineItemLocationContent(
body = "User location geo:52.2445,0.7186;u=5000", body: String = "",
senderId: UserId = UserId("@sender:matrix.org"),
senderProfile: ProfileDetails = aProfileDetailsReady(),
mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static,
) = TimelineItemLocationContent(
body = body,
location = Location( location = Location(
lat = 52.2445, lat = 52.2445,
lon = 0.7186, lon = 0.7186,
accuracy = 5000f, accuracy = 5000f,
), ),
description = description, senderId = senderId,
senderProfile = senderProfile,
mode = mode
) )

View file

@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.FakeSendHandle
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
internal fun aMessageEvent( internal fun aMessageEvent(
@ -52,7 +52,7 @@ internal fun aMessageEvent(
eventId = eventId, eventId = eventId,
transactionId = transactionId, transactionId = transactionId,
senderId = A_USER_ID, senderId = A_USER_ID,
senderProfile = aProfileTimelineDetailsReady(displayName = A_USER_NAME), senderProfile = aProfileDetailsReady(displayName = A_USER_NAME),
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender), senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
content = content, content = content,
sentTime = "", sentTime = "",

View file

@ -60,8 +60,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aProfileDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
@ -84,7 +86,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")), content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -102,14 +105,18 @@ class TimelineItemContentMessageFactoryTest {
val assetType = AssetType.SENDER val assetType = AssetType.SENDER
val result = sut.create( val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description", assetType)), content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description", assetType)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemLocationContent( val expected = TimelineItemLocationContent(
body = "body", body = "body",
location = Location(lat = 1.0, lon = 2.0, accuracy = 0.0F), location = Location(lat = 1.0, lon = 2.0, accuracy = null),
description = "description", description = "description",
assetType = assetType, assetType = assetType,
mode = TimelineItemLocationContent.Mode.Static,
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
) )
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
} }
@ -119,7 +126,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "", null, null)), content = createMessageContent(type = LocationMessageType("body", "", null, null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -136,7 +144,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = TextMessageType("body", null)), content = createMessageContent(type = TextMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -153,7 +162,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = TextMessageType("https://www.example.org", null)), content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) as TimelineItemTextContent ) as TimelineItemTextContent
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -200,7 +210,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, expected.toString()) formatted = FormattedBody(MessageFormat.HTML, expected.toString())
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected) assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
@ -218,7 +229,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted") formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body")) assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body"))
@ -229,7 +241,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VideoMessageType("filename", null, null, MediaSource("url"), null)), content = createMessageContent(type = VideoMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
@ -282,7 +295,8 @@ class TimelineItemContentMessageFactoryTest {
), ),
isEdited = true, isEdited = true,
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
@ -312,7 +326,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = AudioMessageType("filename", null, null, MediaSource("url"), null)), content = createMessageContent(type = AudioMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
@ -348,7 +363,8 @@ class TimelineItemContentMessageFactoryTest {
), ),
isEdited = true, isEdited = true,
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
@ -371,7 +387,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VoiceMessageType("filename", null, null, MediaSource("url"), null, null)), content = createMessageContent(type = VoiceMessageType("filename", null, null, MediaSource("url"), null, null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
@ -413,7 +430,8 @@ class TimelineItemContentMessageFactoryTest {
), ),
isEdited = true, isEdited = true,
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
@ -438,7 +456,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = ImageMessageType("filename", "body", null, MediaSource("url"), null)), content = createMessageContent(type = ImageMessageType("filename", "body", null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
@ -518,7 +537,8 @@ class TimelineItemContentMessageFactoryTest {
), ),
isEdited = true, isEdited = true,
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
@ -547,7 +567,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = FileMessageType("filename", null, null, MediaSource("url"), null)), content = createMessageContent(type = FileMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
@ -589,7 +610,8 @@ class TimelineItemContentMessageFactoryTest {
), ),
isEdited = true, isEdited = true,
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
@ -612,7 +634,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = NoticeMessageType("body", null)), content = createMessageContent(type = NoticeMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemNoticeContent( val expected = TimelineItemNoticeContent(
@ -634,7 +657,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted") formatted = FormattedBody(MessageFormat.HTML, "formatted")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
(result as TimelineItemNoticeContent).formattedBody.assertSpannedEquals(SpannedString("formatted")) (result as TimelineItemNoticeContent).formattedBody.assertSpannedEquals(SpannedString("formatted"))
@ -645,7 +669,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = EmoteMessageType("body", null)), content = createMessageContent(type = EmoteMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails("Bob"),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemEmoteContent( val expected = TimelineItemEmoteContent(
@ -667,7 +692,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted") formatted = FormattedBody(MessageFormat.HTML, "formatted")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails("Bob"),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
@ -693,7 +719,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test <a href=\"https://www.example.org\">me@matrix.org</a>") formatted = FormattedBody(MessageFormat.HTML, "Test <a href=\"https://www.example.org\">me@matrix.org</a>")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned) (result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
@ -718,7 +745,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org") formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned) (result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
@ -744,7 +772,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org") formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org")
) )
), ),
senderDisambiguatedDisplayName = "Bob", senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSen
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.FakeSendHandle
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.junit.Test import org.junit.Test
@ -34,7 +34,7 @@ class TimelineItemGrouperTest {
id = UniqueId("0"), id = UniqueId("0"),
senderId = A_USER_ID, senderId = A_USER_ID,
senderAvatar = anAvatarData(), senderAvatar = anAvatarData(),
senderProfile = aProfileTimelineDetailsReady(displayName = ""), senderProfile = aProfileDetailsReady(displayName = ""),
content = TimelineItemStateEventContent(body = "a state event"), content = TimelineItemStateEventContent(body = "a state event"),
reactionsState = aTimelineItemReactions(count = 0), reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()), readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
@ -115,6 +116,10 @@ class DefaultRoomLatestEventFormatter(
val message = sp.getString(CommonStrings.common_unsupported_event) val message = sp.getString(CommonStrings.common_unsupported_event)
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
} }
is LiveLocationContent -> {
val message = sp.getString(CommonStrings.common_shared_location)
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
}
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call) is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
}?.take(DEFAULT_SAFE_LENGTH) }?.take(DEFAULT_SAFE_LENGTH)

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
@ -69,6 +70,7 @@ class DefaultTimelineEventFormatter(
is MessageContent, is MessageContent,
is FailedToParseMessageLikeContent, is FailedToParseMessageLikeContent,
is FailedToParseStateContent, is FailedToParseStateContent,
is LiveLocationContent,
is UnknownContent -> { is UnknownContent -> {
if (buildMeta.isDebuggable) { if (buildMeta.isDebuggable) {
error("You should not use this formatter for this event content: $content") error("You should not use this formatter for this event content: $content")

View file

@ -0,0 +1,15 @@
/*
* 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.matrix.api.room.location
data class LiveLocationInfo(
val description: String?,
val geoUri: String,
val timestamp: Long,
)

View file

@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.location.LiveLocationInfo
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
@ -102,6 +104,15 @@ data class FailedToParseStateContent(
val error: String val error: String
) : EventContent ) : EventContent
data class LiveLocationContent(
val body: String,
val isLive: Boolean,
val description: String?,
val timeout: Long,
val assetType: AssetType?,
val locations: List<LiveLocationInfo>,
): EventContent
data object LegacyCallInviteContent : EventContent data object LegacyCallInviteContent : EventContent
data object CallNotifyContent : EventContent data object CallNotifyContent : EventContent

View file

@ -152,13 +152,13 @@ private fun aInReplyToDetails(
eventId = EventId("\$event"), eventId = EventId("\$event"),
eventContent = eventContent, eventContent = eventContent,
senderId = UserId("@Sender:domain"), senderId = UserId("@Sender:domain"),
senderProfile = aProfileTimelineDetailsReady( senderProfile = aProfileDetailsReady(
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
), ),
textContent = (eventContent as? MessageContent)?.body.orEmpty(), textContent = (eventContent as? MessageContent)?.body.orEmpty(),
) )
fun aProfileTimelineDetailsReady( fun aProfileDetailsReady(
displayName: String? = "Sender", displayName: String? = "Sender",
displayNameAmbiguous: Boolean = false, displayNameAmbiguous: Boolean = false,
avatarUrl: String? = null, avatarUrl: String? = null,

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
@ -32,6 +33,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Text
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Thumbnail
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Immutable @Immutable
@ -60,7 +63,7 @@ internal sealed interface InReplyToMetadata {
@Composable @Composable
internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetadata? = when (eventContent) { internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetadata? = when (eventContent) {
is MessageContent -> when (val type = eventContent.type) { is MessageContent -> when (val type = eventContent.type) {
is ImageMessageType -> InReplyToMetadata.Thumbnail( is ImageMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
thumbnailSource = (type.info?.thumbnailSource ?: type.source).takeUnless { hideImage }, thumbnailSource = (type.info?.thumbnailSource ?: type.source).takeUnless { hideImage },
textContent = eventContent.body, textContent = eventContent.body,
@ -68,7 +71,7 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad
blurHash = type.info?.blurhash, blurHash = type.info?.blurhash,
) )
) )
is VideoMessageType -> InReplyToMetadata.Thumbnail( is VideoMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage }, thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage },
textContent = eventContent.body, textContent = eventContent.body,
@ -76,34 +79,34 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad
blurHash = type.info?.blurhash, blurHash = type.info?.blurhash,
) )
) )
is FileMessageType -> InReplyToMetadata.Thumbnail( is FileMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage }, thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage },
textContent = eventContent.body, textContent = eventContent.body,
type = AttachmentThumbnailType.File, type = AttachmentThumbnailType.File,
) )
) )
is LocationMessageType -> InReplyToMetadata.Thumbnail( is LocationMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
textContent = stringResource(CommonStrings.common_shared_location), textContent = stringResource(CommonStrings.common_shared_location),
type = AttachmentThumbnailType.Location, type = AttachmentThumbnailType.Location,
) )
) )
is AudioMessageType -> InReplyToMetadata.Thumbnail( is AudioMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
textContent = eventContent.body, textContent = eventContent.body,
type = AttachmentThumbnailType.Audio, type = AttachmentThumbnailType.Audio,
) )
) )
is VoiceMessageType -> InReplyToMetadata.Thumbnail( is VoiceMessageType -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
textContent = stringResource(CommonStrings.common_voice_message), textContent = stringResource(CommonStrings.common_voice_message),
type = AttachmentThumbnailType.Voice, type = AttachmentThumbnailType.Voice,
) )
) )
else -> InReplyToMetadata.Text(textContent ?: eventContent.body) else -> Text(textContent ?: eventContent.body)
} }
is StickerContent -> InReplyToMetadata.Thumbnail( is StickerContent -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
thumbnailSource = eventContent.source.takeUnless { hideImage }, thumbnailSource = eventContent.source.takeUnless { hideImage },
textContent = eventContent.body, textContent = eventContent.body,
@ -111,7 +114,7 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad
blurHash = eventContent.info.blurhash, blurHash = eventContent.info.blurhash,
) )
) )
is PollContent -> InReplyToMetadata.Thumbnail( is PollContent -> Thumbnail(
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
textContent = eventContent.question, textContent = eventContent.question,
type = AttachmentThumbnailType.Poll, type = AttachmentThumbnailType.Poll,
@ -127,5 +130,6 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad
UnknownContent, UnknownContent,
is LegacyCallInviteContent, is LegacyCallInviteContent,
is CallNotifyContent, is CallNotifyContent,
is LiveLocationContent,
null -> null null -> null
} }

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
@ -75,6 +76,7 @@ class EventItemFactory(
is StateContent, is StateContent,
is StickerContent, is StickerContent,
is UnableToDecryptContent, is UnableToDecryptContent,
is LiveLocationContent,
UnknownContent -> { UnknownContent -> {
Timber.w("Should not happen: ${content.javaClass.simpleName}") Timber.w("Should not happen: ${content.javaClass.simpleName}")
null null