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:
Chris Smith 2023-06-08 16:22:27 +01:00
parent 6174a36d66
commit fe322d072e
23 changed files with 368 additions and 20 deletions

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
@ -232,6 +233,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

@ -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.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)