Merge pull request #726 from vector-im/feature/cjs/view-location-in-timeline
Show location events in the timeline
This commit is contained in:
1
changelog.d/689.feature
Normal file
1
changelog.d/689.feature
Normal file
@@ -0,0 +1 @@
|
||||
Show location events in the timeline
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user