Merge pull request #773 from vector-im/feature/cjs/location-viewing
This commit is contained in:
commit
dde879fa2a
38 changed files with 739 additions and 63 deletions
|
|
@ -17,6 +17,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,25 @@
|
||||||
|
|
||||||
package io.element.android.features.location.api
|
package io.element.android.features.location.api
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
private const val GEO_URI_REGEX = """geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?"""
|
private const val GEO_URI_REGEX = """geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?"""
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class Location(
|
data class Location(
|
||||||
val lat: Double,
|
val lat: Double,
|
||||||
val lon: Double,
|
val lon: Double,
|
||||||
val accuracy: Float,
|
val accuracy: Float,
|
||||||
)
|
) : Parcelable {
|
||||||
|
companion object {
|
||||||
fun parseGeoUri(geoUri: String): Location? {
|
fun fromGeoUri(geoUri: String): Location? {
|
||||||
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
|
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
|
||||||
return Location (
|
return Location(
|
||||||
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
|
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||||
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
|
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||||
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
|
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
||||||
|
interface ShowLocationEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
|
data class Inputs(val location: Location, val description: String?) : NodeInputs
|
||||||
|
|
||||||
|
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
|
||||||
|
}
|
||||||
|
|
@ -19,57 +19,57 @@ package io.element.android.features.location.api
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
internal class GeoUrisKtTest {
|
internal class LocationKtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parseGeoUri - returns null for invalid urls`() {
|
fun `parseGeoUri - returns null for invalid urls`() {
|
||||||
assertThat(parseGeoUri("")).isNull()
|
assertThat(Location.fromGeoUri("")).isNull()
|
||||||
assertThat(parseGeoUri("http://example.com/")).isNull()
|
assertThat(Location.fromGeoUri("http://example.com/")).isNull()
|
||||||
assertThat(parseGeoUri("geo:")).isNull()
|
assertThat(Location.fromGeoUri("geo:")).isNull()
|
||||||
assertThat(parseGeoUri("geo:1.234")).isNull()
|
assertThat(Location.fromGeoUri("geo:1.234")).isNull()
|
||||||
assertThat(parseGeoUri("geo:1.234,")).isNull()
|
assertThat(Location.fromGeoUri("geo:1.234,")).isNull()
|
||||||
assertThat(parseGeoUri("geo:,1.234")).isNull()
|
assertThat(Location.fromGeoUri("geo:,1.234")).isNull()
|
||||||
assertThat(parseGeoUri("notgeo:1.234,5.678")).isNull()
|
assertThat(Location.fromGeoUri("notgeo:1.234,5.678")).isNull()
|
||||||
assertThat(parseGeoUri("geo:+1.234,5.678")).isNull()
|
assertThat(Location.fromGeoUri("geo:+1.234,5.678")).isNull()
|
||||||
assertThat(parseGeoUri("geo:+1.234,*5.678")).isNull()
|
assertThat(Location.fromGeoUri("geo:+1.234,*5.678")).isNull()
|
||||||
assertThat(parseGeoUri("geo:not,good")).isNull()
|
assertThat(Location.fromGeoUri("geo:not,good")).isNull()
|
||||||
assertThat(parseGeoUri("geo:1.234,5.678;u=wrong")).isNull()
|
assertThat(Location.fromGeoUri("geo:1.234,5.678;u=wrong")).isNull()
|
||||||
assertThat(parseGeoUri("geo:1.234,5.678trailing")).isNull()
|
assertThat(Location.fromGeoUri("geo:1.234,5.678trailing")).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parseGeoUri - returns location for valid urls`() {
|
fun `parseGeoUri - returns location for valid urls`() {
|
||||||
assertThat(parseGeoUri("geo:1.234,5.678")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:1.234,5.678")).isEqualTo(Location(
|
||||||
lat = 1.234,
|
lat = 1.234,
|
||||||
lon = 5.678,
|
lon = 5.678,
|
||||||
accuracy = 0f,
|
accuracy = 0f,
|
||||||
))
|
))
|
||||||
|
|
||||||
assertThat(parseGeoUri("geo:1,5")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:1,5")).isEqualTo(Location(
|
||||||
lat = 1.0,
|
lat = 1.0,
|
||||||
lon = 5.0,
|
lon = 5.0,
|
||||||
accuracy = 0f,
|
accuracy = 0f,
|
||||||
))
|
))
|
||||||
|
|
||||||
assertThat(parseGeoUri("geo:1.234,5.678;u=3000")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:1.234,5.678;u=3000")).isEqualTo(Location(
|
||||||
lat = 1.234,
|
lat = 1.234,
|
||||||
lon = 5.678,
|
lon = 5.678,
|
||||||
accuracy = 3000f,
|
accuracy = 3000f,
|
||||||
))
|
))
|
||||||
|
|
||||||
assertThat(parseGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
|
||||||
lat = 1.0,
|
lat = 1.0,
|
||||||
lon = 5.0,
|
lon = 5.0,
|
||||||
accuracy = 3000f,
|
accuracy = 3000f,
|
||||||
))
|
))
|
||||||
|
|
||||||
assertThat(parseGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
|
||||||
lat = -1.234,
|
lat = -1.234,
|
||||||
lon = -5.678,
|
lon = -5.678,
|
||||||
accuracy = 9.10f,
|
accuracy = 9.10f,
|
||||||
))
|
))
|
||||||
|
|
||||||
assertThat(parseGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
|
assertThat(Location.fromGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
|
||||||
lat = -1.0,
|
lat = -1.0,
|
||||||
lon = -5.0,
|
lon = -5.0,
|
||||||
accuracy = 9.10f,
|
accuracy = 9.10f,
|
||||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.maplibre)
|
implementation(libs.maplibre)
|
||||||
implementation(libs.maplibre.annotation)
|
implementation(libs.maplibre.annotation)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.core.content.getSystemService
|
||||||
import androidx.core.location.LocationListenerCompat
|
import androidx.core.location.LocationListenerCompat
|
||||||
import androidx.core.location.LocationManagerCompat
|
import androidx.core.location.LocationManagerCompat
|
||||||
import androidx.core.location.LocationRequestCompat
|
import androidx.core.location.LocationRequestCompat
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.location.impl.map
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -29,7 +30,9 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -44,10 +47,12 @@ 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 com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
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.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.theme.components.Text
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -69,7 +74,11 @@ fun MapView(
|
||||||
// When in preview, early return a Box with the received modifier preserving layout
|
// When in preview, early return a Box with the received modifier preserving layout
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
@Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return.
|
@Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return.
|
||||||
Box(modifier = modifier)
|
Box(
|
||||||
|
modifier = modifier.background(Color.DarkGray)
|
||||||
|
) {
|
||||||
|
Text("[MapView]", modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +164,7 @@ fun MapView(
|
||||||
.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))
|
.withIconAnchor(ICON_ANCHOR_BOTTOM)
|
||||||
)
|
)
|
||||||
Timber.d("Shown pin at location: $location")
|
Timber.d("Shown pin at location: $location")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class AndroidLocationActions @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context
|
||||||
|
) : LocationActions {
|
||||||
|
|
||||||
|
private var activityContext: Context? = null
|
||||||
|
|
||||||
|
override fun share(location: Location, label: String?) {
|
||||||
|
runCatching {
|
||||||
|
val uri = Uri.parse(buildUrl(location, label))
|
||||||
|
val showMapsIntent = Intent(Intent.ACTION_VIEW).setData(uri)
|
||||||
|
val chooserIntent = Intent.createChooser(showMapsIntent, null)
|
||||||
|
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
appContext.startActivity(chooserIntent)
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.v("Open location succeed")
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it, "Open location failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun buildUrl(
|
||||||
|
location: Location,
|
||||||
|
label: String?,
|
||||||
|
urlEncoder: (String) -> String = Uri::encode
|
||||||
|
): String {
|
||||||
|
// Ref: https://developer.android.com/guide/components/intents-common#ViewMap
|
||||||
|
val base = "geo:0,0?q=%.6f,%.6f".format(location.lat, location.lon)
|
||||||
|
return if (label == null) {
|
||||||
|
base
|
||||||
|
} else {
|
||||||
|
"%s (%s)".format(base, urlEncoder(label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,13 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.impl.location
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
/**
|
import io.element.android.features.location.api.Location
|
||||||
* Represents a location sample emitted by the device's location subsystem.
|
|
||||||
*/
|
interface LocationActions {
|
||||||
data class Location(
|
fun share(location: Location, label: String?)
|
||||||
val lat: Double,
|
}
|
||||||
val lon: Double,
|
|
||||||
val accuracy: Float,
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class ShowLocationEntryPointImpl @Inject constructor() : ShowLocationEntryPoint {
|
||||||
|
override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: ShowLocationEntryPoint.Inputs): Node {
|
||||||
|
return parentNode.createNode<ShowLocationNode>(buildContext, listOf(inputs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
sealed interface ShowLocationEvents {
|
||||||
|
object Share : ShowLocationEvents
|
||||||
|
}
|
||||||
|
|
@ -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.location.impl.show
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
|
@ContributesNode(RoomScope::class)
|
||||||
|
class ShowLocationNode @AssistedInject constructor(
|
||||||
|
presenterFactory: ShowLocationPresenter.Factory,
|
||||||
|
analyticsService: AnalyticsService,
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycle.subscribe(
|
||||||
|
onResume = {
|
||||||
|
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.LocationView))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val inputs: ShowLocationEntryPoint.Inputs = inputs()
|
||||||
|
private val presenter = presenterFactory.create(inputs.location, inputs.description)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
ShowLocationView(
|
||||||
|
state = presenter.present(),
|
||||||
|
modifier = modifier,
|
||||||
|
onBackPressed = ::navigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
|
class ShowLocationPresenter @AssistedInject constructor(
|
||||||
|
private val actions: LocationActions,
|
||||||
|
@Assisted private val location: Location,
|
||||||
|
@Assisted private val description: String?
|
||||||
|
) : Presenter<ShowLocationState> {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(location: Location, description: String?): ShowLocationPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ShowLocationState {
|
||||||
|
return ShowLocationState(
|
||||||
|
location = location,
|
||||||
|
description = description
|
||||||
|
) {
|
||||||
|
when (it) {
|
||||||
|
ShowLocationEvents.Share -> actions.share(location, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
|
data class ShowLocationState(
|
||||||
|
val location: Location,
|
||||||
|
val description: String?,
|
||||||
|
val eventSink: (ShowLocationEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
|
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
|
override val values: Sequence<ShowLocationState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = null,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = "My favourite place!",
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = "For some reason I decided to write a small essay in the location description. " +
|
||||||
|
"It is so long that it will wrap onto more than two lines!",
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
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.impl.map.MapState
|
||||||
|
import io.element.android.features.location.impl.map.MapView
|
||||||
|
import io.element.android.features.location.impl.map.rememberMapState
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.theme.compound.generated.TypographyTokens
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ShowLocationView(
|
||||||
|
state: ShowLocationState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val mapState = rememberMapState(
|
||||||
|
location = state.location,
|
||||||
|
position = MapState.CameraPosition(state.location.lat, state.location.lon, 15.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
Scaffold(modifier,
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.screen_view_location_title),
|
||||||
|
style = TypographyTokens.fontBodyLgMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackPressed)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) {
|
||||||
|
Icon(imageVector = Icons.Outlined.Share, contentDescription = stringResource(CommonStrings.action_share))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.consumeWindowInsets(paddingValues)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
state.description?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = TypographyTokens.fontBodyMdRegular,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MapView(
|
||||||
|
mapState = mapState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun ShowLocationViewLightPreview(@PreviewParameter(ShowLocationStateProvider::class) state: ShowLocationState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun ShowLocationViewDarkPreview(@PreviewParameter(ShowLocationStateProvider::class) state: ShowLocationState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: ShowLocationState) {
|
||||||
|
ShowLocationView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,18 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.features.location.impl.location
|
package io.element.android.features.location.impl.location
|
||||||
|
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
fun fakeLocationUpdatesFlow(): Flow<io.element.android.features.location.impl.location.Location> = flow {
|
fun fakeLocationUpdatesFlow(): Flow<Location> = flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
emit(aLocation())
|
emit(aLocation())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aLocation() = io.element.android.features.location.impl.location.Location(
|
private fun aLocation() = Location(
|
||||||
lat = 51.49404,
|
lat = 51.49404,
|
||||||
lon = -0.25484,
|
lon = -0.25484,
|
||||||
accuracy = 5f
|
accuracy = 5f
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
import org.junit.Test
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
internal class AndroidLocationActionsTest {
|
||||||
|
|
||||||
|
// We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests
|
||||||
|
private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildUrl - truncates excessive decimals to 6dp`() {
|
||||||
|
val location = Location(
|
||||||
|
lat = 1.234567890123,
|
||||||
|
lon = 123.456789012345,
|
||||||
|
accuracy = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
val actual = buildUrl(location, null, ::urlEncoder)
|
||||||
|
val expected = "geo:0,0?q=1.234568,123.456789"
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildUrl - appends label if set`() {
|
||||||
|
val location = Location(
|
||||||
|
lat = 1.000001,
|
||||||
|
lon = 2.000001,
|
||||||
|
accuracy = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
val actual = buildUrl(location, "point", ::urlEncoder)
|
||||||
|
val expected = "geo:0,0?q=1.000001,2.000001 (point)"
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildUrl - URL encodes label`() {
|
||||||
|
val location = Location(
|
||||||
|
lat = 1.000001,
|
||||||
|
lon = 2.000001,
|
||||||
|
accuracy = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
val actual = buildUrl(location, "(weird/stuff here)", ::urlEncoder)
|
||||||
|
val expected = "geo:0,0?q=1.000001,2.000001 (%28weird%2Fstuff+here%29)"
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
|
class FakeLocationActions : LocationActions {
|
||||||
|
|
||||||
|
var sharedLocation: Location? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
var sharedLabel: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun share(location: Location, label: String?) {
|
||||||
|
sharedLocation = location
|
||||||
|
sharedLabel = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.show
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ShowLocationPresenterTest {
|
||||||
|
|
||||||
|
private val actions = FakeLocationActions()
|
||||||
|
private val location = Location(1.23, 4.56, 7.8f)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits initial state`() = runTest {
|
||||||
|
val presenter = ShowLocationPresenter(
|
||||||
|
actions,
|
||||||
|
location,
|
||||||
|
A_DESCRIPTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||||
|
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `uses action to share location`() = runTest {
|
||||||
|
val presenter = ShowLocationPresenter(
|
||||||
|
actions,
|
||||||
|
location,
|
||||||
|
A_DESCRIPTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(ShowLocationEvents.Share)
|
||||||
|
|
||||||
|
Truth.assertThat(actions.sharedLocation).isEqualTo(location)
|
||||||
|
Truth.assertThat(actions.sharedLabel).isEqualTo(A_DESCRIPTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val A_DESCRIPTION = "My happy place"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,9 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||||
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
||||||
|
|
@ -41,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNo
|
||||||
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.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.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
|
@ -59,6 +62,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||||
|
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||||
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Messages,
|
initialElement = NavTarget.Messages,
|
||||||
|
|
@ -82,6 +86,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AttachmentPreview(val attachment: Attachment) : NavTarget
|
data class AttachmentPreview(val attachment: Attachment) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class LocationViewer(val location: Location, val description: String?) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget
|
data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget
|
||||||
|
|
||||||
|
|
@ -147,6 +154,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
|
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
|
||||||
createNode<AttachmentsPreviewNode>(buildContext, listOf(inputs))
|
createNode<AttachmentsPreviewNode>(buildContext, listOf(inputs))
|
||||||
}
|
}
|
||||||
|
is NavTarget.LocationViewer -> {
|
||||||
|
val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description)
|
||||||
|
showLocationEntryPoint.createNode(this, buildContext, inputs)
|
||||||
|
}
|
||||||
is NavTarget.EventDebugInfo -> {
|
is NavTarget.EventDebugInfo -> {
|
||||||
val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo)
|
val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo)
|
||||||
createNode<EventDebugInfoNode>(buildContext, listOf(inputs))
|
createNode<EventDebugInfoNode>(buildContext, listOf(inputs))
|
||||||
|
|
@ -213,6 +224,13 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
backstack.push(navTarget)
|
backstack.push(navTarget)
|
||||||
}
|
}
|
||||||
|
is TimelineItemLocationContent -> {
|
||||||
|
val navTarget = NavTarget.LocationViewer(
|
||||||
|
location = event.content.location,
|
||||||
|
description = event.content.description,
|
||||||
|
)
|
||||||
|
backstack.push(navTarget)
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
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.location.api.Location
|
||||||
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
|
||||||
|
|
@ -68,7 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is LocationMessageType -> {
|
is LocationMessageType -> {
|
||||||
val location = parseGeoUri(messageType.geoUri)
|
val location = Location.fromGeoUri(messageType.geoUri)
|
||||||
if (location == null) {
|
if (location == null) {
|
||||||
TimelineItemTextContent(
|
TimelineItemTextContent(
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:74409b405f143793ac5641bb16f66731d7fa96d513a85e5e38c368b295b297c7
|
oid sha256:dcd8ccab99efd822f085614edb7296e5d73f3c1ae8d84ec0ccf930ead56bfa13
|
||||||
size 4965
|
size 7803
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
oid sha256:e6040743cf442f6e3069eb77f562d6803eb9243edc959c0db7b5631ad00c583b
|
||||||
size 4457
|
size 7103
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f1f40593f1075d245f98547fea35ab6861858cb02568efd1e9c6d1f1e07d85a9
|
||||||
|
size 10193
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9f571102bed06886257392347b27a935d3d34187de1004311395ee6f849e44a2
|
||||||
|
size 13504
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e13f80abff301cf0d345434a2010c7aa9e84f65514842712b1c8be64fde81160
|
||||||
|
size 23003
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:779f80e6cc4cb9ca9c3dd113cfaaeb528244b48d3b915f262ae5c797ad0b6524
|
||||||
|
size 10101
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:57852301ef65b259a7b3a28943918fc37261c3d183d5b543e246ef9fd07b542f
|
||||||
|
size 13937
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3f1d57e7cbabd106d236a9afb49cfbaf5546c1f2d6b490d11a5d313d101632b4
|
||||||
|
size 24994
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:46073600d49a438279e3fb891573a88eb17dd7c74f853078ce7d33dadc81c298
|
oid sha256:fd4a7d61010660b1c9081c48e562b219fd454e6ef23088bf0a6a1529d5fa67f6
|
||||||
size 14260
|
size 17379
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5bd98280b4cfbd97bfebe731a772874cef0a50ceea363d0d365318d38d164774
|
oid sha256:7204bf50f6f7352160f9f45dbbecebbb2186b0d2e59d51e4d5237b536f345867
|
||||||
size 16249
|
size 18062
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:32c0ace0df1b31d9e47aaa4fcfc4fa7eb0d7e7d9cde321bc3e13ffd85e991f22
|
oid sha256:a90e03f54c92c91388b3218942e633ced908fd3b77defbeefc492d5c97de97f5
|
||||||
size 39371
|
size 39371
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9c3a0506c870eb5f940821e5f05357f098f18fa177b143828e3e2afad09732dc
|
oid sha256:884654e20bd77864e1f7b7f137a539f8111ea50b8dd9c17ab689e246c70b2ead
|
||||||
size 40704
|
size 40705
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bdca76cdbf676d1b5dfd4bdf3871913a3fca0a9efafbf962e461a45278c9df13
|
oid sha256:c81885a3cffdb92e8282bfd511ac4efff4b02b1de2d6a37c01f9e41b94841e25
|
||||||
size 5426
|
size 5429
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:73750e1428e85548a8a8add1193d3c32709d79ffdc6c18125e3fbbc13d078c73
|
oid sha256:d08207b517c75145a791e0d1872bb65eb24cfd8c5c9364fb620e392ed9025c51
|
||||||
size 5683
|
size 5678
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:870b7a0002ec6fd77bfe071a64b7c5faf98062227800ca55dd1c21f517025a48
|
oid sha256:7d83815b1834a70e52fd55ab3558cb3f705abeba74474f4b23f6aafbb028a5ff
|
||||||
size 77684
|
size 77687
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:582d2f55a8f6707f1e4a55b4eadb7b19f18f3f78a097300151753c66e737e8e2
|
oid sha256:116fb7199e2577efa0d1b3de60f70054697671d554bfb2f695fb229511c03c2f
|
||||||
size 80326
|
size 80331
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue