Merge branch 'develop' into feature/bma/swipeToReply
This commit is contained in:
commit
579e4df9cc
87 changed files with 1484 additions and 376 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -50,4 +50,6 @@ jobs:
|
|||
USERNAME=maestroelement
|
||||
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||
ROOM_NAME=MyRoom
|
||||
INVITEE1_MXID=@maestroelement2:matrix.org
|
||||
INVITEE2_MXID=@maestroelement3:matrix.org
|
||||
APP_ID=io.element.android.x.debug
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ From root dir of the project
|
|||
```shell
|
||||
maestro test \
|
||||
-e APP_ID=io.element.android.x.debug \
|
||||
-e USERNAME=user \
|
||||
-e USERNAME=user1 \
|
||||
-e PASSWORD=123 \
|
||||
-e ROOM_NAME="MyRoom" \
|
||||
-e INVITEE1_MXID=user2 \
|
||||
-e INVITEE2_MXID=user3 \
|
||||
.maestro/allTests.yaml
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- tapOn: "Sign in manually"
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
||||
- takeScreenshot: build/maestro/100-SignIn
|
||||
- runFlow: changeServer.yaml
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "Communicate and collaborate securely"
|
||||
visible: "Be in your element"
|
||||
timeout: 10_000
|
||||
|
|
|
|||
13
.maestro/tests/roomList/createAndDeleteDM.yaml
Normal file
13
.maestro/tests/roomList/createAndDeleteDM.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
# Purpose: Test the creation and deletion of a DM room.
|
||||
- tapOn: "Create a new conversation or room"
|
||||
- tapOn: "Search for someone"
|
||||
- inputText: ${INVITEE1_MXID}
|
||||
- tapOn:
|
||||
text: ${INVITEE1_MXID}
|
||||
index: 1
|
||||
- takeScreenshot: build/maestro/330-createAndDeleteDM
|
||||
- tapOn: "maestroelement2"
|
||||
- tapOn: "Leave room"
|
||||
- tapOn: "Leave"
|
||||
33
.maestro/tests/roomList/createAndDeleteRoom.yaml
Normal file
33
.maestro/tests/roomList/createAndDeleteRoom.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
# Purpose: Test the creation and deletion of a room
|
||||
- tapOn: "Create a new conversation or room"
|
||||
- tapOn: "New room"
|
||||
- tapOn: "Search for someone"
|
||||
- inputText: ${INVITEE1_MXID}
|
||||
- tapOn:
|
||||
text: ${INVITEE1_MXID}
|
||||
index: 1
|
||||
- tapOn: "Next"
|
||||
- tapOn: "e.g. your project name"
|
||||
- inputText: "aRoomName"
|
||||
- tapOn: "What is this room about?"
|
||||
- inputText: "aRoomTopic"
|
||||
- tapOn: "Create"
|
||||
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
||||
- tapOn: "aRoomName"
|
||||
- tapOn: "Invite people"
|
||||
# assert there's 1 memeber and 1 invitee
|
||||
- tapOn: "Search for someone"
|
||||
- inputText: ${INVITEE2_MXID}
|
||||
- tapOn:
|
||||
text: ${INVITEE2_MXID}
|
||||
index: 1
|
||||
- tapOn: "Send"
|
||||
- tapOn: "Back"
|
||||
- tapOn: "aRoomName"
|
||||
- tapOn: "People"
|
||||
# assert there's 1 memeber and 2 invitees
|
||||
- tapOn: "Back"
|
||||
- tapOn: "Leave room"
|
||||
- tapOn: "Leave"
|
||||
14
.maestro/tests/roomList/roomContextMenu.yaml
Normal file
14
.maestro/tests/roomList/roomContextMenu.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
# Purpose: Test the context menu of a room in the room list
|
||||
- longPressOn: ${ROOM_NAME}
|
||||
- takeScreenshot: build/maestro/310-RoomList-ContextMenu
|
||||
- tapOn:
|
||||
text: "Settings"
|
||||
index: 0
|
||||
- tapOn: "Back"
|
||||
- longPressOn: ${ROOM_NAME}
|
||||
- tapOn:
|
||||
text: "Leave room"
|
||||
index: 0
|
||||
- tapOn: "Cancel"
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- takeScreenshot: build/maestro/300-RoomList
|
||||
- runFlow: searchRoomList.yaml
|
||||
- takeScreenshot: build/maestro/300-RoomList
|
||||
- runFlow: timeline/timeline.yaml
|
||||
|
||||
- runFlow: roomContextMenu.yaml
|
||||
- runFlow: createAndDeleteRoom.yaml
|
||||
- runFlow: createAndDeleteDM.yaml
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ allprojects {
|
|||
config = files("$rootDir/tools/detekt/detekt.yml")
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.1.10")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.1.11")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
|
|||
1
changelog.d/712.bugfix
Normal file
1
changelog.d/712.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix actions for redacted, not sent and media messages
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -16,19 +16,25 @@
|
|||
|
||||
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+)?))?"""
|
||||
|
||||
@Parcelize
|
||||
data class Location(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
val accuracy: Float,
|
||||
)
|
||||
|
||||
fun parseGeoUri(geoUri: String): Location? {
|
||||
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
|
||||
return Location (
|
||||
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
|
||||
)
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
fun fromGeoUri(geoUri: String): Location? {
|
||||
val result = Regex(GEO_URI_REGEX).matchEntire(geoUri) ?: return null
|
||||
return Location(
|
||||
lat = result.groups["latitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||
lon = result.groups["longitude"]?.value?.toDoubleOrNull() ?: return null,
|
||||
accuracy = result.groups["uncertainty"]?.value?.toFloatOrNull() ?: 0f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 org.junit.Test
|
||||
|
||||
internal class GeoUrisKtTest {
|
||||
internal class LocationKtTest {
|
||||
|
||||
@Test
|
||||
fun `parseGeoUri - returns null for invalid urls`() {
|
||||
assertThat(parseGeoUri("")).isNull()
|
||||
assertThat(parseGeoUri("http://example.com/")).isNull()
|
||||
assertThat(parseGeoUri("geo:")).isNull()
|
||||
assertThat(parseGeoUri("geo:1.234")).isNull()
|
||||
assertThat(parseGeoUri("geo:1.234,")).isNull()
|
||||
assertThat(parseGeoUri("geo:,1.234")).isNull()
|
||||
assertThat(parseGeoUri("notgeo:1.234,5.678")).isNull()
|
||||
assertThat(parseGeoUri("geo:+1.234,5.678")).isNull()
|
||||
assertThat(parseGeoUri("geo:+1.234,*5.678")).isNull()
|
||||
assertThat(parseGeoUri("geo:not,good")).isNull()
|
||||
assertThat(parseGeoUri("geo:1.234,5.678;u=wrong")).isNull()
|
||||
assertThat(parseGeoUri("geo:1.234,5.678trailing")).isNull()
|
||||
assertThat(Location.fromGeoUri("")).isNull()
|
||||
assertThat(Location.fromGeoUri("http://example.com/")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:1.234")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:1.234,")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:,1.234")).isNull()
|
||||
assertThat(Location.fromGeoUri("notgeo:1.234,5.678")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:+1.234,5.678")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:+1.234,*5.678")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:not,good")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:1.234,5.678;u=wrong")).isNull()
|
||||
assertThat(Location.fromGeoUri("geo:1.234,5.678trailing")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
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,
|
||||
lon = 5.678,
|
||||
accuracy = 0f,
|
||||
))
|
||||
|
||||
assertThat(parseGeoUri("geo:1,5")).isEqualTo(Location(
|
||||
assertThat(Location.fromGeoUri("geo:1,5")).isEqualTo(Location(
|
||||
lat = 1.0,
|
||||
lon = 5.0,
|
||||
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,
|
||||
lon = 5.678,
|
||||
accuracy = 3000f,
|
||||
))
|
||||
|
||||
assertThat(parseGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
|
||||
assertThat(Location.fromGeoUri("geo:1,5;u=3000")).isEqualTo(Location(
|
||||
lat = 1.0,
|
||||
lon = 5.0,
|
||||
accuracy = 3000f,
|
||||
))
|
||||
|
||||
assertThat(parseGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
|
||||
assertThat(Location.fromGeoUri("geo:-1.234,-5.678;u=9.10")).isEqualTo(Location(
|
||||
lat = -1.234,
|
||||
lon = -5.678,
|
||||
accuracy = 9.10f,
|
||||
))
|
||||
|
||||
assertThat(parseGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
|
||||
assertThat(Location.fromGeoUri("geo:-1,-5;u=9.10")).isEqualTo(Location(
|
||||
lat = -1.0,
|
||||
lon = -5.0,
|
||||
accuracy = 9.10f,
|
||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.maplibre)
|
||||
implementation(libs.maplibre.annotation)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ 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 io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -62,6 +63,9 @@ class SendLocationPresenter @Inject constructor(
|
|||
room.sendLocation(
|
||||
body = "Location at latitude: ${event.lat}, longitude: ${event.lng}",
|
||||
geoUri = "geo:${event.lat},${event.lng}",
|
||||
description = null,
|
||||
zoomLevel = 15, // Send default zoom level for now.
|
||||
assetType = AssetType.PIN,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.core.content.getSystemService
|
|||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.location.impl.map
|
|||
import android.annotation.SuppressLint
|
||||
import android.view.Gravity
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -29,7 +30,9 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
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.plugins.annotation.SymbolManager
|
||||
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.impl.location.Location
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -69,7 +74,11 @@ fun MapView(
|
|||
// When in preview, early return a Box with the received modifier preserving layout
|
||||
if (LocalInspectionMode.current) {
|
||||
@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
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +164,7 @@ fun MapView(
|
|||
.withLatLng(LatLng(location.lat, location.lon))
|
||||
.withIconImage("pin")
|
||||
.withIconSize(1.3f)
|
||||
.withIconOffset(arrayOf(0f, 0.5f))
|
||||
.withIconAnchor(ICON_ANCHOR_BOTTOM)
|
||||
)
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.location
|
||||
package io.element.android.features.location.impl.show
|
||||
|
||||
/**
|
||||
* Represents a location sample emitted by the device's location subsystem.
|
||||
*/
|
||||
data class Location(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
val accuracy: Float,
|
||||
)
|
||||
import io.element.android.features.location.api.Location
|
||||
|
||||
interface LocationActions {
|
||||
fun share(location: Location, label: String?)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
import kotlinx.coroutines.delay
|
||||
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) {
|
||||
delay(1_000)
|
||||
emit(aLocation())
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLocation() = io.element.android.features.location.impl.location.Location(
|
||||
private fun aLocation() = Location(
|
||||
lat = 51.49404,
|
||||
lon = -0.25484,
|
||||
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.AssistedInject
|
||||
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.ShowLocationEntryPoint
|
||||
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.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.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
|
|
@ -59,6 +62,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
|
|
@ -83,7 +87,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
data class AttachmentPreview(val attachment: Attachment) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget
|
||||
data class LocationViewer(val location: Location, val description: String?) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class EventDebugInfo(val eventId: EventId?, val debugInfo: TimelineItemDebugInfo) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ForwardEvent(val eventId: EventId) : NavTarget
|
||||
|
|
@ -117,7 +124,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
callback?.onUserDataClicked(userId)
|
||||
}
|
||||
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) {
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +154,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
|
||||
createNode<AttachmentsPreviewNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
is NavTarget.LocationViewer -> {
|
||||
val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description)
|
||||
showLocationEntryPoint.createNode(this, buildContext, inputs)
|
||||
}
|
||||
is NavTarget.EventDebugInfo -> {
|
||||
val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo)
|
||||
createNode<EventDebugInfoNode>(buildContext, listOf(inputs))
|
||||
|
|
@ -213,6 +224,13 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
is TimelineItemLocationContent -> {
|
||||
val navTarget = NavTarget.LocationViewer(
|
||||
location = event.content.location,
|
||||
description = event.content.description,
|
||||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
|
||||
interface MessagesNavigator {
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
|
||||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportContentClicked(eventId: EventId, senderId: UserId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
fun onEventClicked(event: TimelineItem.Event)
|
||||
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
|
||||
fun onUserDataClicked(userId: UserId)
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
|
||||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||
fun onSendLocationClicked()
|
||||
|
|
@ -83,7 +83,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
private fun onUserDataClicked(userId: UserId) {
|
||||
callback?.onUserDataClicked(userId)
|
||||
}
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) {
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
||||
callback?.onReportMessage(eventId, senderId)
|
||||
}
|
||||
|
||||
|
||||
private fun onSendLocationClicked() {
|
||||
callback?.onSendLocationClicked()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,15 +227,19 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
room.redactEvent(event.eventId)
|
||||
if (event.failedToSend) {
|
||||
// If the message hasn't been sent yet, just cancel it
|
||||
event.transactionId?.let { room.cancelSend(it) }
|
||||
} else if (event.eventId != null) {
|
||||
room.redactEvent(event.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
if (targetEvent.eventId == null) return
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.eventId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty()
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(),
|
||||
targetEvent.transactionId,
|
||||
)
|
||||
composerState.eventSink(
|
||||
MessageComposerEvents.SetMode(composerMode)
|
||||
|
|
@ -288,7 +292,6 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleShowDebugInfoAction(event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
navigator.onShowEventDebugInfoClicked(event.eventId, event.debugInfo)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.messages.impl.actionlist
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -28,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -45,6 +48,10 @@ class ActionListPresenter @Inject constructor(
|
|||
mutableStateOf(ActionListState.Target.None)
|
||||
}
|
||||
|
||||
val displayEmojiReactions by remember {
|
||||
derivedStateOf { (target.value as? ActionListState.Target.Success)?.event?.sendState is EventSendState.Sent }
|
||||
}
|
||||
|
||||
fun handleEvents(event: ActionListEvents) {
|
||||
when (event) {
|
||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
||||
|
|
@ -54,29 +61,37 @@ class ActionListPresenter @Inject constructor(
|
|||
|
||||
return ActionListState(
|
||||
target = target.value,
|
||||
displayEmojiReactions = displayEmojiReactions,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val itemSent = timelineItem.sendState is EventSendState.Sent
|
||||
val actions =
|
||||
when (timelineItem.content) {
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemRedactedContent -> {
|
||||
if (buildMeta.isDebuggable) {
|
||||
listOf(TimelineItemAction.Developer)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
is TimelineItemStateContent -> {
|
||||
buildList {
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
add(TimelineItemAction.Copy)
|
||||
if (buildMeta.isDebuggable) {
|
||||
add(TimelineItemAction.Developer)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> buildList<TimelineItemAction> {
|
||||
add(TimelineItemAction.Reply)
|
||||
add(TimelineItemAction.Forward)
|
||||
if (timelineItem.isMine) {
|
||||
if (itemSent) {
|
||||
add(TimelineItemAction.Reply)
|
||||
add(TimelineItemAction.Forward)
|
||||
}
|
||||
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
|
|
@ -93,6 +108,10 @@ class ActionListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList())
|
||||
if (actions.isNotEmpty()) {
|
||||
target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList())
|
||||
} else {
|
||||
target.value = ActionListState.Target.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
@Immutable
|
||||
data class ActionListState(
|
||||
val target: Target,
|
||||
val displayEmojiReactions: Boolean,
|
||||
val eventSink: (ActionListEvents) -> Unit,
|
||||
) {
|
||||
sealed interface Target {
|
||||
|
|
|
|||
|
|
@ -61,11 +61,19 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemLocationContent()),
|
||||
actions = aTimelineItemActionList(),
|
||||
),
|
||||
displayEmojiReactions = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun anActionListState() = ActionListState(
|
||||
target = ActionListState.Target.None,
|
||||
displayEmojiReactions = true,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -175,13 +175,15 @@ private fun SheetContent(
|
|||
Divider()
|
||||
}
|
||||
}
|
||||
item {
|
||||
EmojiReactionsRow(
|
||||
onEmojiReactionClicked = onEmojiReactionClicked,
|
||||
onCustomReactionClicked = onCustomReactionClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Divider()
|
||||
if (state.displayEmojiReactions) {
|
||||
item {
|
||||
EmojiReactionsRow(
|
||||
onEmojiReactionClicked = onEmojiReactionClicked,
|
||||
onCustomReactionClicked = onCustomReactionClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
items(
|
||||
items = actions,
|
||||
|
|
|
|||
|
|
@ -196,10 +196,11 @@ class MessageComposerPresenter @Inject constructor(
|
|||
composerMode.setToNormal()
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(text)
|
||||
is MessageComposerMode.Edit -> room.editMessage(
|
||||
capturedMode.eventId,
|
||||
text
|
||||
)
|
||||
is MessageComposerMode.Edit -> {
|
||||
val eventId = capturedMode.eventId
|
||||
val transactionId = capturedMode.transactionId
|
||||
room.editMessage(eventId, transactionId, text)
|
||||
}
|
||||
|
||||
is MessageComposerMode.Quote -> TODO()
|
||||
is MessageComposerMode.Reply -> room.replyMessage(
|
||||
|
|
|
|||
|
|
@ -166,20 +166,12 @@ fun TimelineItemRow(
|
|||
)
|
||||
}
|
||||
is TimelineItem.Event -> {
|
||||
fun onClick() {
|
||||
onClick(timelineItem)
|
||||
}
|
||||
|
||||
fun onLongClick() {
|
||||
onLongClick(timelineItem)
|
||||
}
|
||||
|
||||
if (timelineItem.content is TimelineItemStateContent) {
|
||||
TimelineItemStateEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -187,8 +179,8 @@ fun TimelineItemRow(
|
|||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
canReply = canReply,
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
onUserDataClick = onUserDataClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onReactionClick = onReactionClick,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class EventDebugInfoNode @AssistedInject constructor(
|
|||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val eventId: EventId,
|
||||
val eventId: EventId?,
|
||||
val timelineItemDebugInfo: TimelineItemDebugInfo,
|
||||
) : NodeInputs
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun EventDebugInfoView(
|
||||
eventId: EventId,
|
||||
eventId: EventId?,
|
||||
model: String,
|
||||
originalJson: String?,
|
||||
latestEditedJson: String?,
|
||||
|
|
@ -99,7 +99,7 @@ fun EventDebugInfoView(
|
|||
item {
|
||||
Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Event ID:")
|
||||
CopyableText(text = eventId.value)
|
||||
CopyableText(text = eventId?.value ?: "-", modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
|
@ -142,7 +142,7 @@ private fun CollapsibleSection(
|
|||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) {
|
||||
CopyableText(text = text)
|
||||
CopyableText(text = text, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
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.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
|
|
@ -68,7 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
is LocationMessageType -> {
|
||||
val location = parseGeoUri(messageType.geoUri)
|
||||
val location = Location.fromGeoUri(messageType.geoUri)
|
||||
if (location == null) {
|
||||
TimelineItemTextContent(
|
||||
body = messageType.body,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -69,6 +70,10 @@ sealed interface TimelineItem {
|
|||
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||
|
||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
||||
|
||||
val failedToSend: Boolean = sendState is EventSendState.SendingFailed
|
||||
|
||||
val isTextMessage: Boolean = content is TimelineItemTextBasedContent
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class FakeMessagesNavigator : MessagesNavigator {
|
|||
var onReportContentClickedCount = 0
|
||||
private set
|
||||
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) {
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||
onShowEventDebugInfoClickedCount++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,12 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
|||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -62,7 +65,6 @@ class ActionListPresenterTest {
|
|||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
)
|
||||
|
|
@ -88,7 +90,6 @@ class ActionListPresenterTest {
|
|||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
)
|
||||
|
|
@ -184,7 +185,6 @@ class ActionListPresenterTest {
|
|||
persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Developer,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
|
|
@ -195,6 +195,63 @@ class ActionListPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute for a state item in debug build`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val stateEvent = aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
stateEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute for a state item in non-debuggable build`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val stateEvent = aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
stateEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
)
|
||||
)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute message in non-debuggable build`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||
|
|
@ -226,6 +283,62 @@ class ActionListPresenterTest {
|
|||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute message with no actions`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(
|
||||
isMine = true,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
val redactedEvent = aMessageEvent(
|
||||
isMine = true,
|
||||
content = TimelineItemRedactedContent,
|
||||
)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
|
||||
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent))
|
||||
awaitItem().run {
|
||||
assertThat(target).isEqualTo(ActionListState.Target.None)
|
||||
assertThat(displayEmojiReactions).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute not sent message`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(
|
||||
isMine = true,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
||||
sendState = EventSendState.NotSentYet,
|
||||
)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(successState.displayEmojiReactions).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal fun aMessageEvent(
|
|||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
||||
inReplyTo: InReplyTo? = null,
|
||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||
sendState: EventSendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
) = TimelineItem.Event(
|
||||
id = eventId?.value.orEmpty(),
|
||||
eventId = eventId,
|
||||
|
|
@ -48,7 +49,7 @@ internal fun aMessageEvent(
|
|||
sentTime = "",
|
||||
isMine = isMine,
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
sendState = sendState,
|
||||
inReplyTo = inReplyTo,
|
||||
debugInfo = debugInfo,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
|||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
|
@ -43,6 +44,7 @@ import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
|
|||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_REPLY
|
||||
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
|
|
@ -193,7 +195,7 @@ class MessageComposerPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - edit message`() = runTest {
|
||||
fun `present - edit sent message`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = createPresenter(
|
||||
this,
|
||||
|
|
@ -219,7 +221,38 @@ class MessageComposerPresenterTest {
|
|||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageParameter).isEqualTo(ANOTHER_MESSAGE)
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - edit not sent message`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = createPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
|
||||
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE))
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -474,6 +507,10 @@ class MessageComposerPresenterTest {
|
|||
)
|
||||
}
|
||||
|
||||
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
||||
fun anEditMode(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
message: String = A_MESSAGE,
|
||||
transactionId: String? = null,
|
||||
) = MessageComposerMode.Edit(eventId, message, transactionId)
|
||||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
|
||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ recyclerview = "1.3.0"
|
|||
lifecycle = "2.6.1"
|
||||
activity = "1.7.2"
|
||||
startup = "1.1.1"
|
||||
media3 = "1.0.2"
|
||||
media3 = "1.1.0"
|
||||
browser = "1.5.0"
|
||||
|
||||
# Compose
|
||||
|
|
@ -160,7 +160,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
|
|||
|
||||
# Analytics
|
||||
posthog = "com.posthog.android:posthog:2.0.3"
|
||||
sentry_android = "io.sentry:sentry-android:6.24.0"
|
||||
sentry_android = "io.sentry:sentry-android:6.25.0"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
|
||||
|
||||
# Di
|
||||
|
|
|
|||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
9
gradle/wrapper/gradle-wrapper.properties
vendored
9
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -14,10 +14,11 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
#Fri Oct 07 15:02:00 CEST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionSha256Sum=2cbafcd2c47a101cb2165f636b4677fac0b954949c9429c1c988da399defe6a9
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionSha256Sum=5022b0b25fe182b0e50867e77f484501dba44feeea88f5c1f13b6b4660463640
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
293
gradlew
vendored
293
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,67 +17,98 @@
|
|||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
|
@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
|
@ -98,88 +129,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
|||
181
gradlew.bat
vendored
181
gradlew.bat
vendored
|
|
@ -1,89 +1,92 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
|
|||
19
libraries/designsystem/src/main/res/drawable-night/pin.xml
Normal file
19
libraries/designsystem/src/main/res/drawable-night/pin.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="54dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="54">
|
||||
<path
|
||||
android:pathData="M25,54L18.938,48L31.062,48L25,54Z"
|
||||
android:fillColor="#EBEEF2"/>
|
||||
<path
|
||||
android:pathData="M25,25m-25,0a25,25 0,1 1,50 0a25,25 0,1 1,-50 0"
|
||||
android:fillColor="#EBEEF2"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M13,13h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M25,13C20.356,13 16.6,16.858 16.6,21.629C16.6,26.769 21.904,33.857 24.088,36.556C24.568,37.148 25.444,37.148 25.924,36.556C28.096,33.857 33.4,26.769 33.4,21.629C33.4,16.858 29.644,13 25,13ZM25,24.71C23.344,24.71 22,23.33 22,21.629C22,19.928 23.344,18.547 25,18.547C26.656,18.547 28,19.928 28,21.629C28,23.33 26.656,24.71 25,24.71Z"
|
||||
android:fillColor="#101317"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -3,21 +3,17 @@
|
|||
android:height="54dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="54">
|
||||
<path
|
||||
android:pathData="M25,54L18.938,48L31.062,48L25,54Z"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<path
|
||||
android:pathData="M25,25m-25,0a25,25 0,1 1,50 0a25,25 0,1 1,-50 0"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h50v108h-50z"/>
|
||||
android:pathData="M13,13h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M25,54L18.94,48L31.06,48L25,54Z"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<path
|
||||
android:pathData="M25,25m-25,0a25,25 0,1 1,50 0a25,25 0,1 1,-50 0"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M13,13h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M25,13C20.36,13 16.6,16.86 16.6,21.63C16.6,26.77 21.9,33.86 24.09,36.56C24.57,37.15 25.44,37.15 25.92,36.56C28.1,33.86 33.4,26.77 33.4,21.63C33.4,16.86 29.64,13 25,13ZM25,24.71C23.34,24.71 22,23.33 22,21.63C22,19.93 23.34,18.55 25,18.55C26.66,18.55 28,19.93 28,21.63C28,23.33 26.66,24.71 25,24.71Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
android:pathData="M25,13C20.356,13 16.6,16.858 16.6,21.629C16.6,26.769 21.904,33.857 24.088,36.556C24.568,37.148 25.444,37.148 25.924,36.556C28.096,33.857 33.4,26.769 33.4,21.629C33.4,16.858 29.644,13 25,13ZM25,24.71C23.344,24.71 22,23.33 22,21.629C22,19.928 23.344,18.547 25,18.547C26.656,18.547 28,19.928 28,21.629C28,23.33 26.656,24.71 25,24.71Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import io.element.android.libraries.matrix.api.media.AudioInfo
|
|||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
|
@ -71,7 +71,7 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun sendMessage(message: String): Result<Unit>
|
||||
|
||||
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit>
|
||||
suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit>
|
||||
|
||||
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit>
|
||||
|
||||
|
|
@ -123,6 +123,16 @@ interface MatrixRoom : Closeable {
|
|||
* @param body A human readable textual representation of the location.
|
||||
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
|
||||
* Respectively: latitude, longitude, and (optional) uncertainty.
|
||||
* @param description Optional description of the location to display to the user.
|
||||
* @param zoomLevel Optional zoom level to display the map at.
|
||||
* @param assetType Optional type of the location asset.
|
||||
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
|
||||
*/
|
||||
suspend fun sendLocation(body: String, geoUri: String): Result<Unit>
|
||||
suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String? = null,
|
||||
zoomLevel: Int? = null,
|
||||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.libraries.matrix.api.room.location
|
||||
|
||||
enum class AssetType {
|
||||
SENDER,
|
||||
PIN
|
||||
}
|
||||
|
|
@ -31,9 +31,9 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
|||
return RoomSummaryDetails(
|
||||
roomId = RoomId(roomListItem.id()),
|
||||
name = roomListItem.name() ?: roomListItem.id(),
|
||||
canonicalAlias = room?.canonicalAlias(),
|
||||
isDirect = room?.isDirect() ?: false,
|
||||
avatarURLString = room?.avatarUrl(),
|
||||
canonicalAlias = roomListItem.canonicalAlias(),
|
||||
isDirect = roomListItem.isDirect(),
|
||||
avatarURLString = roomListItem.avatarUrl(),
|
||||
unreadNotificationCount = roomListItem.unreadNotifications().use { it.notificationCount().toInt() },
|
||||
lastMessage = latestRoomMessage,
|
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ import kotlinx.coroutines.CompletableDeferred
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -31,6 +33,7 @@ class RoomSummaryListProcessor(
|
|||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
||||
private val roomListService: RoomListService,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
private val shouldFetchFullRoom: Boolean = false,
|
||||
) {
|
||||
|
||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||
|
|
@ -113,7 +116,7 @@ class RoomSummaryListProcessor(
|
|||
|
||||
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
|
||||
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
roomListItem.fullRoom().use { fullRoom ->
|
||||
roomListItem.fullRoomOrNull().use { fullRoom ->
|
||||
RoomSummary.Filled(
|
||||
details = roomSummaryDetailsFactory.create(roomListItem, fullRoom)
|
||||
)
|
||||
|
|
@ -123,6 +126,14 @@ class RoomSummaryListProcessor(
|
|||
return builtRoomSummary
|
||||
}
|
||||
|
||||
private fun RoomListItem.fullRoomOrNull(): Room? {
|
||||
return if (shouldFetchFullRoom) {
|
||||
fullRoom()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) =
|
||||
mutex.withLock {
|
||||
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
|
|
@ -33,8 +34,10 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
|
|||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
|
||||
|
|
@ -44,6 +47,7 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -207,11 +211,16 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
// val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId)
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
if (originalEventId != null) {
|
||||
runCatching {
|
||||
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
transactionId?.let { cancelSend(it) }
|
||||
innerRoom.send(messageEventContentFromMarkdown(message), genTransactionId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,13 +377,22 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
//TODO expose inner parameters
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.sendLocation(body, geoUri, null, null, null, genTransactionId())
|
||||
innerRoom.sendLocation(
|
||||
body = body,
|
||||
geoUri = geoUri,
|
||||
description = description,
|
||||
zoomLevel = zoomLevel?.toUByte(),
|
||||
assetType = assetType?.toInner(),
|
||||
txnId = genTransactionId()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ internal class RustRoomSummaryDataSource(
|
|||
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
|
||||
private val allRoomsLoadingState: MutableStateFlow<RoomSummaryDataSource.LoadingState> = MutableStateFlow(RoomSummaryDataSource.LoadingState.NotLoaded)
|
||||
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, roomListService, roomSummaryDetailsFactory)
|
||||
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory)
|
||||
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
|
||||
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
|
||||
|
||||
init {
|
||||
sessionCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
|
||||
fun AssetType.toInner(): org.matrix.rustcomponents.sdk.AssetType = when (this) {
|
||||
AssetType.SENDER -> org.matrix.rustcomponents.sdk.AssetType.SENDER
|
||||
AssetType.PIN -> org.matrix.rustcomponents.sdk.AssetType.PIN
|
||||
}
|
||||
|
|
@ -110,4 +110,8 @@ class RustMatrixTimeline(
|
|||
innerRoom.sendReadReceipt(eventId = eventId.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun getItemById(eventId: EventId): MatrixTimelineItem.Event? {
|
||||
return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
|
|
@ -80,6 +81,7 @@ class FakeMatrixRoom(
|
|||
private var reportContentResult = Result.success(Unit)
|
||||
private var sendLocationResult = Result.success(Unit)
|
||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||
val editMessageCalls = mutableListOf<String>()
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
|
@ -173,11 +175,8 @@ class FakeMatrixRoom(
|
|||
return cancelSendResult
|
||||
}
|
||||
|
||||
var editMessageParameter: String? = null
|
||||
private set
|
||||
|
||||
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
|
||||
editMessageParameter = message
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit> {
|
||||
editMessageCalls += message
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +285,10 @@ class FakeMatrixRoom(
|
|||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> = simulateLongTask {
|
||||
sendLocationCount++
|
||||
return sendLocationResult
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ sealed interface MessageComposerMode : Parcelable {
|
|||
@Parcelize
|
||||
data class Normal(val content: CharSequence?) : MessageComposerMode
|
||||
|
||||
sealed class Special(open val eventId: EventId, open val defaultContent: CharSequence) :
|
||||
sealed class Special(open val eventId: EventId?, open val defaultContent: CharSequence) :
|
||||
MessageComposerMode
|
||||
|
||||
@Parcelize
|
||||
data class Edit(override val eventId: EventId, override val defaultContent: CharSequence) :
|
||||
data class Edit(override val eventId: EventId?, override val defaultContent: CharSequence, val transactionId: String?) :
|
||||
Special(eventId, defaultContent)
|
||||
|
||||
@Parcelize
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ private fun EditContentToPreview() {
|
|||
TextComposer(
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"),
|
||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", "1234"),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74409b405f143793ac5641bb16f66731d7fa96d513a85e5e38c368b295b297c7
|
||||
size 4965
|
||||
oid sha256:dcd8ccab99efd822f085614edb7296e5d73f3c1ae8d84ec0ccf930ead56bfa13
|
||||
size 7803
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
oid sha256:e6040743cf442f6e3069eb77f562d6803eb9243edc959c0db7b5631ad00c583b
|
||||
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
|
||||
oid sha256:46073600d49a438279e3fb891573a88eb17dd7c74f853078ce7d33dadc81c298
|
||||
size 14260
|
||||
oid sha256:fd4a7d61010660b1c9081c48e562b219fd454e6ef23088bf0a6a1529d5fa67f6
|
||||
size 17379
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5bd98280b4cfbd97bfebe731a772874cef0a50ceea363d0d365318d38d164774
|
||||
size 16249
|
||||
oid sha256:7204bf50f6f7352160f9f45dbbecebbb2186b0d2e59d51e4d5237b536f345867
|
||||
size 18062
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:32c0ace0df1b31d9e47aaa4fcfc4fa7eb0d7e7d9cde321bc3e13ffd85e991f22
|
||||
oid sha256:a90e03f54c92c91388b3218942e633ced908fd3b77defbeefc492d5c97de97f5
|
||||
size 39371
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12f2f8146898375b4556dfd0718b02b18ac99859efe7e857914b2cf424ca17ba
|
||||
size 25792
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c3a0506c870eb5f940821e5f05357f098f18fa177b143828e3e2afad09732dc
|
||||
size 40704
|
||||
oid sha256:884654e20bd77864e1f7b7f137a539f8111ea50b8dd9c17ab689e246c70b2ead
|
||||
size 40705
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2822e203b85195a5584fa0d3e0be2b260ff233e725ae104f237c46978f6ae068
|
||||
size 27337
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15c77e539cc793ab0875813ac98eab14412a0619ca16afc90513e11f41462e07
|
||||
size 32575
|
||||
oid sha256:b28bdd7f227340b1af4456f0e24a78b8b279819b7e41edca11c0f2bc9a14a15b
|
||||
size 32894
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:047b5e935803b2c7b12ebb1d86feabccac9d6291de3c14fea1185c2279db8d57
|
||||
size 35318
|
||||
oid sha256:f7d39ebcf85878e3869d369a10f95efe6c786db44c098864473b3e5bda51bd83
|
||||
size 35141
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bdca76cdbf676d1b5dfd4bdf3871913a3fca0a9efafbf962e461a45278c9df13
|
||||
size 5426
|
||||
oid sha256:c81885a3cffdb92e8282bfd511ac4efff4b02b1de2d6a37c01f9e41b94841e25
|
||||
size 5429
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73750e1428e85548a8a8add1193d3c32709d79ffdc6c18125e3fbbc13d078c73
|
||||
size 5683
|
||||
oid sha256:d08207b517c75145a791e0d1872bb65eb24cfd8c5c9364fb620e392ed9025c51
|
||||
size 5678
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:870b7a0002ec6fd77bfe071a64b7c5faf98062227800ca55dd1c21f517025a48
|
||||
size 77684
|
||||
oid sha256:7d83815b1834a70e52fd55ab3558cb3f705abeba74474f4b23f6aafbb028a5ff
|
||||
size 77687
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:582d2f55a8f6707f1e4a55b4eadb7b19f18f3f78a097300151753c66e737e8e2
|
||||
size 80326
|
||||
oid sha256:116fb7199e2577efa0d1b3de60f70054697671d554bfb2f695fb229511c03c2f
|
||||
size 80331
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue