Live location : ensure it's not sorted randomly

This commit is contained in:
ganfra 2026-04-17 14:54:53 +02:00
parent fbfeeae084
commit 8182a149d0
6 changed files with 124 additions and 26 deletions

View file

@ -0,0 +1,20 @@
/*
* 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.features.location.impl.show
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
class LiveLocationShareComparator(private val currentUser: UserId) : Comparator<LiveLocationShare> {
override fun compare(p0: LiveLocationShare, p1: LiveLocationShare): Int {
val p0IsCurrentUser = p0.userId == currentUser
val p1IsCurrentUser = p1.userId == currentUser
if (p0IsCurrentUser != p1IsCurrentUser) return if (p0IsCurrentUser) -1 else 1
return p1.startTimestamp.compareTo(p0.startTimestamp)
}
}

View file

@ -133,35 +133,39 @@ class ShowLocationPresenter(
}
is ShowLocationMode.Live -> {
produceState(persistentListOf()) {
val comparator = LiveLocationShareComparator(currentUser = joinedRoom.sessionId)
val liveLocationSharesFlow = joinedRoom.subscribeToLiveLocationShares()
val membersStateFlow = joinedRoom.membersStateFlow.mapState { it.joinedRoomMembers() }
combine(liveLocationSharesFlow, membersStateFlow) { liveShares, members ->
liveShares.mapNotNull { share ->
val lastLocation = share.lastLocation ?: return@mapNotNull null
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
val member = members.find { it.userId == share.userId }
val displayName = member?.getBestName() ?: share.userId.value
val avatarUrl = member?.avatarUrl
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
val formattedTimestamp = stringProvider.getString(
CommonStrings.screen_static_location_sheet_timestamp_description,
relativeTime
)
LocationShareItem(
userId = share.userId,
displayName = displayName,
avatarData = AvatarData(
id = share.userId.value,
name = displayName,
url = avatarUrl,
size = AvatarSize.UserListItem,
),
formattedTimestamp = formattedTimestamp,
location = location,
isLive = true,
assetType = lastLocation.assetType,
)
}.toImmutableList()
liveShares
.sortedWith(comparator)
.mapNotNull { share ->
val lastLocation = share.lastLocation ?: return@mapNotNull null
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
val member = members.find { it.userId == share.userId }
val displayName = member?.getBestName() ?: share.userId.value
val avatarUrl = member?.avatarUrl
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
val formattedTimestamp = stringProvider.getString(
CommonStrings.screen_static_location_sheet_timestamp_description,
relativeTime
)
LocationShareItem(
userId = share.userId,
displayName = displayName,
avatarData = AvatarData(
id = share.userId.value,
name = displayName,
url = avatarUrl,
size = AvatarSize.UserListItem,
),
formattedTimestamp = formattedTimestamp,
location = location,
isLive = true,
assetType = lastLocation.assetType,
)
}
.toImmutableList()
}.collect { value = it }
}.value
}

View file

@ -0,0 +1,69 @@
/*
* 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.features.location.impl.show
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
import org.junit.Test
class LiveLocationShareComparatorTest {
private val currentUser = UserId("@me:matrix.org")
private val comparator = LiveLocationShareComparator(currentUser)
@Test
fun `compare returns zero when comparing the same current user share`() {
val share = aLiveLocationShare(userId = currentUser, startTimestamp = 123L)
val result = comparator.compare(share, share)
assertThat(result).isEqualTo(0)
}
@Test
fun `compare orders current user share before another user share`() {
val otherShare = aLiveLocationShare(userId = UserId("@alice:matrix.org"), startTimestamp = 200L)
val currentUserShare = aLiveLocationShare(userId = currentUser, startTimestamp = 100L)
val sortedShares = listOf(otherShare, currentUserShare).sortedWith(comparator)
assertThat(sortedShares).containsExactly(currentUserShare, otherShare).inOrder()
}
@Test
fun `compare orders current user shares by newest start timestamp first`() {
val newerShare = aLiveLocationShare(userId = currentUser, startTimestamp = 200L)
val olderShare = aLiveLocationShare(userId = currentUser, startTimestamp = 100L)
val sortedShares = listOf(olderShare, newerShare).sortedWith(comparator)
assertThat(sortedShares).containsExactly(newerShare, olderShare).inOrder()
}
@Test
fun `compare orders non current user shares by newest start timestamp first`() {
val newerShare = aLiveLocationShare(userId = UserId("@alice:matrix.org"), startTimestamp = 200L)
val olderShare = aLiveLocationShare(userId = UserId("@bob:matrix.org"), startTimestamp = 100L)
val sortedShares = listOf(olderShare, newerShare).sortedWith(comparator)
assertThat(sortedShares).containsExactly(newerShare, olderShare).inOrder()
}
}
private fun aLiveLocationShare(
userId: UserId,
startTimestamp: Long,
): LiveLocationShare {
return LiveLocationShare(
userId = userId,
lastLocation = null,
startTimestamp = startTimestamp,
endTimestamp = startTimestamp + 1_000L,
)
}

View file

@ -469,6 +469,7 @@ private fun aLiveLocationShare(
userId: UserId,
geoUri: String = "geo:48.8584,2.2945",
timestamp: Long = 0L,
startTimestamp: Long = 0L,
endTimestamp: Long = Long.MAX_VALUE,
assetType: AssetType = AssetType.SENDER,
): LiveLocationShare {
@ -479,6 +480,7 @@ private fun aLiveLocationShare(
timestamp = timestamp,
assetType = assetType,
),
startTimestamp = startTimestamp,
endTimestamp = endTimestamp,
)
}