Alias permalink navigation - WIP

This commit is contained in:
Benoit Marty
2024-04-16 12:20:25 +02:00
committed by Benoit Marty
parent a602849ec5
commit c1188ebb2d
12 changed files with 137 additions and 68 deletions

View File

@@ -66,6 +66,8 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.appnavstate.api.AppNavigationStateService
@@ -190,7 +192,7 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data class Room(
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(null)
) : NavTarget
@@ -229,7 +231,7 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.RoomList -> {
val callback = object : RoomListEntryPoint.Callback {
override fun onRoomClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
override fun onSettingsClicked() {
@@ -245,7 +247,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onRoomSettingsClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
}
override fun onReportBugClicked() {
@@ -264,7 +266,7 @@ class LoggedInFlowNode @AssistedInject constructor(
is NavTarget.Room -> {
val callback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
override fun onForwardedToSingleRoom(roomId: RoomId) {
@@ -279,20 +281,23 @@ class LoggedInFlowNode @AssistedInject constructor(
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
}
is PermalinkData.RoomIdLink -> {
backstack.push(NavTarget.Room(data.roomId))
backstack.push(NavTarget.Room(data.roomId.toRoomIdOrAlias()))
}
is PermalinkData.RoomAliasLink -> {
// FIXME Implement room alias navigation
Timber.e("Room alias link clicked: ${data.roomAlias}. TODO Handle a room alias navigation")
backstack.push(NavTarget.Room(data.roomAlias.toRoomIdOrAlias()))
}
is PermalinkData.EventIdAliasLink -> {
// FIXME Implement event alias navigation
Timber.e("Event with room alias link clicked: ${data.eventId}. TODO Handle an event with room alias navigation")
backstack.push(
NavTarget.Room(
data.roomAlias.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Messages(data.eventId)
)
)
}
is PermalinkData.EventIdLink -> {
backstack.push(
NavTarget.Room(
data.roomId,
data.roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Messages(data.eventId)
)
)
@@ -310,7 +315,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
val inputs = RoomFlowNode.Inputs(
roomId = navTarget.roomId,
roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription),
initialElement = navTarget.initialElement
)
@@ -327,7 +332,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
@@ -339,7 +344,7 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.CreateRoom -> {
val callback = object : CreateRoomEntryPoint.Callback {
override fun onSuccess(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomId))
backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
@@ -366,11 +371,11 @@ class LoggedInFlowNode @AssistedInject constructor(
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
.callback(object : RoomDirectoryEntryPoint.Callback {
override fun onRoomJoined(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
override fun onResultClicked(roomDescription: RoomDescription) {
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription))
}
})
.build()
@@ -389,7 +394,7 @@ class LoggedInFlowNode @AssistedInject constructor(
if (!canShowRoomList()) return
attachChild<RoomFlowNode> {
backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.Room(roomId))
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}

View File

@@ -47,8 +47,10 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -73,7 +75,7 @@ class RoomFlowNode @AssistedInject constructor(
plugins = plugins
) {
data class Inputs(
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
) : NodeInputs
@@ -88,42 +90,45 @@ class RoomFlowNode @AssistedInject constructor(
data object JoinRoom : NavTarget
@Parcelize
data object JoinedRoom : NavTarget
data class JoinedRoom(val roomId: RoomId) : NavTarget
}
private var roomMembershipJob: Job? = null
override fun onBuilt() {
super.onBuilt()
client.getRoomInfoFlow(
inputs.roomId
inputs.roomIdOrAlias
).onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
if (roomInfo.getOrNull()?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom)
val info = roomInfo.getOrNull()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom(info.id))
// When leaving the room from this session only, navigate up.
roomMembershipJob?.cancel()
roomMembershipJob = roomMembershipObserver.updates
.filter { update -> update.roomId == info.id && !update.isUserInRoom }
.onEach {
navigateUp()
}
.launchIn(lifecycleScope)
} else {
backstack.newRoot(NavTarget.JoinRoom)
}
}
.launchIn(lifecycleScope)
// When leaving the room from this session only, navigate up.
roomMembershipObserver.updates
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
.onEach {
navigateUp()
}
.launchIn(lifecycleScope)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomIdOrAlias, roomDescription = inputs.roomDescription)
joinRoomEntryPoint.createNode(this, buildContext, inputs)
}
NavTarget.JoinedRoom -> {
is NavTarget.JoinedRoom -> {
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement)
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
}
}

View File

@@ -21,14 +21,14 @@ import com.bumble.appyx.core.node.Node
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import java.util.Optional
interface JoinRoomEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
data class Inputs(
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>,
) : NodeInputs
}

View File

@@ -37,7 +37,7 @@ class JoinRoomNode @AssistedInject constructor(
private val acceptDeclineInviteView: AcceptDeclineInviteView,
) : Node(buildContext, plugins = plugins) {
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription)
private val presenter = presenterFactory.create(inputs.roomIdOrAlias, inputs.roomDescription)
@Composable
override fun View(modifier: Modifier) {

View File

@@ -30,7 +30,7 @@ import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@@ -38,20 +38,20 @@ import kotlinx.coroutines.launch
import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> {
interface Factory {
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
}
@Composable
override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope()
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) {
val roomInfo by matrixClient.getRoomInfoFlow(roomIdOrAlias).collectAsState(initial = Optional.empty())
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomIdOrAlias), key1 = roomInfo) {
value = when {
roomInfo.isPresent -> {
roomInfo.get().toContentState()
@@ -61,12 +61,12 @@ class JoinRoomPresenter @AssistedInject constructor(
}
else -> {
coroutineScope.launch {
val result = matrixClient.getRoomPreview(roomId.value)
val result = matrixClient.getRoomPreview(roomIdOrAlias)
value = result.getOrNull()
?.toContentState()
?: ContentState.UnknownRoom(roomId)
?: ContentState.UnknownRoom(roomIdOrAlias)
}
ContentState.Loading(roomId)
ContentState.Loading(roomIdOrAlias)
}
}
}

View File

@@ -22,6 +22,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@Immutable
data class JoinRoomState(
@@ -36,8 +37,8 @@ data class JoinRoomState(
}
sealed interface ContentState {
data class Loading(val roomId: RoomId) : ContentState
data class UnknownRoom(val roomId: RoomId) : ContentState
data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState
data class Loaded(
val roomId: RoomId,
val name: String?,

View File

@@ -21,6 +21,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState>
@@ -43,9 +44,9 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
)
}
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId)
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId)
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
fun aLoadedContentState(
roomId: RoomId = A_ROOM_ID,

View File

@@ -25,7 +25,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import java.util.Optional
@Module
@@ -37,9 +37,9 @@ object JoinRoomModule {
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory {
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
override fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
return JoinRoomPresenter(
roomId = roomId,
roomIdOrAlias = roomIdOrAlias,
roomDescription = roomDescription,
matrixClient = client,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,

View File

@@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
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.createroom.CreateRoomParameters
@@ -94,12 +95,12 @@ interface MatrixClient : Closeable {
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>>
fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>>
fun isMe(userId: UserId?) = userId == sessionId
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId>
suspend fun getRoomPreview(roomIdOrAlias: String): Result<RoomPreview>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview>
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 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.core
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed interface RoomIdOrAlias : Parcelable {
@Parcelize
@JvmInline
value class Id(val roomId: RoomId) : RoomIdOrAlias
@Parcelize
@JvmInline
value class Alias(val roomAlias: RoomAlias) : RoomIdOrAlias
val identifier: String
get() = when (this) {
is Id -> roomId.value
is Alias -> roomAlias.value
}
}
fun RoomId.toRoomIdOrAlias() = RoomIdOrAlias.Id(this)
fun RoomAlias.toRoomIdOrAlias() = RoomIdOrAlias.Alias(this)

View File

@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
@@ -469,9 +470,9 @@ class RustMatrixClient(
}
}
override suspend fun getRoomPreview(roomIdOrAlias: String): Result<RoomPreview> = withContext(sessionDispatcher) {
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching {
client.getRoomPreview(roomIdOrAlias).let(RoomPreviewMapper::map)
client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
}
}
@@ -561,18 +562,33 @@ class RustMatrixClient(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> {
override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> {
return flow {
var room = getRoom(roomId)
if (room == null) {
emit(Optional.empty())
awaitRoom(roomId, INFINITE)
room = getRoom(roomId)
val roomId = when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
resolveRoomAlias(roomIdOrAlias.roomAlias)
.onFailure {
// TODO Get a way to emit an error
Timber.e("Unable to resolve room alias ${roomIdOrAlias.roomAlias}")
emit(Optional.empty())
return@flow
}
.getOrNull()
}
is RoomIdOrAlias.Id -> roomIdOrAlias.roomId
}
room?.use {
room.roomInfoFlow
.map { roomInfo -> Optional.of(roomInfo) }
.collect(this)
if (roomId != null) {
var room = getRoom(roomId)
if (room == null) {
emit(Optional.empty())
awaitRoom(roomId, INFINITE)
room = getRoom(roomId)
}
room?.use {
room.roomInfoFlow
.map { roomInfo -> Optional.of(roomInfo) }
.collect(this)
}
}
}.distinctUntilChanged()
}

View File

@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
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.createroom.CreateRoomParameters
@@ -76,7 +77,7 @@ class FakeMatrixClient(
private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
private val accountManagementUrlString: Result<String?> = Result.success(null),
private val resolveRoomAliasResult: (RoomAlias) -> Result<RoomId> = { Result.success(A_ROOM_ID) },
private val getRoomPreviewResult: (String) -> Result<RoomPreview> = { TODO("Not implemented") },
private val getRoomPreviewResult: (RoomIdOrAlias) -> Result<RoomPreview> = { TODO("Not implemented") },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@@ -106,7 +107,7 @@ class FakeMatrixClient(
Result.success(it)
}
var getRoomInfoFlowLambda = { _: RoomId ->
var getRoomInfoFlowLambda = { _: RoomIdOrAlias ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
}
@@ -284,7 +285,7 @@ class FakeMatrixClient(
return resolveRoomAliasResult(roomAlias)
}
override suspend fun getRoomPreview(roomIdOrAlias: String): Result<RoomPreview> {
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> {
return getRoomPreviewResult(roomIdOrAlias)
}
@@ -292,5 +293,5 @@ class FakeMatrixClient(
return Result.success(visitedRoomsId)
}
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomInfoFlowLambda(roomIdOrAlias)
}