Show location replies per the designs

This commit is contained in:
Chris Smith 2023-06-29 16:15:15 +01:00
parent 6c82c02dc8
commit bd0e24598a
14 changed files with 167 additions and 16 deletions

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(libs.coil.compose) implementation(libs.coil.compose)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImagePainter import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
@ -43,6 +44,7 @@ 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
import io.element.android.libraries.matrix.ui.R as MatrixUIR
/** /**
* Shows a static map image downloaded via a third party service's static maps API. * Shows a static map image downloaded via a third party service's static maps API.
@ -103,9 +105,15 @@ fun StaticMapView(
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
Icon( Icon(
resourceId = R.drawable.pin, resourceId = MatrixUIR.drawable.pin,
contentDescription = null, contentDescription = null,
tint = Color.Unspecified tint = Color.Unspecified,
modifier = Modifier.align { size, space, _ ->
IntOffset(
x = (space.width - size.width) / 2,
y = (space.height / 2) - size.height,
)
}
) )
} else { } else {
StaticMapPlaceholder( StaticMapPlaceholder(

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.matrixui)
implementation(libs.maplibre) implementation(libs.maplibre)
implementation(libs.maplibre.annotation) implementation(libs.maplibre.annotation)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)

View file

@ -39,9 +39,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.features.location.api.R
import io.element.android.features.location.impl.map.MapView import io.element.android.features.location.impl.map.MapView
import io.element.android.features.location.impl.map.rememberMapState import io.element.android.features.location.impl.map.rememberMapState
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@ -52,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.matrix.ui.R as MatrixUIR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
@ -115,9 +116,15 @@ fun SendLocationView(
mapState = mapState, mapState = mapState,
) )
Icon( Icon(
resourceId = R.drawable.pin, resourceId = MatrixUIR.drawable.pin,
contentDescription = null, contentDescription = null,
tint = Color.Unspecified tint = Color.Unspecified,
modifier = Modifier.align { size, space, _ ->
IntOffset(
x = (space.width - size.width) / 2,
y = (space.height / 2) - size.height,
)
}
) )
} }
} }

View file

@ -44,7 +44,6 @@ import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
import io.element.android.features.location.api.R
import io.element.android.features.location.api.internal.buildTileServerUrl import io.element.android.features.location.api.internal.buildTileServerUrl
import io.element.android.features.location.impl.location.Location import io.element.android.features.location.impl.location.Location
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -55,6 +54,7 @@ import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import io.element.android.libraries.matrix.ui.R as MatrixUIR
/** /**
* Composable wrapper around MapLibre's [MapView]. * Composable wrapper around MapLibre's [MapView].
@ -149,12 +149,13 @@ fun MapView(
LaunchedEffect(mapRefs, mapState.location) { LaunchedEffect(mapRefs, mapState.location) {
mapRefs?.let { mapRefs -> mapRefs?.let { mapRefs ->
mapState.location?.let { location -> mapState.location?.let { location ->
context.getDrawable(R.drawable.pin)?.let { mapRefs.style.addImage("pin", it) } context.getDrawable(MatrixUIR.drawable.pin)?.let { mapRefs.style.addImage("pin", it) }
mapRefs.symbolManager.create( mapRefs.symbolManager.create(
SymbolOptions() SymbolOptions()
.withLatLng(LatLng(location.lat, location.lon)) .withLatLng(LatLng(location.lat, location.lon))
.withIconImage("pin") .withIconImage("pin")
.withIconSize(1.3f) .withIconSize(1.3f)
.withIconOffset(arrayOf(0f, 0.5f))
) )
Timber.d("Shown pin at location: $location") Timber.d("Shown pin at location: $location")
} }
@ -275,7 +276,7 @@ private fun ContentToPreview() {
), ),
markers = listOf( markers = listOf(
MapState.Marker( MapState.Marker(
drawable = R.drawable.pin, drawable = MatrixUIR.drawable.pin,
lat = 0.0, lat = 0.0,
lon = 0.0, lon = 0.0,
) )

View file

@ -264,11 +264,16 @@ class MessagesPresenter @AssistedInject constructor(
type = AttachmentThumbnailType.File, type = AttachmentThumbnailType.File,
blurHash = null, blurHash = null,
) )
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
mediaSource = null,
textContent = null,
type = AttachmentThumbnailType.Location,
blurHash = null,
)
is TimelineItemTextBasedContent, is TimelineItemTextBasedContent,
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(

View file

@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
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.aTimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -54,6 +55,12 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemLocationContent()),
actions = aTimelineItemActionList(),
)
),
) )
} }

View file

@ -78,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -232,8 +233,21 @@ 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 TimelineItemLocationContent -> {
icon = {
AttachmentThumbnail(
modifier = imageModifier,
info = AttachmentThumbnailInfo(
type = AttachmentThumbnailType.Location,
textContent = stringResource(CommonStrings.common_shared_location),
mediaSource = null,
blurHash = null,
)
)
}
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
}
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
icon = { icon = {
AttachmentThumbnail( AttachmentThumbnail(

View file

@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -54,8 +55,8 @@ import io.element.android.features.messages.impl.timeline.components.event.toExt
import io.element.android.features.messages.impl.timeline.model.TimelineItem 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.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
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
@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.api.core.UserId
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.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.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
@ -77,6 +79,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import org.jsoup.Jsoup import org.jsoup.Jsoup
@Composable @Composable
@ -300,9 +303,10 @@ private fun MessageEventBubbleContent(
if (inReplyToDetails != null) { if (inReplyToDetails != null) {
val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
val text = textForInReplyTo(inReplyToDetails)
ReplyToContent( ReplyToContent(
senderName = senderName, senderName = senderName,
text = inReplyToDetails.content.body, text = text,
attachmentThumbnailInfo = attachmentThumbnailInfo, attachmentThumbnailInfo = attachmentThumbnailInfo,
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, start = 8.dp, end = 8.dp) .padding(top = 8.dp, start = 8.dp, end = 8.dp)
@ -409,9 +413,22 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
type = AttachmentThumbnailType.File, type = AttachmentThumbnailType.File,
blurHash = null, blurHash = null,
) )
is LocationMessageType -> AttachmentThumbnailInfo(
mediaSource = null,
textContent = inReplyTo.content.body,
type = AttachmentThumbnailType.Location,
blurHash = null,
)
else -> null else -> null
} }
@Composable
private fun textForInReplyTo(inReplyTo: InReplyTo.Ready) =
when (inReplyTo.content.type) {
is LocationMessageType -> stringResource(CommonStrings.common_shared_location)
else -> inReplyTo.content.body
}
@Preview @Preview
@Composable @Composable
internal fun TimelineItemEventRowLightPreview() = internal fun TimelineItemEventRowLightPreview() =

View file

@ -43,7 +43,7 @@ class MessageSummaryFormatterImpl @Inject constructor(
is TimelineItemTextBasedContent -> event.content.body is TimelineItemTextBasedContent -> event.content.body
is TimelineItemProfileChangeContent -> event.content.body is TimelineItemProfileChangeContent -> event.content.body
is TimelineItemStateContent -> event.content.body is TimelineItemStateContent -> event.content.body
is TimelineItemLocationContent -> event.content.body is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
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)

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.ui.components
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Attachment import androidx.compose.material.icons.outlined.Attachment
import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.material.icons.outlined.VideoCameraBack
@ -73,6 +74,11 @@ fun AttachmentThumbnail(
modifier = Modifier.rotate(-45f) modifier = Modifier.rotate(-45f)
) )
} }
AttachmentThumbnailType.Location -> {
PinIcon(
modifier = Modifier.fillMaxSize()
)
}
else -> Unit else -> Unit
} }
} }
@ -81,7 +87,7 @@ fun AttachmentThumbnail(
@Parcelize @Parcelize
enum class AttachmentThumbnailType: Parcelable { enum class AttachmentThumbnailType: Parcelable {
Image, Video, File Image, Video, File, Location
} }
@Parcelize @Parcelize

View file

@ -0,0 +1,66 @@
/*
* 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.libraries.matrix.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.matrix.ui.R
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PinIcon(
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.background(ElementTheme.colors.bgSubtlePrimary)
) {
Icon(
modifier = Modifier
.align(Alignment.Center)
.width(22.dp),
resourceId = R.drawable.pin,
contentDescription = null,
tint = Color.Unspecified,
)
}
}
@Preview
@Composable
fun PinIconLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
fun PinIconDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
PinIcon()
}

View file

@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp" android:width="50dp"
android:height="108dp" android:height="54dp"
android:viewportWidth="50" android:viewportWidth="50"
android:viewportHeight="108"> android:viewportHeight="54">
<group> <group>
<clip-path <clip-path
android:pathData="M0,0h50v108h-50z"/> android:pathData="M0,0h50v108h-50z"/>

View file

@ -552,5 +552,23 @@ private fun ReplyContentToPreview() {
composerCanSendMessage = true, composerCanSendMessage = true,
composerText = "A message", composerText = "A message",
) )
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
composerMode = MessageComposerMode.Reply(
senderName = "Alice",
eventId = EventId("$1234"),
attachmentThumbnailInfo = AttachmentThumbnailInfo(
mediaSource = null,
textContent = null,
type = AttachmentThumbnailType.Location,
blurHash = null,
),
defaultContent = "Shared location"
),
onResetComposerMode = {},
composerCanSendMessage = true,
composerText = "A message",
)
} }
} }