Merge branch 'develop' into feature/fga/space_list_join_action
This commit is contained in:
commit
bb5a4f4954
70 changed files with 824 additions and 385 deletions
|
|
@ -244,7 +244,9 @@ interface BaseRoom : Closeable {
|
|||
|
||||
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
|
||||
|
||||
/**
|
||||
suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?>
|
||||
|
||||
/**
|
||||
* Destroy the room and release all resources associated to it.
|
||||
*/
|
||||
fun destroy()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.Optional
|
||||
|
|
@ -17,9 +18,13 @@ interface SpaceRoomList {
|
|||
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
|
||||
}
|
||||
|
||||
val roomId: RoomId
|
||||
|
||||
val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
|
||||
|
||||
val spaceRoomsFlow: Flow<List<SpaceRoom>>
|
||||
val paginationStatusFlow: StateFlow<PaginationStatus>
|
||||
suspend fun paginate(): Result<Unit>
|
||||
|
||||
fun destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,14 @@ package io.element.android.libraries.matrix.api.verification
|
|||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class SessionVerificationRequestDetails(
|
||||
val senderProfile: SenderProfile,
|
||||
val senderProfile: MatrixUser,
|
||||
val flowId: FlowId,
|
||||
val deviceId: DeviceId,
|
||||
val deviceDisplayName: String?,
|
||||
val firstSeenTimestamp: Long,
|
||||
) : Parcelable {
|
||||
@Parcelize
|
||||
data class SenderProfile(
|
||||
val userId: UserId,
|
||||
val displayName: String?,
|
||||
val avatarUrl: String?,
|
||||
) : Parcelable
|
||||
}
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
|
|
@ -75,7 +76,6 @@ import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
|||
import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import io.element.android.libraries.matrix.impl.sync.map
|
||||
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
||||
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
|
|
@ -403,7 +403,7 @@ class RustMatrixClient(
|
|||
|
||||
override suspend fun getProfile(userId: UserId): Result<MatrixUser> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.getProfile(userId.value).let(UserProfileMapper::map)
|
||||
innerClient.getProfile(userId.value).map()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,14 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import org.matrix.rustcomponents.sdk.UserProfile
|
||||
|
||||
object UserProfileMapper {
|
||||
fun map(userProfile: UserProfile): MatrixUser =
|
||||
MatrixUser(
|
||||
userId = UserId(userProfile.userId),
|
||||
displayName = userProfile.displayName,
|
||||
avatarUrl = userProfile.avatarUrl,
|
||||
)
|
||||
}
|
||||
fun UserProfile.map() = MatrixUser(
|
||||
userId = UserId(userId),
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
|
@ -322,4 +322,12 @@ class RustBaseRoom(
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.loadOrFetchEvent(eventId.value).use {
|
||||
it.threadRootEventId()?.let(::ThreadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,26 +8,31 @@
|
|||
package io.element.android.libraries.matrix.impl.spaces
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
|
||||
import java.util.Optional
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
|
||||
|
||||
class RustSpaceRoomList(
|
||||
override val roomId: RoomId,
|
||||
private val innerProvider: suspend () -> InnerSpaceRoomList,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
spaceRoomMapper: SpaceRoomMapper,
|
||||
) : SpaceRoomList {
|
||||
private val inner = CompletableDeferred<InnerSpaceRoomList>()
|
||||
private val innerCompletable = CompletableDeferred<InnerSpaceRoomList>()
|
||||
|
||||
override val currentSpaceFlow = MutableStateFlow<Optional<SpaceRoom>>(Optional.empty())
|
||||
|
||||
|
|
@ -41,37 +46,45 @@ class RustSpaceRoomList(
|
|||
)
|
||||
|
||||
init {
|
||||
sessionCoroutineScope.launch {
|
||||
inner.complete(innerProvider())
|
||||
}
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().paginationStateFlow()
|
||||
coroutineScope.launch {
|
||||
val inner = innerProvider()
|
||||
innerCompletable.complete(inner)
|
||||
|
||||
inner.paginationStateFlow()
|
||||
.onEach { paginationStatus ->
|
||||
paginationStatusFlow.emit(paginationStatus.into())
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().spaceListUpdateFlow()
|
||||
inner.spaceListUpdateFlow()
|
||||
.onEach { updates ->
|
||||
spaceListUpdateProcessor.postUpdates(updates)
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
sessionCoroutineScope.launch {
|
||||
inner.await().spaceUpdateFlow()
|
||||
.launchIn(this)
|
||||
|
||||
inner.spaceUpdateFlow()
|
||||
.map { space -> space.map(spaceRoomMapper::map) }
|
||||
.onEach { space ->
|
||||
currentSpaceFlow.emit(space)
|
||||
}
|
||||
.collect()
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun paginate(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
inner.await().paginate()
|
||||
innerCompletable.await().paginate()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun destroy() {
|
||||
Timber.d("Destroying SpaceRoomList $roomId")
|
||||
coroutineScope.cancel()
|
||||
try {
|
||||
innerCompletable.getCompleted().destroy()
|
||||
} catch (_: Exception) {
|
||||
// Ignore, we just want to make sure it's completed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.spaces
|
||||
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
|
|
@ -54,9 +55,11 @@ class RustSpaceService(
|
|||
}
|
||||
|
||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
||||
return RustSpaceRoomList(
|
||||
roomId = id,
|
||||
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
coroutineScope = childCoroutineScope,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@
|
|||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.SearchUsersResults
|
||||
|
||||
object UserSearchResultMapper {
|
||||
fun map(result: SearchUsersResults): MatrixSearchUserResults {
|
||||
return MatrixSearchUserResults(
|
||||
results = result.results.map(UserProfileMapper::map).toImmutableList(),
|
||||
results = result.results
|
||||
.map { userProfile -> userProfile.map() }
|
||||
.toImmutableList(),
|
||||
limited = result.limited,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,17 @@ import io.element.android.libraries.matrix.api.core.FlowId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
|
||||
import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile
|
||||
|
||||
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
|
||||
senderProfile = senderProfile.map(),
|
||||
flowId = FlowId(flowId),
|
||||
deviceId = DeviceId(deviceId),
|
||||
deviceDisplayName = deviceDisplayName,
|
||||
firstSeenTimestamp = firstSeenTimestamp.toLong(),
|
||||
)
|
||||
|
||||
fun RustUserProfile.map() = SessionVerificationRequestDetails.SenderProfile(
|
||||
userId = UserId(userId),
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
fun RustSessionVerificationRequestDetails.toVerificationRequest(currentUserId: UserId): VerificationRequest.Incoming {
|
||||
val details = map()
|
||||
return if (currentUserId == details.senderProfile.userId) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.usersearch
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -16,7 +16,7 @@ import org.junit.Test
|
|||
class UserProfileMapperTest {
|
||||
@Test
|
||||
fun map() {
|
||||
assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")))
|
||||
assertThat(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl").map())
|
||||
.isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl"))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,11 @@ package io.element.android.libraries.matrix.impl.spaces
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -84,13 +86,15 @@ class RustSpaceRoomListTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createRustSpaceRoomList(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
|
||||
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
|
||||
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
|
||||
): RustSpaceRoomList {
|
||||
return RustSpaceRoomList(
|
||||
roomId = roomId,
|
||||
innerProvider = innerProvider,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
coroutineScope = backgroundScope,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class FakeBaseRoom(
|
|||
private val forgetResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val reportRoomResult: (String?) -> Result<Unit> = { lambdaError() },
|
||||
private val predecessorRoomResult: () -> PredecessorRoom? = { null },
|
||||
private val threadRootIdForEventResult: (EventId) -> Result<ThreadId?> = { lambdaError() },
|
||||
) : BaseRoom {
|
||||
private val _roomInfoFlow: MutableStateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
|
||||
override val roomInfoFlow: StateFlow<RoomInfo> = _roomInfoFlow
|
||||
|
|
@ -244,6 +245,10 @@ class FakeBaseRoom(
|
|||
fun givenUpdateMembersResult(result: () -> Unit) {
|
||||
updateMembersResult = result
|
||||
}
|
||||
|
||||
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> {
|
||||
return threadRootIdForEventResult(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues(
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -18,6 +20,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import java.util.Optional
|
||||
|
||||
class FakeSpaceRoomList(
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
initialSpaceFlowValue: SpaceRoom? = null,
|
||||
initialSpaceRoomsValue: List<SpaceRoom> = emptyList(),
|
||||
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
|
||||
|
|
@ -47,4 +50,8 @@ class FakeSpaceRoomList(
|
|||
override suspend fun paginate(): Result<Unit> = simulateLongTask {
|
||||
paginateResult()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
libraries/ui-common/build.gradle.kts
Normal file
19
libraries/ui-common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.ui.common"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.appyx.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.ui.common.nodes
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.node
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323
|
||||
*/
|
||||
fun emptyNode(
|
||||
buildContext: BuildContext,
|
||||
): Node = node(buildContext) { modifier ->
|
||||
EmptyView(modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyView(
|
||||
modifier: Modifier = Modifier,
|
||||
) = Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(ElementTheme.colors.bgCanvasDefault),
|
||||
)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun EmptyViewPreview() = ElementPreview {
|
||||
EmptyView(Modifier)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue