Merge pull request #6342 from element-hq/feature/fga/live_location_sharing_setup
Setup live location sharing feature
This commit is contained in:
commit
92920b862b
197 changed files with 3767 additions and 2803 deletions
|
|
@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
|||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
|
|
@ -182,4 +183,30 @@ interface JoinedRoom : BaseRoom {
|
|||
* Subscribe to a [Flow] of [SendQueueUpdate] related to this room.
|
||||
*/
|
||||
fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate>
|
||||
|
||||
/**
|
||||
* Subscribe to live location shares in this room.
|
||||
* @return Flow of list of active live location shares.
|
||||
*/
|
||||
fun subscribeToLiveLocationShares(): Flow<List<LiveLocationShare>>
|
||||
|
||||
/**
|
||||
* Start sharing live location in this room.
|
||||
* @param durationMillis How long to share location (in milliseconds).
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit>
|
||||
|
||||
/**
|
||||
* Stop sharing live location in this room.
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun stopLiveLocationShare(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Send a live location update while a live location share is active.
|
||||
* @param geoUri The geo URI (e.g., "geo:51.5074,-0.1278").
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun sendLiveLocation(geoUri: String): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.room.location
|
|||
|
||||
enum class AssetType {
|
||||
SENDER,
|
||||
PIN
|
||||
PIN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.location
|
||||
|
||||
data class LiveLocationInfo(
|
||||
val description: String?,
|
||||
val geoUri: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Represents a live location share from a user in a room.
|
||||
*/
|
||||
data class LiveLocationShare(
|
||||
/** The user who is sharing their location. */
|
||||
val userId: UserId,
|
||||
/** The last known geo URI (e.g., "geo:51.5074,-0.1278"). */
|
||||
val lastGeoUri: String,
|
||||
/** The timestamp of the last location update. */
|
||||
val lastTimestamp: Long,
|
||||
/** Whether the live location share is still active. */
|
||||
val isLive: Boolean,
|
||||
)
|
||||
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
|
@ -102,6 +104,15 @@ data class FailedToParseStateContent(
|
|||
val error: String
|
||||
) : EventContent
|
||||
|
||||
data class LiveLocationContent(
|
||||
val body: String,
|
||||
val isLive: Boolean,
|
||||
val description: String?,
|
||||
val timeout: Long,
|
||||
val assetType: AssetType?,
|
||||
val locations: List<LiveLocationInfo>,
|
||||
) : EventContent
|
||||
|
||||
data object LegacyCallInviteContent : EventContent
|
||||
|
||||
data object CallNotifyContent : EventContent
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
|
||||
@Immutable
|
||||
sealed interface MessageType
|
||||
|
|
@ -55,6 +56,7 @@ data class LocationMessageType(
|
|||
val body: String,
|
||||
val geoUri: String,
|
||||
val description: String?,
|
||||
val assetType: AssetType?,
|
||||
) : MessageType
|
||||
|
||||
data class AudioMessageType(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.SendQueueUpdate
|
|||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
|
|
@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.impl.mapper.map
|
|||
import io.element.android.libraries.matrix.impl.room.history.map
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
|
||||
import io.element.android.libraries.matrix.impl.room.location.map
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustTimeline
|
||||
|
|
@ -66,6 +68,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
||||
import org.matrix.rustcomponents.sdk.KnockRequestsListener
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationShareListener
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
|
||||
import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate
|
||||
import org.matrix.rustcomponents.sdk.SendQueueListener
|
||||
|
|
@ -500,6 +503,34 @@ class JoinedRustRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override fun subscribeToLiveLocationShares(): Flow<List<LiveLocationShare>> {
|
||||
return mxCallbackFlow {
|
||||
innerRoom.subscribeToLiveLocationShares(object : LiveLocationShareListener {
|
||||
override fun call(liveLocationShares: List<org.matrix.rustcomponents.sdk.LiveLocationShare>) {
|
||||
trySend(liveLocationShares.map { it.map() })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.startLiveLocationShare(durationMillis.toULong())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopLiveLocationShare(): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.stopLiveLocationShare()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendLiveLocation(geoUri: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.sendLiveLocation(geoUri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() = destroy()
|
||||
|
||||
override fun destroy() {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,16 @@
|
|||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import org.matrix.rustcomponents.sdk.AssetType as RustAssetType
|
||||
|
||||
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
|
||||
fun AssetType.into(): RustAssetType = when (this) {
|
||||
AssetType.SENDER -> RustAssetType.SENDER
|
||||
AssetType.PIN -> RustAssetType.PIN
|
||||
AssetType.UNKNOWN -> RustAssetType.UNKNOWN
|
||||
}
|
||||
|
||||
fun RustAssetType.into(): AssetType = when (this) {
|
||||
RustAssetType.SENDER -> AssetType.SENDER
|
||||
RustAssetType.PIN -> AssetType.PIN
|
||||
RustAssetType.UNKNOWN -> AssetType.UNKNOWN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare
|
||||
|
||||
fun RustLiveLocationShare.map(): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
userId = UserId(userId),
|
||||
lastGeoUri = lastLocation.location.geoUri,
|
||||
lastTimestamp = lastLocation.ts.toLong(),
|
||||
isLive = isLive,
|
||||
)
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
|||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||
import io.element.android.libraries.matrix.impl.room.location.into
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
|
|
@ -478,7 +478,7 @@ class RustTimeline(
|
|||
geoUri = geoUri,
|
||||
description = description,
|
||||
zoomLevel = zoomLevel?.toUByte(),
|
||||
assetType = assetType?.toInner(),
|
||||
assetType = assetType?.into(),
|
||||
repliedToEventId = inReplyToEventId?.value,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.room.location.into
|
||||
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
|
||||
import org.matrix.rustcomponents.sdk.InReplyToDetails
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
|
|
@ -112,7 +113,12 @@ class EventMessageMapper {
|
|||
)
|
||||
}
|
||||
is RustMessageType.Location -> {
|
||||
LocationMessageType(type.content.body, type.content.geoUri, type.content.description)
|
||||
LocationMessageType(
|
||||
body = type.content.body,
|
||||
geoUri = type.content.geoUri,
|
||||
description = type.content.description,
|
||||
assetType = type.content.asset.into()
|
||||
)
|
||||
}
|
||||
is MessageType.Other -> {
|
||||
OtherMessageType(type.msgtype, type.body)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import org.junit.Test
|
|||
class AssetTypeKtTest {
|
||||
@Test
|
||||
fun toInner() {
|
||||
assertThat(AssetType.SENDER.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.SENDER)
|
||||
assertThat(AssetType.PIN.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.PIN)
|
||||
assertThat(AssetType.SENDER.into()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.SENDER)
|
||||
assertThat(AssetType.PIN.into()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.PIN)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.room.SendQueueUpdate
|
|||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
|
|
@ -84,6 +85,10 @@ class FakeJoinedRoom(
|
|||
private val enableEncryptionResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
|
||||
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
|
||||
private val liveLocationSharesFlow: Flow<List<LiveLocationShare>> = MutableStateFlow(emptyList()),
|
||||
private val startLiveLocationShareResult: (Long) -> Result<Unit> = { lambdaError() },
|
||||
private val stopLiveLocationShareResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val sendLiveLocationResult: (String) -> Result<Unit> = { lambdaError() },
|
||||
) : JoinedRoom, BaseRoom by baseRoom {
|
||||
private val sendQueueUpdates = MutableSharedFlow<SendQueueUpdate>(extraBufferCapacity = 10)
|
||||
|
||||
|
|
@ -227,6 +232,22 @@ class FakeJoinedRoom(
|
|||
return sendQueueUpdates
|
||||
}
|
||||
|
||||
override fun subscribeToLiveLocationShares(): Flow<List<LiveLocationShare>> {
|
||||
return liveLocationSharesFlow
|
||||
}
|
||||
|
||||
override suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> = simulateLongTask {
|
||||
startLiveLocationShareResult(durationMillis)
|
||||
}
|
||||
|
||||
override suspend fun stopLiveLocationShare(): Result<Unit> = simulateLongTask {
|
||||
stopLiveLocationShareResult()
|
||||
}
|
||||
|
||||
override suspend fun sendLiveLocation(geoUri: String): Result<Unit> = simulateLongTask {
|
||||
sendLiveLocationResult(geoUri)
|
||||
}
|
||||
|
||||
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
|
||||
progressCallbackValues.forEach { (current, total) ->
|
||||
progressCallback?.onProgress(current, total)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue