Merge pull request #726 from vector-im/feature/cjs/view-location-in-timeline

Show location events in the timeline
This commit is contained in:
Chris Smith 2023-06-30 13:00:00 +01:00 committed by GitHub
commit 6ae72709dc
36 changed files with 405 additions and 40 deletions

1
changelog.d/689.feature Normal file
View file

@ -0,0 +1 @@
Show location events in the timeline

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.api
private const val GEO_URI_REGEX = """geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?"""
data class Location(
val lat: Double,
val lon: Double,
val accuracy: Float,
)
fun parseGeoUri(geoUri: String): Location? {
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
return Location (
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
)
}

View file

@ -34,10 +34,12 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import io.element.android.features.location.api.internal.AttributionPlacement
import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.theme.ElementTheme
import timber.log.Timber
@ -73,9 +75,13 @@ fun StaticMapView(
lat = lat,
lon = lon,
desiredZoom = zoom,
desiredWidth = constraints.maxWidth,
desiredHeight = constraints.maxHeight,
darkMode = darkMode,
attributionPlacement = AttributionPlacement.BottomLeft,
// Size the map based on DP rather than pixels, as otherwise the features and attribution
// end up being illegibly tiny on high density displays.
desiredWidth = constraints.maxWidth.toDp().value.toInt(),
desiredHeight = constraints.maxHeight.toDp().value.toInt(),
doubleScale = true,
)
)
.size(width = constraints.maxWidth, height = constraints.maxHeight)

View file

@ -23,7 +23,7 @@ private const val BASE_URL = "https://api.maptiler.com"
private const val LIGHT_MAP_ID = "9bc819c8-e627-474a-a348-ec144fe3d810"
private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3"
private const val STATIC_MAP_FORMAT = "webp"
private const val STATIC_MAP_SCALE = "" // Either "" (empty string) for normal image or "@2x" for retina images.
private const val STATIC_MAP_SCALE_2X = "@2x"
private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048
private const val STATIC_MAP_MAX_ZOOM = 22.0
@ -35,6 +35,14 @@ fun buildTileServerUrl(
"$BASE_URL/maps/$DARK_MAP_ID/style.json?key=$API_KEY"
}
internal enum class AttributionPlacement(val value: String) {
BottomRight("bottomright"),
BottomLeft("bottomleft"),
TopLeft("topleft"),
TopRight("topright"),
Hidden("false"),
}
/**
* Builds a valid URL for maptiler.com static map api based on the given params.
*
@ -50,7 +58,9 @@ internal fun buildStaticMapsApiUrl(
desiredZoom: Double,
desiredWidth: Int,
desiredHeight: Int,
darkMode: Boolean
darkMode: Boolean,
doubleScale: Boolean,
attributionPlacement: AttributionPlacement,
): String {
require(desiredWidth > 0 && desiredHeight > 0) {
"Width ($desiredHeight) and height ($desiredHeight) must be > 0"
@ -72,9 +82,10 @@ internal fun buildStaticMapsApiUrl(
width = (height * aspectRatio).roundToInt()
}
}
return if (!darkMode) {
"$BASE_URL/maps/$LIGHT_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY"
} else {
"$BASE_URL/maps/$DARK_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY"
}
val mapId = if (darkMode) DARK_MAP_ID else LIGHT_MAP_ID
val scaleSuffix = if (doubleScale) STATIC_MAP_SCALE_2X else ""
return "$BASE_URL/maps/$mapId/static/${lon},${lat},${zoom}/${width}x${height}${scaleSuffix}.$STATIC_MAP_FORMAT" +
"?key=$API_KEY&attribution=${attributionPlacement.value}"
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.api
import com.google.common.truth.Truth.assertThat
import org.junit.Test
internal class GeoUrisKtTest {
@Test
fun `parseGeoUri - returns null for invalid urls`() {
assertThat(parseGeoUri("")).isNull()
assertThat(parseGeoUri("http://example.com/")).isNull()
assertThat(parseGeoUri("geo:")).isNull()
assertThat(parseGeoUri("geo:1.234")).isNull()
assertThat(parseGeoUri("geo:1.234,")).isNull()
assertThat(parseGeoUri("geo:,1.234")).isNull()
assertThat(parseGeoUri("notgeo:1.234,5.678")).isNull()
assertThat(parseGeoUri("geo:+1.234,5.678")).isNull()
assertThat(parseGeoUri("geo:+1.234,*5.678")).isNull()
assertThat(parseGeoUri("geo:not,good")).isNull()
assertThat(parseGeoUri("geo:1.234,5.678;u=wrong")).isNull()
assertThat(parseGeoUri("geo:1.234,5.678trailing")).isNull()
}
@Test
fun `parseGeoUri - returns location for valid urls`() {
assertThat(parseGeoUri("geo:1.234,5.678")).isEqualTo(Location(
lat = 1.234,
lon = 5.678,
accuracy = 0f,
))
assertThat(parseGeoUri("geo:1,5")).isEqualTo(Location(
lat = 1.0,
lon = 5.0,
accuracy = 0f,
))
assertThat(parseGeoUri("geo:1.234,5.678;u=3000")).isEqualTo(Location(
lat = 1.234,
lon = 5.678,
accuracy = 3000f,
))
assertThat(parseGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
lat = 1.0,
lon = 5.0,
accuracy = 3000f,
))
assertThat(parseGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
lat = -1.234,
lon = -5.678,
accuracy = 9.10f,
))
assertThat(parseGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
lat = -1.0,
lon = -5.0,
accuracy = 9.10f,
))
}
}

View file

@ -17,7 +17,6 @@
package io.element.android.features.location.api.internal
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
import org.junit.Test
class BuildStaticMapsApiUrlTest {
@ -30,10 +29,13 @@ class BuildStaticMapsApiUrlTest {
desiredZoom = 1.2,
desiredWidth = 100,
desiredHeight = 200,
darkMode = false
darkMode = false,
doubleScale = false,
attributionPlacement = AttributionPlacement.BottomLeft,
)
).isEqualTo(
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx"
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" +
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft"
)
}
@ -46,10 +48,51 @@ class BuildStaticMapsApiUrlTest {
desiredZoom = 1.2,
desiredWidth = 100,
desiredHeight = 200,
darkMode = true
darkMode = true,
doubleScale = false,
attributionPlacement = AttributionPlacement.BottomLeft,
)
).isEqualTo(
"https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx"
"https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp" +
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft"
)
}
@Test
fun `buildStaticMapsApiUrl builds double scale mode url`() {
assertThat(
buildStaticMapsApiUrl(
lat = 1.234,
lon = 5.678,
desiredZoom = 1.2,
desiredWidth = 100,
desiredHeight = 200,
darkMode = false,
doubleScale = true,
attributionPlacement = AttributionPlacement.BottomLeft,
)
).isEqualTo(
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200@2x.webp" +
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft"
)
}
@Test
fun `buildStaticMapsApiUrl builds no attribution url`() {
assertThat(
buildStaticMapsApiUrl(
lat = 1.234,
lon = 5.678,
desiredZoom = 1.2,
desiredWidth = 100,
desiredHeight = 200,
darkMode = false,
doubleScale = false,
attributionPlacement = AttributionPlacement.Hidden,
)
).isEqualTo(
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" +
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=false"
)
}
@ -62,10 +105,13 @@ class BuildStaticMapsApiUrlTest {
desiredZoom = 100.0,
desiredWidth = 8192,
desiredHeight = 4096,
darkMode = false
darkMode = false,
doubleScale = false,
attributionPlacement = AttributionPlacement.BottomLeft,
)
).isEqualTo(
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp?key=fU3vlMsMn4Jb6dnEIFsx"
"https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp" +
"?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft"
)
}
}

View file

@ -45,6 +45,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@ -267,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor(
is TimelineItemRedactedContent,
is TimelineItemStateContent,
is TimelineItemEncryptedContent,
is TimelineItemLocationContent,
is TimelineItemUnknownContent -> null
}
val composerMode = MessageComposerMode.Reply(

View file

@ -58,6 +58,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@ -231,6 +232,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
is TimelineItemStateContent,
is TimelineItemEncryptedContent,
is TimelineItemRedactedContent,
is TimelineItemLocationContent,
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
is TimelineItemImageContent -> {
icon = {

View file

@ -55,6 +55,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
@ -224,7 +225,9 @@ private fun MessageEventBubbleContent(
onTimestampClicked: () -> Unit,
modifier: Modifier = Modifier
) {
val isMediaItem = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent
val isMediaItem = event.content is TimelineItemImageContent
|| event.content is TimelineItemVideoContent
|| event.content is TimelineItemLocationContent
val replyToDetails = event.inReplyTo as? InReplyTo.Ready
// Long clicks are not not automatically propagated from a `clickable`

View file

@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@ -62,6 +63,10 @@ fun TimelineItemEventContentView(
extraPadding = extraPadding,
modifier = modifier
)
is TimelineItemLocationContent -> TimelineItemLocationView(
content = content,
modifier = modifier
)
is TimelineItemImageContent -> TimelineItemImageView(
content = content,
modifier = modifier,

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.TimelineItemLocationContentProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemLocationView(
content: TimelineItemLocationContent,
modifier: Modifier = Modifier,
) {
StaticMapView(
modifier = modifier
.fillMaxWidth()
.heightIn(max = 188.dp),
lat = content.location.lat,
lon = content.location.lon,
zoom = 15.0,
contentDescription = content.body
)
}
@Preview
@Composable
internal fun TimelineItemLocationViewLightPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemLocationViewDarkPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: TimelineItemLocationContent) {
TimelineItemLocationView(content)
}

View file

@ -16,10 +16,12 @@
package io.element.android.features.messages.impl.timeline.factories.event
import io.element.android.features.location.api.parseGeoUri
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
@ -31,6 +33,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
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.ImageMessageType
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.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
@ -64,6 +67,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
)
}
is LocationMessageType -> {
val location = parseGeoUri(messageType.geoUri)
if (location == null) {
TimelineItemTextContent(
body = messageType.body,
htmlDocument = null,
isEdited = content.isEdited,
)
} else {
TimelineItemLocationContent(
body = messageType.body,
location = location,
)
}
}
is VideoMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemVideoContent(

View file

@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
@ -51,6 +52,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
is TimelineItemImageContent,
is TimelineItemFileContent,
is TimelineItemVideoContent,
is TimelineItemLocationContent,
TimelineItemRedactedContent,
TimelineItemUnknownContent -> false
is TimelineItemProfileChangeContent,

View file

@ -28,6 +28,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
aTimelineItemVideoContent(),
aTimelineItemFileContent("A file.pdf"),
aTimelineItemFileContent("A bigger file name which doesn't fit.pdf"),
aTimelineItemLocationContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.location.api.Location
data class TimelineItemLocationContent(
val body: String,
val location: Location,
val description: String? = null,
) : TimelineItemEventContent {
override val type: String = "TimelineItemLocationContent"
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
open class TimelineItemLocationContentProvider : PreviewParameterProvider<TimelineItemLocationContent> {
override val values: Sequence<TimelineItemLocationContent>
get() = sequenceOf(
aTimelineItemLocationContent(),
)
}
fun aTimelineItemLocationContent() = TimelineItemLocationContent(
body = "User location geo:52.2445,0.7186;u=5000",
location = Location(
lat = 52.2445,
lon = 0.7186,
accuracy = 5000f,
)
)

View file

@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
@ -40,8 +41,9 @@ class MessageSummaryFormatterImpl @Inject constructor(
override fun format(event: TimelineItem.Event): String {
return when (event.content) {
is TimelineItemTextBasedContent -> event.content.body
is TimelineItemStateContent -> event.content.body
is TimelineItemProfileChangeContent -> event.content.body
is TimelineItemStateContent -> event.content.body
is TimelineItemLocationContent -> event.content.body
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
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.LocationMessageType
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.NoticeMessageType
@ -116,6 +117,9 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
is ImageMessageType -> {
sp.getString(CommonStrings.common_image)
}
is LocationMessageType -> {
sp.getString(CommonStrings.common_shared_location)
}
is FileMessageType -> {
sp.getString(CommonStrings.common_file)
}

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
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.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
@ -161,6 +162,7 @@ class DefaultRoomLastMessageFormatterTests {
AudioMessageType(body, MediaSource("url"), null),
ImageMessageType(body, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2"),
NoticeMessageType(body, null),
EmoteMessageType(body, null),
)
@ -196,6 +198,7 @@ class DefaultRoomLastMessageFormatterTests {
is AudioMessageType -> "Audio"
is ImageMessageType -> "Image"
is FileMessageType -> "File"
is LocationMessageType -> "Shared location"
is EmoteMessageType -> "- $senderName ${type.body}"
is TextMessageType, is NoticeMessageType -> body
UnknownMessageType -> "Unsupported event"
@ -211,6 +214,7 @@ class DefaultRoomLastMessageFormatterTests {
is AudioMessageType -> "$senderName: Audio"
is ImageMessageType -> "$senderName: Image"
is FileMessageType -> "$senderName: File"
is LocationMessageType -> "$senderName: Shared location"
is EmoteMessageType -> "- $senderName ${type.body}"
is TextMessageType, is NoticeMessageType -> "$senderName: $body"
UnknownMessageType -> "$senderName: Unsupported event"
@ -220,6 +224,7 @@ class DefaultRoomLastMessageFormatterTests {
is AudioMessageType -> true
is ImageMessageType -> true
is FileMessageType -> true
is LocationMessageType -> false
is EmoteMessageType -> false
is TextMessageType, is NoticeMessageType -> true
UnknownMessageType -> true

View file

@ -125,6 +125,11 @@ data class ImageMessageType(
val info: ImageInfo?
) : MessageType
data class LocationMessageType(
val body: String,
val geoUri: String,
) : MessageType
data class AudioMessageType(
val body: String,
val source: MediaSource,

View file

@ -100,9 +100,9 @@ private fun MessageType.toContent(): String {
is MessageType.Emote -> content.body
is MessageType.File -> content.use { it.body }
is MessageType.Image -> content.use { it.body }
is MessageType.Location -> content.body
is MessageType.Notice -> content.body
is MessageType.Text -> content.body
is MessageType.Video -> content.use { it.body }
is MessageType.Location -> content.body
}
}

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
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.MessageFormat
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
@ -53,6 +54,9 @@ class EventMessageMapper {
is MessageType.Image -> {
ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
is MessageType.Location -> {
LocationMessageType(type.content.body, type.content.geoUri)
}
is MessageType.Notice -> {
NoticeMessageType(type.content.body, type.content.formatted?.map())
}
@ -65,7 +69,6 @@ class EventMessageMapper {
is MessageType.Video -> {
VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
is MessageType.Location,
null -> {
UnknownMessageType
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b9dd2f15bb1a4d1ff8a1614f0a4c29f61b9277dcd04e921b8c164e7f18946e2
size 76727

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7cf8ad6c8f8da98fdfc7a9d5c47e959aaa612dfc5192dd93b589c783d4e94fd6
size 155690

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a2d25abb7d0d15d53994b2b7af2aa08819b2b68e5e13931e53b4d7bbdc27f18
size 47543
oid sha256:d9e4fc3870b5dfb628f6bcdcb69ea1fccbb085b4a538bb7ba85d946d7d37ffd6
size 56908

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a2d25abb7d0d15d53994b2b7af2aa08819b2b68e5e13931e53b4d7bbdc27f18
size 47543

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1da5db0c72cee6075e73ec958efad2a4e7d84b4d45d0e93187135bbe2a50451f
size 44299
oid sha256:fcda324bbc41e69e5d88d35e09e527f3ad918f9c37131284e2da3b9daecb9c38
size 171506

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:081081bf7e41eccb9a8812c95100fcc0a2a4ead33c82ef4e314fe02fb7a0551d
size 55601
oid sha256:1da5db0c72cee6075e73ec958efad2a4e7d84b4d45d0e93187135bbe2a50451f
size 44299

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21d1b7469df1b1ba6f3c4b053702b045506fa5b510f16d07661bb8842a59047f
size 40750
oid sha256:081081bf7e41eccb9a8812c95100fcc0a2a4ead33c82ef4e314fe02fb7a0551d
size 55601

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9e4fc3870b5dfb628f6bcdcb69ea1fccbb085b4a538bb7ba85d946d7d37ffd6
size 56908
oid sha256:21d1b7469df1b1ba6f3c4b053702b045506fa5b510f16d07661bb8842a59047f
size 40750

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e88cdaaf2c98cd872c9645662b97f7157b3c862ab70616d29c4e8d53424612fc
size 49040
oid sha256:42f04d6a93187cc26c9c0fb5c9d60c418d0e01fc42a27eb14d13f6267a7aabd5
size 59076

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e88cdaaf2c98cd872c9645662b97f7157b3c862ab70616d29c4e8d53424612fc
size 49040

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd951c7ffcd7673b2224986e5fdf168543e763b7697e1389bbbeea071a7a3c83
size 45317
oid sha256:d9434e4e7cd01ea5c241a631800348e79c1538344ee1525aba3bdd5b3dd5f300
size 359637

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6db451aec7d5d3b19c3378ff15218aacb0d838714b2d196465d0cc9190b45ae1
size 57725
oid sha256:fd951c7ffcd7673b2224986e5fdf168543e763b7697e1389bbbeea071a7a3c83
size 45317

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f547ad07db87cbc08c31922e1f21fdbe6fdbdea8667159c8e100a440a2c05c7f
size 41624
oid sha256:6db451aec7d5d3b19c3378ff15218aacb0d838714b2d196465d0cc9190b45ae1
size 57725

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42f04d6a93187cc26c9c0fb5c9d60c418d0e01fc42a27eb14d13f6267a7aabd5
size 59076
oid sha256:f547ad07db87cbc08c31922e1f21fdbe6fdbdea8667159c8e100a440a2c05c7f
size 41624