Send pin-drop location (#636)
Share pindrop location This feature allows the user to share any location by just selecting it from the map. Closes: https://github.com/vector-im/element-x-android/issues/690
This commit is contained in:
parent
b292c3a29b
commit
0858cfb272
30 changed files with 534 additions and 113 deletions
|
|
@ -247,7 +247,7 @@ koverMerged {
|
||||||
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
|
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
|
||||||
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
|
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
|
||||||
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
|
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
|
||||||
excludes += "io.element.android.features.location.api.MapState"
|
excludes += "io.element.android.features.location.impl.map.MapState"
|
||||||
}
|
}
|
||||||
bound {
|
bound {
|
||||||
minValue = 90
|
minValue = 90
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.anvil)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,29 +23,14 @@ android {
|
||||||
namespace = "io.element.android.features.location.api"
|
namespace = "io.element.android.features.location.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
|
||||||
generateDaggerFactories.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.network)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.maplibre)
|
|
||||||
implementation(libs.network.retrofit)
|
|
||||||
implementation(libs.maplibre.annotation)
|
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.serialization.json)
|
|
||||||
implementation(libs.accompanist.permission)
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Send location" screen.
|
||||||
|
*
|
||||||
|
* Allows a user to share a location message within a room.
|
||||||
|
*/
|
||||||
|
interface SendLocationEntryPoint : SimpleFeatureEntryPoint
|
||||||
|
|
@ -27,7 +27,7 @@ private const val STATIC_MAP_SCALE = "" // Either "" (empty string) for normal i
|
||||||
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
|
||||||
|
|
||||||
internal fun buildTileServerUrl(
|
fun buildTileServerUrl(
|
||||||
darkMode: Boolean
|
darkMode: Boolean
|
||||||
): String = if (!darkMode) {
|
): String = if (!darkMode) {
|
||||||
"$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY"
|
"$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY"
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("io.element.android-compose-library")
|
|
||||||
alias(libs.plugins.anvil)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "io.element.android.features.location.fake"
|
|
||||||
}
|
|
||||||
|
|
||||||
anvil {
|
|
||||||
generateDaggerFactories.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.dagger)
|
|
||||||
api(projects.features.location.api)
|
|
||||||
implementation(projects.libraries.designsystem)
|
|
||||||
implementation(projects.libraries.di)
|
|
||||||
implementation(projects.libraries.network)
|
|
||||||
implementation(projects.libraries.core)
|
|
||||||
implementation(libs.maplibre)
|
|
||||||
implementation(libs.network.retrofit)
|
|
||||||
implementation(libs.maplibre.annotation)
|
|
||||||
implementation(libs.coil.compose)
|
|
||||||
implementation(libs.serialization.json)
|
|
||||||
implementation(libs.accompanist.permission)
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,18 +29,18 @@ anvil {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
|
||||||
api(projects.features.location.api)
|
api(projects.features.location.api)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.network)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(libs.maplibre)
|
implementation(libs.maplibre)
|
||||||
implementation(libs.network.retrofit)
|
|
||||||
implementation(libs.maplibre.annotation)
|
implementation(libs.maplibre.annotation)
|
||||||
implementation(libs.coil.compose)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.dagger)
|
||||||
implementation(libs.accompanist.permission)
|
implementation(projects.anvilannotations)
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
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.SendLocationEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class SendLocationEntryPointImpl @Inject constructor() : SendLocationEntryPoint {
|
||||||
|
override fun createNode(
|
||||||
|
parentNode: Node, buildContext: BuildContext
|
||||||
|
): SendLocationNode = parentNode.createNode(buildContext)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
sealed interface SendLocationEvents {
|
||||||
|
data class ShareLocation(val lat: Double, val lng: Double) : SendLocationEvents
|
||||||
|
data class SwitchMode(val mode: SendLocationState.Mode) : SendLocationEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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 io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
||||||
|
@ContributesNode(RoomScope::class)
|
||||||
|
class SendLocationNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: SendLocationPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
SendLocationView(
|
||||||
|
state = presenter.present(),
|
||||||
|
modifier = modifier,
|
||||||
|
onBackPressed = ::navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SendLocationPresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
) : Presenter<SendLocationState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): SendLocationState {
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var mode by remember {
|
||||||
|
mutableStateOf<SendLocationState.Mode>(SendLocationState.Mode.ALocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: SendLocationEvents) {
|
||||||
|
when (event) {
|
||||||
|
is SendLocationEvents.ShareLocation -> scope.launch {
|
||||||
|
shareLocation(event)
|
||||||
|
}
|
||||||
|
is SendLocationEvents.SwitchMode -> {
|
||||||
|
mode = event.mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendLocationState(
|
||||||
|
mode = mode,
|
||||||
|
eventSink = ::handleEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun shareLocation(
|
||||||
|
event: SendLocationEvents.ShareLocation
|
||||||
|
) {
|
||||||
|
room.sendLocation(
|
||||||
|
body = "Location at latitude: ${event.lat}, longitude: ${event.lng}",
|
||||||
|
geoUri = "geo:${event.lat},${event.lng}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
data class SendLocationState(
|
||||||
|
val mode: Mode = Mode.ALocation,
|
||||||
|
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||||
|
) {
|
||||||
|
sealed interface Mode {
|
||||||
|
object MyLocation : Mode
|
||||||
|
object ALocation : Mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
|
class SendLocationStateProvider : PreviewParameterProvider<SendLocationState> {
|
||||||
|
override val values: Sequence<SendLocationState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
SendLocationState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
|
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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.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.BottomSheetScaffold
|
||||||
|
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.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SendLocationView(
|
||||||
|
state: SendLocationState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val mapState = rememberMapState()
|
||||||
|
BottomSheetScaffold(
|
||||||
|
sheetContent = {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(CommonStrings.screen_share_this_location_action))
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
state.eventSink(
|
||||||
|
SendLocationEvents.ShareLocation(
|
||||||
|
lat = mapState.position.lat,
|
||||||
|
lng = mapState.position.lon
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onBackPressed()
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(Icons.Default.LocationOn, null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
scaffoldState = rememberBottomSheetScaffoldState(
|
||||||
|
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded),
|
||||||
|
),
|
||||||
|
sheetDragHandle = {},
|
||||||
|
sheetSwipeEnabled = false,
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.screen_share_location_title),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackPressed)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(it)
|
||||||
|
.consumeWindowInsets(it),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
MapView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
mapState = mapState,
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
resourceId = R.drawable.pin,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SendLocationViewLightPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SendLocationViewDarkPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: SendLocationState) {
|
||||||
|
SendLocationView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.api
|
package io.element.android.features.location.impl.location
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a location sample emitted by the device's location subsystem.
|
* Represents a location sample emitted by the device's location subsystem.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.impl
|
package io.element.android.features.location.impl.location
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -25,7 +25,6 @@ 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.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.features.location.api.Location
|
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
|
@ -14,15 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.api
|
package io.element.android.features.location.impl.map
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.Gravity
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -31,7 +29,6 @@ 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.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
|
@ -47,11 +44,11 @@ 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.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.FloatingActionButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
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
|
||||||
|
|
@ -68,7 +65,6 @@ fun MapView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
mapState: MapState = rememberMapState(),
|
mapState: MapState = rememberMapState(),
|
||||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||||
onLocationClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
// 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) {
|
||||||
|
|
@ -88,7 +84,10 @@ fun MapView(
|
||||||
LaunchedEffect(darkMode) {
|
LaunchedEffect(darkMode) {
|
||||||
mapView.awaitMap().let { map ->
|
mapView.awaitMap().let { map ->
|
||||||
map.uiSettings.apply {
|
map.uiSettings.apply {
|
||||||
|
attributionGravity = Gravity.TOP
|
||||||
|
logoGravity = Gravity.TOP
|
||||||
isCompassEnabled = false
|
isCompassEnabled = false
|
||||||
|
isRotateGesturesEnabled = false
|
||||||
}
|
}
|
||||||
map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style ->
|
map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style ->
|
||||||
mapRefs = MapRefs(
|
mapRefs = MapRefs(
|
||||||
|
|
@ -180,20 +179,10 @@ fun MapView(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ModifierReused")
|
@Suppress("ModifierReused")
|
||||||
Box(modifier = modifier) {
|
AndroidView(
|
||||||
AndroidView(factory = { mapView })
|
factory = { mapView },
|
||||||
FloatingActionButton(
|
modifier = modifier
|
||||||
onClick = onLocationClick,
|
)
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(16.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.LocationOn,
|
|
||||||
contentDescription = null, // TODO
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -292,6 +281,5 @@ private fun ContentToPreview() {
|
||||||
)
|
)
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
),
|
),
|
||||||
onLocationClick = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SendLocationPresenterTest {
|
||||||
|
|
||||||
|
private val room = FakeMatrixRoom()
|
||||||
|
private val presenter = SendLocationPresenter(room)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits initial state`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.ALocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `share location event shares a location`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SendLocationEvents.ShareLocation(1.0, 2.0))
|
||||||
|
delay(1)
|
||||||
|
Truth.assertThat(room.sendLocationCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `switches mode`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SendLocationEvents.SwitchMode(SendLocationState.Mode.MyLocation))
|
||||||
|
Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.MyLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,21 +14,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.fake
|
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<Location> = flow {
|
fun fakeLocationUpdatesFlow(): Flow<io.element.android.features.location.impl.location.Location> = flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
emit(aLocation())
|
emit(aLocation())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aLocation() = Location(
|
private fun aLocation() = io.element.android.features.location.impl.location.Location(
|
||||||
lat = 51.49404,
|
lat = 51.49404,
|
||||||
lon = -0.25484,
|
lon = -0.25484,
|
||||||
accuracy = 5f
|
accuracy = 5f
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ 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.SendLocationEntryPoint
|
||||||
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
|
||||||
|
|
@ -57,6 +58,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class MessagesFlowNode @AssistedInject constructor(
|
class MessagesFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||||
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Messages,
|
initialElement = NavTarget.Messages,
|
||||||
|
|
@ -88,6 +90,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
|
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object SendLocation : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||||
|
|
@ -123,6 +128,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
override fun onReportMessage(eventId: EventId, senderId: UserId) {
|
override fun onReportMessage(eventId: EventId, senderId: UserId) {
|
||||||
backstack.push(NavTarget.ReportMessage(eventId, senderId))
|
backstack.push(NavTarget.ReportMessage(eventId, senderId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSendLocationClicked() {
|
||||||
|
backstack.push(NavTarget.SendLocation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +164,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
|
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
|
||||||
createNode<ReportMessageNode>(buildContext, listOf(inputs))
|
createNode<ReportMessageNode>(buildContext, listOf(inputs))
|
||||||
}
|
}
|
||||||
|
NavTarget.SendLocation -> {
|
||||||
|
sendLocationEntryPoint.createNode(this, buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||||
fun onForwardEventClicked(eventId: EventId)
|
fun onForwardEventClicked(eventId: EventId)
|
||||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||||
|
fun onSendLocationClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -93,6 +94,10 @@ class MessagesNode @AssistedInject constructor(
|
||||||
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
||||||
callback?.onReportMessage(eventId, senderId)
|
callback?.onReportMessage(eventId, senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSendLocationClicked() {
|
||||||
|
callback?.onSendLocationClicked()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
@ -104,6 +109,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
onEventClicked = this::onEventClicked,
|
onEventClicked = this::onEventClicked,
|
||||||
onPreviewAttachments = this::onPreviewAttachments,
|
onPreviewAttachments = this::onPreviewAttachments,
|
||||||
onUserDataClicked = this::onUserDataClicked,
|
onUserDataClicked = this::onUserDataClicked,
|
||||||
|
onSendLocationClicked = this::onSendLocationClicked,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ fun MessagesView(
|
||||||
onEventClicked: (event: TimelineItem.Event) -> Unit,
|
onEventClicked: (event: TimelineItem.Event) -> Unit,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||||
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||||
|
|
@ -153,7 +154,8 @@ fun MessagesView(
|
||||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onReactionClicked = ::onEmojiReactionClicked
|
onReactionClicked = ::onEmojiReactionClicked,
|
||||||
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -237,6 +239,7 @@ fun MessagesViewContent(
|
||||||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -260,6 +263,7 @@ fun MessagesViewContent(
|
||||||
if (state.userHasPermissionToSendMessage) {
|
if (state.userHasPermissionToSendMessage) {
|
||||||
MessageComposerView(
|
MessageComposerView(
|
||||||
state = state.composerState,
|
state = state.composerState,
|
||||||
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(Alignment.Bottom)
|
.wrapContentHeight(Alignment.Bottom)
|
||||||
|
|
@ -347,5 +351,6 @@ private fun ContentToPreview(state: MessagesState) {
|
||||||
onEventClicked = {},
|
onEventClicked = {},
|
||||||
onPreviewAttachments = {},
|
onPreviewAttachments = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
|
onSendLocationClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material.ListItem
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AttachFile
|
import androidx.compose.material.icons.filled.AttachFile
|
||||||
import androidx.compose.material.icons.filled.Collections
|
import androidx.compose.material.icons.filled.Collections
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
import androidx.compose.material.icons.filled.PhotoCamera
|
import androidx.compose.material.icons.filled.PhotoCamera
|
||||||
import androidx.compose.material.icons.filled.Videocam
|
import androidx.compose.material.icons.filled.Videocam
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -48,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AttachmentsBottomSheet(
|
internal fun AttachmentsBottomSheet(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val localView = LocalView.current
|
val localView = LocalView.current
|
||||||
|
|
@ -78,7 +80,10 @@ internal fun AttachmentsBottomSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onDismissRequest = { isVisible = false }
|
onDismissRequest = { isVisible = false }
|
||||||
) {
|
) {
|
||||||
AttachmentSourcePickerMenu(eventSink = state.eventSink)
|
AttachmentSourcePickerMenu(
|
||||||
|
eventSink = state.eventSink,
|
||||||
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +92,7 @@ internal fun AttachmentsBottomSheet(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AttachmentSourcePickerMenu(
|
internal fun AttachmentSourcePickerMenu(
|
||||||
eventSink: (MessageComposerEvents) -> Unit,
|
eventSink: (MessageComposerEvents) -> Unit,
|
||||||
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -113,5 +119,13 @@ internal fun AttachmentSourcePickerMenu(
|
||||||
icon = { Icon(Icons.Default.Videocam, null) },
|
icon = { Icon(Icons.Default.Videocam, null) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||||
)
|
)
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
||||||
|
onSendLocationClicked()
|
||||||
|
},
|
||||||
|
icon = { Icon(Icons.Default.LocationOn, null) },
|
||||||
|
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,6 @@ sealed interface MessageComposerEvents {
|
||||||
object FromFiles : PickAttachmentSource
|
object FromFiles : PickAttachmentSource
|
||||||
object PhotoFromCamera : PickAttachmentSource
|
object PhotoFromCamera : PickAttachmentSource
|
||||||
object VideoFromCamera : PickAttachmentSource
|
object VideoFromCamera : PickAttachmentSource
|
||||||
|
object Location : PickAttachmentSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,10 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
cameraVideoPicker.launch()
|
cameraVideoPicker.launch()
|
||||||
}
|
}
|
||||||
|
MessageComposerEvents.PickAttachmentSource.Location -> {
|
||||||
|
showAttachmentSourcePicker = false
|
||||||
|
// Navigation to the location picker screen is done at the view layer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.textcomposer.TextComposer
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageComposerView(
|
fun MessageComposerView(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onFullscreenToggle() {
|
fun onFullscreenToggle() {
|
||||||
|
|
@ -55,7 +56,10 @@ fun MessageComposerView(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
AttachmentsBottomSheet(state = state)
|
AttachmentsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
|
)
|
||||||
|
|
||||||
TextComposer(
|
TextComposer(
|
||||||
onSendMessage = ::sendMessage,
|
onSendMessage = ::sendMessage,
|
||||||
|
|
@ -83,5 +87,8 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: MessageComposerState) {
|
private fun ContentToPreview(state: MessageComposerState) {
|
||||||
MessageComposerView(state)
|
MessageComposerView(
|
||||||
|
state = state,
|
||||||
|
onSendLocationClicked = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:46073600d49a438279e3fb891573a88eb17dd7c74f853078ce7d33dadc81c298
|
||||||
|
size 14260
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5bd98280b4cfbd97bfebe731a772874cef0a50ceea363d0d365318d38d164774
|
||||||
|
size 16249
|
||||||
Loading…
Add table
Add a link
Reference in a new issue