Show location events in timeline
Not in scope: interacting with the timeline items, reply formatting. These will be implemented separately. Closes #689
This commit is contained in:
parent
6174a36d66
commit
fe322d072e
23 changed files with 368 additions and 20 deletions
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,28 @@
|
||||||
|
/*
|
||||||
|
* 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+)?))?"""
|
||||||
|
|
||||||
|
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.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.ImageRequest
|
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.StaticMapPlaceholder
|
||||||
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
|
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -73,9 +75,13 @@ fun StaticMapView(
|
||||||
lat = lat,
|
lat = lat,
|
||||||
lon = lon,
|
lon = lon,
|
||||||
desiredZoom = zoom,
|
desiredZoom = zoom,
|
||||||
desiredWidth = constraints.maxWidth,
|
|
||||||
desiredHeight = constraints.maxHeight,
|
|
||||||
darkMode = darkMode,
|
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)
|
.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 LIGHT_MAP_ID = "9bc819c8-e627-474a-a348-ec144fe3d810"
|
||||||
private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3"
|
private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3"
|
||||||
private const val STATIC_MAP_FORMAT = "webp"
|
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_WIDTH_HEIGHT = 2048
|
||||||
private const val STATIC_MAP_MAX_ZOOM = 22.0
|
private const val STATIC_MAP_MAX_ZOOM = 22.0
|
||||||
|
|
||||||
|
|
@ -35,6 +35,14 @@ internal fun buildTileServerUrl(
|
||||||
"$BASE_URL/maps/$DARK_MAP_ID/style.json?key=$API_KEY"
|
"$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.
|
* Builds a valid URL for maptiler.com static map api based on the given params.
|
||||||
*
|
*
|
||||||
|
|
@ -50,7 +58,9 @@ internal fun buildStaticMapsApiUrl(
|
||||||
desiredZoom: Double,
|
desiredZoom: Double,
|
||||||
desiredWidth: Int,
|
desiredWidth: Int,
|
||||||
desiredHeight: Int,
|
desiredHeight: Int,
|
||||||
darkMode: Boolean
|
darkMode: Boolean,
|
||||||
|
doubleScale: Boolean,
|
||||||
|
attributionPlacement: AttributionPlacement,
|
||||||
): String {
|
): String {
|
||||||
require(desiredWidth > 0 && desiredHeight > 0) {
|
require(desiredWidth > 0 && desiredHeight > 0) {
|
||||||
"Width ($desiredHeight) and height ($desiredHeight) must be > 0"
|
"Width ($desiredHeight) and height ($desiredHeight) must be > 0"
|
||||||
|
|
@ -72,9 +82,10 @@ internal fun buildStaticMapsApiUrl(
|
||||||
width = (height * aspectRatio).roundToInt()
|
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"
|
val mapId = if (darkMode) DARK_MAP_ID else LIGHT_MAP_ID
|
||||||
} else {
|
val scaleSuffix = if (doubleScale) STATIC_MAP_SCALE_2X else ""
|
||||||
"$BASE_URL/maps/$DARK_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY"
|
|
||||||
}
|
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
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class BuildStaticMapsApiUrlTest {
|
class BuildStaticMapsApiUrlTest {
|
||||||
|
|
@ -30,10 +29,13 @@ class BuildStaticMapsApiUrlTest {
|
||||||
desiredZoom = 1.2,
|
desiredZoom = 1.2,
|
||||||
desiredWidth = 100,
|
desiredWidth = 100,
|
||||||
desiredHeight = 200,
|
desiredHeight = 200,
|
||||||
darkMode = false
|
darkMode = false,
|
||||||
|
doubleScale = false,
|
||||||
|
attributionPlacement = AttributionPlacement.BottomLeft,
|
||||||
)
|
)
|
||||||
).isEqualTo(
|
).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,
|
desiredZoom = 1.2,
|
||||||
desiredWidth = 100,
|
desiredWidth = 100,
|
||||||
desiredHeight = 200,
|
desiredHeight = 200,
|
||||||
darkMode = true
|
darkMode = true,
|
||||||
|
doubleScale = false,
|
||||||
|
attributionPlacement = AttributionPlacement.BottomLeft,
|
||||||
)
|
)
|
||||||
).isEqualTo(
|
).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,
|
desiredZoom = 100.0,
|
||||||
desiredWidth = 8192,
|
desiredWidth = 8192,
|
||||||
desiredHeight = 4096,
|
desiredHeight = 4096,
|
||||||
darkMode = false
|
darkMode = false,
|
||||||
|
doubleScale = false,
|
||||||
|
attributionPlacement = AttributionPlacement.BottomLeft,
|
||||||
)
|
)
|
||||||
).isEqualTo(
|
).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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.messages.api)
|
api(projects.features.messages.api)
|
||||||
|
implementation(projects.features.location.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -267,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemEncryptedContent,
|
is TimelineItemEncryptedContent,
|
||||||
|
is TimelineItemLocationContent,
|
||||||
is TimelineItemUnknownContent -> null
|
is TimelineItemUnknownContent -> null
|
||||||
}
|
}
|
||||||
val composerMode = MessageComposerMode.Reply(
|
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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -232,6 +233,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemEncryptedContent,
|
is TimelineItemEncryptedContent,
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
|
is TimelineItemLocationContent,
|
||||||
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
icon = {
|
icon = {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,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.bubble.BubbleState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
|
|
@ -224,7 +225,9 @@ private fun MessageEventBubbleContent(
|
||||||
onTimestampClicked: () -> Unit,
|
onTimestampClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
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
|
val replyToDetails = event.inReplyTo as? InReplyTo.Ready
|
||||||
|
|
||||||
// Long clicks are not not automatically propagated from a `clickable`
|
// 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.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -62,6 +63,10 @@ fun TimelineItemEventContentView(
|
||||||
extraPadding = extraPadding,
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
is TimelineItemLocationContent -> TimelineItemLocationView(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
is TimelineItemImageContent -> TimelineItemImageView(
|
is TimelineItemImageContent -> TimelineItemImageView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier,
|
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
|
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.TimelineItemEmoteContent
|
||||||
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.TimelineItemFileContent
|
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.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.TimelineItemNoticeContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
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.EmoteMessageType
|
||||||
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.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
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
|
|
@ -64,6 +67,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
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 -> {
|
is VideoMessageType -> {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemVideoContent(
|
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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
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.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.TimelineItemProfileChangeContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||||
|
|
@ -51,6 +52,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||||
is TimelineItemImageContent,
|
is TimelineItemImageContent,
|
||||||
is TimelineItemFileContent,
|
is TimelineItemFileContent,
|
||||||
is TimelineItemVideoContent,
|
is TimelineItemVideoContent,
|
||||||
|
is TimelineItemLocationContent,
|
||||||
TimelineItemRedactedContent,
|
TimelineItemRedactedContent,
|
||||||
TimelineItemUnknownContent -> false
|
TimelineItemUnknownContent -> false
|
||||||
is TimelineItemProfileChangeContent,
|
is TimelineItemProfileChangeContent,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
||||||
aTimelineItemVideoContent(),
|
aTimelineItemVideoContent(),
|
||||||
aTimelineItemFileContent("A file.pdf"),
|
aTimelineItemFileContent("A file.pdf"),
|
||||||
aTimelineItemFileContent("A bigger file name which doesn't fit.pdf"),
|
aTimelineItemFileContent("A bigger file name which doesn't fit.pdf"),
|
||||||
|
aTimelineItemLocationContent(),
|
||||||
aTimelineItemNoticeContent(),
|
aTimelineItemNoticeContent(),
|
||||||
aTimelineItemRedactedContent(),
|
aTimelineItemRedactedContent(),
|
||||||
aTimelineItemTextContent(),
|
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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
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.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.TimelineItemProfileChangeContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
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.TimelineItemStateContent
|
||||||
|
|
@ -40,8 +41,9 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
||||||
override fun format(event: TimelineItem.Event): String {
|
override fun format(event: TimelineItem.Event): String {
|
||||||
return when (event.content) {
|
return when (event.content) {
|
||||||
is TimelineItemTextBasedContent -> event.content.body
|
is TimelineItemTextBasedContent -> event.content.body
|
||||||
is TimelineItemStateContent -> event.content.body
|
|
||||||
is TimelineItemProfileChangeContent -> 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 TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
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.FailedToParseStateContent
|
||||||
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.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
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
|
|
@ -116,6 +117,9 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
is ImageMessageType -> {
|
is ImageMessageType -> {
|
||||||
sp.getString(CommonStrings.common_image)
|
sp.getString(CommonStrings.common_image)
|
||||||
}
|
}
|
||||||
|
is LocationMessageType -> {
|
||||||
|
sp.getString(CommonStrings.common_shared_location)
|
||||||
|
}
|
||||||
is FileMessageType -> {
|
is FileMessageType -> {
|
||||||
sp.getString(CommonStrings.common_file)
|
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.FailedToParseStateContent
|
||||||
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.LocationMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
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.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
|
|
@ -161,6 +162,7 @@ class DefaultRoomLastMessageFormatterTests {
|
||||||
AudioMessageType(body, MediaSource("url"), null),
|
AudioMessageType(body, MediaSource("url"), null),
|
||||||
ImageMessageType(body, MediaSource("url"), null),
|
ImageMessageType(body, MediaSource("url"), null),
|
||||||
FileMessageType(body, MediaSource("url"), null),
|
FileMessageType(body, MediaSource("url"), null),
|
||||||
|
LocationMessageType(body, "geo:1,2"),
|
||||||
NoticeMessageType(body, null),
|
NoticeMessageType(body, null),
|
||||||
EmoteMessageType(body, null),
|
EmoteMessageType(body, null),
|
||||||
)
|
)
|
||||||
|
|
@ -196,6 +198,7 @@ class DefaultRoomLastMessageFormatterTests {
|
||||||
is AudioMessageType -> "Audio"
|
is AudioMessageType -> "Audio"
|
||||||
is ImageMessageType -> "Image"
|
is ImageMessageType -> "Image"
|
||||||
is FileMessageType -> "File"
|
is FileMessageType -> "File"
|
||||||
|
is LocationMessageType -> "Shared location"
|
||||||
is EmoteMessageType -> "- $senderName ${type.body}"
|
is EmoteMessageType -> "- $senderName ${type.body}"
|
||||||
is TextMessageType, is NoticeMessageType -> body
|
is TextMessageType, is NoticeMessageType -> body
|
||||||
UnknownMessageType -> "Unsupported event"
|
UnknownMessageType -> "Unsupported event"
|
||||||
|
|
@ -211,6 +214,7 @@ class DefaultRoomLastMessageFormatterTests {
|
||||||
is AudioMessageType -> "$senderName: Audio"
|
is AudioMessageType -> "$senderName: Audio"
|
||||||
is ImageMessageType -> "$senderName: Image"
|
is ImageMessageType -> "$senderName: Image"
|
||||||
is FileMessageType -> "$senderName: File"
|
is FileMessageType -> "$senderName: File"
|
||||||
|
is LocationMessageType -> "$senderName: Shared location"
|
||||||
is EmoteMessageType -> "- $senderName ${type.body}"
|
is EmoteMessageType -> "- $senderName ${type.body}"
|
||||||
is TextMessageType, is NoticeMessageType -> "$senderName: $body"
|
is TextMessageType, is NoticeMessageType -> "$senderName: $body"
|
||||||
UnknownMessageType -> "$senderName: Unsupported event"
|
UnknownMessageType -> "$senderName: Unsupported event"
|
||||||
|
|
@ -220,6 +224,7 @@ class DefaultRoomLastMessageFormatterTests {
|
||||||
is AudioMessageType -> true
|
is AudioMessageType -> true
|
||||||
is ImageMessageType -> true
|
is ImageMessageType -> true
|
||||||
is FileMessageType -> true
|
is FileMessageType -> true
|
||||||
|
is LocationMessageType -> false
|
||||||
is EmoteMessageType -> false
|
is EmoteMessageType -> false
|
||||||
is TextMessageType, is NoticeMessageType -> true
|
is TextMessageType, is NoticeMessageType -> true
|
||||||
UnknownMessageType -> true
|
UnknownMessageType -> true
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ data class ImageMessageType(
|
||||||
val info: ImageInfo?
|
val info: ImageInfo?
|
||||||
) : MessageType
|
) : MessageType
|
||||||
|
|
||||||
|
data class LocationMessageType(
|
||||||
|
val body: String,
|
||||||
|
val geoUri: String,
|
||||||
|
) : MessageType
|
||||||
|
|
||||||
data class AudioMessageType(
|
data class AudioMessageType(
|
||||||
val body: String,
|
val body: String,
|
||||||
val source: MediaSource,
|
val source: MediaSource,
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,9 @@ private fun MessageType.toContent(): String {
|
||||||
is MessageType.Emote -> content.body
|
is MessageType.Emote -> content.body
|
||||||
is MessageType.File -> content.use { it.body }
|
is MessageType.File -> content.use { it.body }
|
||||||
is MessageType.Image -> content.use { it.body }
|
is MessageType.Image -> content.use { it.body }
|
||||||
|
is MessageType.Location -> content.body
|
||||||
is MessageType.Notice -> content.body
|
is MessageType.Notice -> content.body
|
||||||
is MessageType.Text -> content.body
|
is MessageType.Text -> content.body
|
||||||
is MessageType.Video -> content.use { it.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.FormattedBody
|
||||||
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.InReplyTo
|
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.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
|
|
@ -53,6 +54,9 @@ class EventMessageMapper {
|
||||||
is MessageType.Image -> {
|
is MessageType.Image -> {
|
||||||
ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
|
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 -> {
|
is MessageType.Notice -> {
|
||||||
NoticeMessageType(type.content.body, type.content.formatted?.map())
|
NoticeMessageType(type.content.body, type.content.formatted?.map())
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +69,6 @@ class EventMessageMapper {
|
||||||
is MessageType.Video -> {
|
is MessageType.Video -> {
|
||||||
VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
|
VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
|
||||||
}
|
}
|
||||||
is MessageType.Location,
|
|
||||||
null -> {
|
null -> {
|
||||||
UnknownMessageType
|
UnknownMessageType
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue