Merge pull request #2713 from element-hq/feature/bma/roomPreview

Permalink navigation - Navigation to rooms and users
This commit is contained in:
Benoit Marty 2024-04-18 11:30:15 +02:00 committed by GitHub
commit 878a435d1b
112 changed files with 1884 additions and 340 deletions

View file

@ -66,6 +66,9 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient 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.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId 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.libraries.matrix.api.sync.SyncState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -189,9 +192,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class Room( data class Room(
val roomId: RoomId, val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: RoomDescription? = null, val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget ) : NavTarget
@Parcelize @Parcelize
@ -228,7 +231,7 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.RoomList -> { NavTarget.RoomList -> {
val callback = object : RoomListEntryPoint.Callback { val callback = object : RoomListEntryPoint.Callback {
override fun onRoomClicked(roomId: RoomId) { override fun onRoomClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
override fun onSettingsClicked() { override fun onSettingsClicked() {
@ -244,7 +247,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
override fun onRoomSettingsClicked(roomId: RoomId) { 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() { override fun onReportBugClicked() {
@ -263,19 +266,41 @@ class LoggedInFlowNode @AssistedInject constructor(
is NavTarget.Room -> { is NavTarget.Room -> {
val callback = object : JoinedRoomLoadedFlowNode.Callback { val callback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId) { override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) } coroutineScope.launch { attachRoom(roomId) }
} }
override fun onPermalinkClicked(data: PermalinkData) {
when (data) {
is PermalinkData.UserLink -> {
// FIXME Add a user profile screen.
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
}
is PermalinkData.RoomLink -> {
backstack.push(
NavTarget.Room(
data.roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(data.eventId),
// TODO Use the viaParameters
)
)
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
// Should not happen (handled by MessagesNode)
}
}
}
override fun onOpenGlobalNotificationSettings() { override fun onOpenGlobalNotificationSettings() {
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
} }
} }
val inputs = RoomFlowNode.Inputs( val inputs = RoomFlowNode.Inputs(
roomId = navTarget.roomId, roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription), roomDescription = Optional.ofNullable(navTarget.roomDescription),
initialElement = navTarget.initialElement initialElement = navTarget.initialElement
) )
@ -292,7 +317,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
override fun onOpenRoomNotificationSettings(roomId: RoomId) { 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) val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
@ -304,7 +329,7 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.CreateRoom -> { NavTarget.CreateRoom -> {
val callback = object : CreateRoomEntryPoint.Callback { val callback = object : CreateRoomEntryPoint.Callback {
override fun onSuccess(roomId: RoomId) { override fun onSuccess(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomId)) backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
} }
@ -331,11 +356,11 @@ class LoggedInFlowNode @AssistedInject constructor(
roomDirectoryEntryPoint.nodeBuilder(this, buildContext) roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
.callback(object : RoomDirectoryEntryPoint.Callback { .callback(object : RoomDirectoryEntryPoint.Callback {
override fun onRoomJoined(roomId: RoomId) { override fun onRoomJoined(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
override fun onResultClicked(roomDescription: RoomDescription) { override fun onResultClicked(roomDescription: RoomDescription) {
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription)) backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription))
} }
}) })
.build() .build()
@ -354,7 +379,7 @@ class LoggedInFlowNode @AssistedInject constructor(
if (!canShowRoomList()) return if (!canShowRoomList()) return
attachChild<RoomFlowNode> { attachChild<RoomFlowNode> {
backstack.singleTop(NavTarget.RoomList) backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
} }

View file

@ -37,6 +37,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
@ -46,12 +47,15 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
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.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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.Optional import java.util.Optional
@ -64,6 +68,7 @@ class RoomFlowNode @AssistedInject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val joinRoomEntryPoint: JoinRoomEntryPoint, private val joinRoomEntryPoint: JoinRoomEntryPoint,
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>( ) : BaseFlowNode<RoomFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Loading, initialElement = NavTarget.Loading,
@ -73,9 +78,9 @@ class RoomFlowNode @AssistedInject constructor(
plugins = plugins plugins = plugins
) { ) {
data class Inputs( data class Inputs(
val roomId: RoomId, val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>, val roomDescription: Optional<RoomDescription>,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
@ -85,29 +90,51 @@ class RoomFlowNode @AssistedInject constructor(
data object Loading : NavTarget data object Loading : NavTarget
@Parcelize @Parcelize
data object JoinRoom : NavTarget data class Resolving(val roomAlias: RoomAlias) : NavTarget
@Parcelize @Parcelize
data object JoinedRoom : NavTarget data class JoinRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget
} }
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
client.getRoomInfoFlow( resolveRoomId()
inputs.roomId }
).onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}") private fun resolveRoomId() {
if (roomInfo.getOrNull()?.currentUserMembership == CurrentUserMembership.JOINED) { lifecycleScope.launch {
backstack.newRoot(NavTarget.JoinedRoom) when (val i = inputs.roomIdOrAlias) {
} else { is RoomIdOrAlias.Alias -> {
backstack.newRoot(NavTarget.JoinRoom) backstack.newRoot(NavTarget.Resolving(i.roomAlias))
}
is RoomIdOrAlias.Id -> {
subscribeToRoomInfoFlow(i.roomId)
}
} }
} }
}
private fun subscribeToRoomInfoFlow(roomId: RoomId) {
client.getRoomInfoFlow(
roomId = roomId
)
.onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
val info = roomInfo.getOrNull()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom(roomId))
} else {
backstack.newRoot(NavTarget.JoinRoom(roomId))
}
}
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
// When leaving the room from this session only, navigate up. // When leaving the room from this session only, navigate up.
roomMembershipObserver.updates roomMembershipObserver.updates
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom } .filter { update -> update.roomId == roomId && !update.isUserInRoom }
.onEach { .onEach {
navigateUp() navigateUp()
} }
@ -116,14 +143,30 @@ class RoomFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Loading -> loadingNode(buildContext) is NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> { is NavTarget.Resolving -> {
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription) val callback = object : RoomAliasResolverEntryPoint.Callback {
override fun onAliasResolved(roomId: RoomId) {
subscribeToRoomInfoFlow(roomId)
}
}
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)
.params(params)
.build()
}
is NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(
roomId = navTarget.roomId,
roomIdOrAlias = inputs.roomIdOrAlias,
roomDescription = inputs.roomDescription,
)
joinRoomEntryPoint.createNode(this, buildContext, inputs) joinRoomEntryPoint.createNode(this, buildContext, inputs)
} }
NavTarget.JoinedRoom -> { is NavTarget.JoinedRoom -> {
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>() 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) createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} }
} }

View file

@ -16,8 +16,17 @@
package io.element.android.appnav.room package io.element.android.appnav.room
enum class RoomNavigationTarget { import android.os.Parcelable
Messages, import io.element.android.libraries.matrix.api.core.EventId
Details, import kotlinx.parcelize.Parcelize
NotificationSettings,
sealed interface RoomNavigationTarget : Parcelable {
@Parcelize
data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget
@Parcelize
data object Details : RoomNavigationTarget
@Parcelize
data object NotificationSettings : RoomNavigationTarget
} }

View file

@ -69,7 +69,7 @@ class JoinedRoomFlowNode @AssistedInject constructor(
) { ) {
data class Inputs( data class Inputs(
val roomId: RoomId, val roomId: RoomId,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()

View file

@ -42,8 +42,10 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -63,8 +65,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
roomComponentFactory: RoomComponentFactory, roomComponentFactory: RoomComponentFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>( ) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) { initialElement = when (val input = plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
RoomNavigationTarget.Messages -> NavTarget.Messages is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
RoomNavigationTarget.Details -> NavTarget.RoomDetails RoomNavigationTarget.Details -> NavTarget.RoomDetails
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
}, },
@ -75,13 +77,14 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
), DaggerComponentOwner { ), DaggerComponentOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId)
fun onPermalinkClicked(data: PermalinkData)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)
fun onOpenGlobalNotificationSettings() fun onOpenGlobalNotificationSettings()
} }
data class Inputs( data class Inputs(
val room: MatrixRoom, val room: MatrixRoom,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
@ -139,7 +142,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Messages -> { is NavTarget.Messages -> {
val callback = object : MessagesEntryPoint.Callback { val callback = object : MessagesEntryPoint.Callback {
override fun onRoomDetailsClicked() { override fun onRoomDetailsClicked() {
backstack.push(NavTarget.RoomDetails) backstack.push(NavTarget.RoomDetails)
@ -149,11 +152,18 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
backstack.push(NavTarget.RoomMemberDetails(userId)) backstack.push(NavTarget.RoomMemberDetails(userId))
} }
override fun onPermalinkClicked(data: PermalinkData) {
callbacks.forEach { it.onPermalinkClicked(data) }
}
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {
callbacks.forEach { it.onForwardedToSingleRoom(roomId) } callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
} }
} }
messagesEntryPoint.createNode(this, buildContext, callback) messagesEntryPoint.nodeBuilder(this, buildContext)
.params(MessagesEntryPoint.Params(navTarget.focusedEventId))
.callback(callback)
.build()
} }
NavTarget.RoomDetails -> { NavTarget.RoomDetails -> {
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails) createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails)
@ -169,7 +179,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Messages : NavTarget data class Messages(val focusedEventId: EventId? = null) : NavTarget
@Parcelize @Parcelize
data object RoomDetails : NavTarget data object RoomDetails : NavTarget

View file

@ -47,14 +47,30 @@ class JoinRoomLoadedFlowNodeTest {
@get:Rule @get:Rule
val mainDispatcherRule = MainDispatcherRule() val mainDispatcherRule = MainDispatcherRule()
private class FakeMessagesEntryPoint : MessagesEntryPoint { private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder {
var buildContext: BuildContext? = null
var nodeId: String? = null var nodeId: String? = null
var parameters: MessagesEntryPoint.Params? = null
var callback: MessagesEntryPoint.Callback? = null var callback: MessagesEntryPoint.Callback? = null
override fun createNode(parentNode: Node, buildContext: BuildContext, callback: MessagesEntryPoint.Callback): Node { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
return node(buildContext) {}.also { this.buildContext = buildContext
return this
}
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
parameters = params
return this
}
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
this.callback = callback
return this
}
override fun build(): Node {
return node(buildContext!!) {}.also {
nodeId = it.id nodeId = it.id
this.callback = callback
} }
} }
} }
@ -118,9 +134,9 @@ class JoinRoomLoadedFlowNodeTest {
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// THEN // THEN
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages) assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages())
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(), Lifecycle.State.CREATED)
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!! val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages())!!
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
} }

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
@ -29,6 +28,7 @@ import io.element.android.features.invite.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@ -102,9 +102,9 @@ private fun DeclineConfirmationDialog(
) )
} }
@PreviewLightDark @PreviewsDayNight
@Composable @Composable
internal fun AcceptDeclineInviteViewLightPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) = internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
ElementPreview { ElementPreview {
AcceptDeclineInviteView( AcceptDeclineInviteView(
state = state, state = state,

View file

@ -22,6 +22,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import java.util.Optional import java.util.Optional
interface JoinRoomEntryPoint : FeatureEntryPoint { interface JoinRoomEntryPoint : FeatureEntryPoint {
@ -29,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
data class Inputs( data class Inputs(
val roomId: RoomId, val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>, val roomDescription: Optional<RoomDescription>,
) : NodeInputs ) : NodeInputs
} }

View file

@ -17,6 +17,7 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents { sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents data object JoinRoom : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents data object DeclineInvite : JoinRoomEvents

View file

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

View file

@ -20,7 +20,10 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
@ -30,33 +33,57 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient 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.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.room.CurrentUserMembership 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.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import java.util.Optional import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor( class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId, @Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>, @Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
interface Factory { interface Factory {
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter fun create(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
): JoinRoomPresenter
} }
@Composable @Composable
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) { val contentState by produceState<ContentState>(
value = when { initialValue = ContentState.Loading(roomIdOrAlias),
key1 = roomInfo,
key2 = retryCount,
) {
when {
roomInfo.isPresent -> { roomInfo.isPresent -> {
roomInfo.get().toContentState() value = roomInfo.get().toContentState()
} }
roomDescription.isPresent -> { roomDescription.isPresent -> {
roomDescription.get().toContentState() value = roomDescription.get().toContentState()
} }
else -> { else -> {
ContentState.Loading(roomId) value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
value = result.fold(
onSuccess = { it.toContentState() },
onFailure = { throwable ->
if (throwable.message?.contains("403") == true) {
ContentState.UnknownRoom(roomIdOrAlias)
} else {
ContentState.Failure(roomIdOrAlias, throwable)
}
}
)
} }
} }
} }
@ -64,7 +91,8 @@ class JoinRoomPresenter @AssistedInject constructor(
fun handleEvents(event: JoinRoomEvents) { fun handleEvents(event: JoinRoomEvents) {
when (event) { when (event) {
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { JoinRoomEvents.AcceptInvite,
JoinRoomEvents.JoinRoom -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData) AcceptDeclineInviteEvents.AcceptInvite(inviteData)
@ -76,6 +104,9 @@ class JoinRoomPresenter @AssistedInject constructor(
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
JoinRoomEvents.RetryFetchingContent -> {
retryCount++
}
} }
} }
@ -87,6 +118,24 @@ class JoinRoomPresenter @AssistedInject constructor(
} }
} }
private fun RoomPreview.toContentState(): ContentState {
return ContentState.Loaded(
roomId = roomId,
name = name,
topic = topic,
alias = canonicalAlias,
numberOfMembers = numberOfJoinedMembers,
isDirect = false,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when {
isInvited -> JoinAuthorisationStatus.IsInvited
canKnock -> JoinAuthorisationStatus.CanKnock
isPublic -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown
}
)
}
@VisibleForTesting @VisibleForTesting
internal fun RoomDescription.toContentState(): ContentState { internal fun RoomDescription.toContentState(): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
@ -108,7 +157,7 @@ internal fun RoomDescription.toContentState(): ContentState {
@VisibleForTesting @VisibleForTesting
internal fun MatrixRoomInfo.toContentState(): ContentState { internal fun MatrixRoomInfo.toContentState(): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
roomId = RoomId(id), roomId = id,
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,

View file

@ -20,7 +20,9 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@Immutable @Immutable
data class JoinRoomState( data class JoinRoomState(
@ -35,13 +37,14 @@ data class JoinRoomState(
} }
sealed interface ContentState { sealed interface ContentState {
data class Loading(val roomId: RoomId) : ContentState data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState
data class UnknownRoom(val roomId: RoomId) : ContentState data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState
data class Loaded( data class Loaded(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
val topic: String?, val topic: String?,
val alias: String?, val alias: RoomAlias?,
val numberOfMembers: Long?, val numberOfMembers: Long?,
val isDirect: Boolean, val isDirect: Boolean,
val roomAvatarUrl: String?, val roomAvatarUrl: String?,
@ -50,7 +53,7 @@ sealed interface ContentState {
val computedTitle = name ?: roomId.value val computedTitle = name ?: roomId.value
val computedSubtitle = when { val computedSubtitle = when {
alias != null -> alias alias != null -> alias.value
name == null -> "" name == null -> ""
else -> roomId.value else -> roomId.value
} }

View file

@ -19,7 +19,10 @@ package io.element.android.features.joinroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState 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.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState> override val values: Sequence<JoinRoomState>
@ -34,22 +37,45 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
), ),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock) contentState = aLoadedContentState(
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
numberOfMembers = 888,
)
), ),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
), ),
aJoinRoomState(
contentState = aFailureContentState()
),
aJoinRoomState(
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
),
) )
} }
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId) fun aFailureContentState(
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias()
): ContentState {
return ContentState.Failure(
roomIdOrAlias = roomIdOrAlias,
error = Exception("Error"),
)
}
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId) fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
fun aLoadedContentState( fun aLoadedContentState(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String = "Element X android", name: String = "Element X android",
alias: String? = "#exa:matrix.org", alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
topic: String? = "Element X is a secure, private and decentralized messenger.", topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null, numberOfMembers: Long? = null,
isDirect: Boolean = false, isDirect: Boolean = false,
@ -77,3 +103,4 @@ fun aJoinRoomState(
) )
private val A_ROOM_ID = RoomId("!exa:matrix.org") private val A_ROOM_ID = RoomId("!exa:matrix.org")
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")

View file

@ -16,42 +16,36 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -71,7 +65,7 @@ fun JoinRoomView(
}, },
footer = { footer = {
JoinRoomFooter( JoinRoomFooter(
joinAuthorisationStatus = state.joinAuthorisationStatus, state = state,
onAcceptInvite = { onAcceptInvite = {
state.eventSink(JoinRoomEvents.AcceptInvite) state.eventSink(JoinRoomEvents.AcceptInvite)
}, },
@ -81,6 +75,9 @@ fun JoinRoomView(
onJoinRoom = { onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom) state.eventSink(JoinRoomEvents.JoinRoom)
}, },
onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
) )
} }
) )
@ -88,46 +85,57 @@ fun JoinRoomView(
@Composable @Composable
private fun JoinRoomFooter( private fun JoinRoomFooter(
joinAuthorisationStatus: JoinAuthorisationStatus, state: JoinRoomState,
onAcceptInvite: () -> Unit, onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit, onDeclineInvite: () -> Unit,
onJoinRoom: () -> Unit, onJoinRoom: () -> Unit,
onRetry: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (joinAuthorisationStatus) { if (state.contentState is ContentState.Failure) {
JoinAuthorisationStatus.IsInvited -> { Button(
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { text = stringResource(CommonStrings.action_retry),
OutlinedButton( onClick = onRetry,
text = stringResource(CommonStrings.action_decline), modifier = modifier.fillMaxWidth(),
onClick = onDeclineInvite, size = ButtonSize.Medium,
modifier = Modifier.weight(1f), )
size = ButtonSize.Medium, } else {
) val joinAuthorisationStatus = state.joinAuthorisationStatus
when (joinAuthorisationStatus) {
JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = onDeclineInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.Medium,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.Medium,
)
}
}
JoinAuthorisationStatus.CanJoin -> {
Button( Button(
text = stringResource(CommonStrings.action_accept), text = stringResource(R.string.screen_join_room_join_action),
onClick = onAcceptInvite, onClick = onJoinRoom,
modifier = Modifier.weight(1f), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Medium,
) )
} }
JoinAuthorisationStatus.CanKnock -> {
Button(
text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium,
)
}
JoinAuthorisationStatus.Unknown -> Unit
} }
JoinAuthorisationStatus.CanJoin -> {
Button(
text = stringResource(R.string.screen_join_room_join_action),
onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium,
)
}
JoinAuthorisationStatus.CanKnock -> {
Button(
text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium,
)
}
JoinAuthorisationStatus.Unknown -> Unit
} }
} }
@ -138,43 +146,43 @@ private fun JoinRoomContent(
) { ) {
when (contentState) { when (contentState) {
is ContentState.Loaded -> { is ContentState.Loaded -> {
ContentScaffold( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
Avatar(contentState.avatarData(AvatarSize.RoomHeader)) Avatar(contentState.avatarData(AvatarSize.RoomHeader))
}, },
title = { title = {
Title(contentState.computedTitle) RoomPreviewTitleAtom(contentState.computedTitle)
}, },
subtitle = { subtitle = {
Subtitle(contentState.computedSubtitle) RoomPreviewSubtitleAtom(contentState.computedSubtitle)
}, },
description = { description = {
Description(contentState.topic ?: "") RoomPreviewDescriptionAtom(contentState.topic ?: "")
}, },
memberCount = { memberCount = {
if (contentState.showMemberCount) { if (contentState.showMemberCount) {
MembersCount(memberCount = contentState.numberOfMembers ?: 0) RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
} }
} }
) )
} }
is ContentState.UnknownRoom -> { is ContentState.UnknownRoom -> {
ContentScaffold( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
}, },
title = { title = {
Title(stringResource(R.string.screen_join_room_title_no_preview)) RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
}, },
subtitle = { subtitle = {
Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview)) RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
}, },
) )
} }
is ContentState.Loading -> { is ContentState.Loading -> {
ContentScaffold( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
@ -187,94 +195,31 @@ private fun JoinRoomContent(
}, },
) )
} }
} is ContentState.Failure -> {
} RoomPreviewOrganism(
modifier = modifier,
@Composable avatar = {
private fun ContentScaffold( PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
avatar: @Composable () -> Unit, },
title: @Composable () -> Unit, title = {
subtitle: @Composable () -> Unit, when (contentState.roomIdOrAlias) {
modifier: Modifier = Modifier, is RoomIdOrAlias.Alias -> {
description: @Composable (() -> Unit)? = null, RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
memberCount: @Composable (() -> Unit)? = null, }
) { is RoomIdOrAlias.Id -> {
Column( PlaceholderAtom(width = 200.dp, height = 22.dp)
modifier = modifier.fillMaxWidth(), }
horizontalAlignment = Alignment.CenterHorizontally }
) { },
avatar() subtitle = {
Spacer(modifier = Modifier.height(16.dp)) Text(
title() text = "Failed to get information about the room",
Spacer(modifier = Modifier.height(8.dp)) textAlign = TextAlign.Center,
subtitle() color = MaterialTheme.colorScheme.error,
Spacer(modifier = Modifier.height(8.dp)) )
if (memberCount != null) { },
memberCount() )
} }
Spacer(modifier = Modifier.height(8.dp))
if (description != null) {
description()
}
Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
private fun Title(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
)
}
@Composable
private fun Subtitle(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
}
@Composable
private fun Description(description: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = description,
style = ElementTheme.typography.fontBodySmRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
@Composable
private fun MembersCount(memberCount: Long) {
Row(
modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
.widthIn(min = 48.dp)
.padding(all = 2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
imageVector = CompoundIcons.UserProfile(),
contentDescription = null,
tint = ElementTheme.colors.iconSecondary,
)
Text(
text = "$memberCount",
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textSecondary,
)
} }
} }
@ -291,7 +236,7 @@ private fun JoinRoomTopBar(
) )
} }
@PreviewLightDark @PreviewsDayNight
@Composable @Composable
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
JoinRoomView( JoinRoomView(

View file

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

View file

@ -23,8 +23,12 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
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.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -49,9 +53,10 @@ class JoinRoomPresenterTest {
val presenter = createJoinRoomPresenter() val presenter = createJoinRoomPresenter()
presenter.test { presenter.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID)) assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
cancelAndIgnoreRemainingEvents()
} }
} }
} }
@ -237,6 +242,110 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
Result.success(
RoomPreview(
roomId = A_ROOM_ID,
canonicalAlias = RoomAlias("#alias:matrix.org"),
name = "Room name",
topic = "Room topic",
avatarUrl = "avatarUrl",
numberOfJoinedMembers = 2,
roomType = null,
isHistoryWorldReadable = false,
isJoined = false,
isInvited = false,
isPublic = true,
canKnock = false,
)
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Loaded(
roomId = A_ROOM_ID,
name = "Room name",
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDirect = false,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
)
)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
Result.failure(AN_EXCEPTION)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
error = AN_EXCEPTION
)
)
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())
)
}
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
error = AN_EXCEPTION
)
)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
Result.failure(Exception("403"))
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.UnknownRoom(
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
)
)
}
}
}
private fun createJoinRoomPresenter( private fun createJoinRoomPresenter(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
roomDescription: Optional<RoomDescription> = Optional.empty(), roomDescription: Optional<RoomDescription> = Optional.empty(),
@ -245,6 +354,7 @@ class JoinRoomPresenterTest {
): JoinRoomPresenter { ): JoinRoomPresenter {
return JoinRoomPresenter( return JoinRoomPresenter(
roomId = roomId, roomId = roomId,
roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription, roomDescription = roomDescription,
matrixClient = matrixClient, matrixClient = matrixClient,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
@ -255,7 +365,7 @@ class JoinRoomPresenterTest {
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME, name: String? = A_ROOM_NAME,
topic: String? = "A room about something", topic: String? = "A room about something",
alias: String? = "#alias:matrix.org", alias: RoomAlias? = RoomAlias("#alias:matrix.org"),
avatarUrl: String? = null, avatarUrl: String? = null,
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers: Long = 2L numberOfMembers: Long = 2L

View file

@ -20,19 +20,28 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
interface MessagesEntryPoint : FeatureEntryPoint { interface MessagesEntryPoint : FeatureEntryPoint {
fun createNode( fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
parentNode: Node,
buildContext: BuildContext, interface NodeBuilder {
callback: Callback, fun params(params: Params): NodeBuilder
): Node fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
data class Params(
val focusedEventId: EventId?,
)
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomDetailsClicked() fun onRoomDetailsClicked()
fun onUserDataClicked(userId: UserId) fun onUserDataClicked(userId: UserId)
fun onPermalinkClicked(data: PermalinkData)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)
} }
} }

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -26,11 +27,23 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint {
override fun createNode( override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
parentNode: Node, val plugins = ArrayList<Plugin>()
buildContext: BuildContext,
callback: MessagesEntryPoint.Callback return object : MessagesEntryPoint.NodeBuilder {
): Node { override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
return parentNode.createNode<MessagesFlowNode>(buildContext, listOf(callback)) plugins += MessagesNode.Inputs(focusedEventId = params.focusedEventId)
return this
}
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<MessagesFlowNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -52,6 +52,7 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.architecture.overlay.operation.show
@ -62,6 +63,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
@ -79,7 +81,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val createPollEntryPoint: CreatePollEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint,
) : BaseFlowNode<MessagesFlowNode.NavTarget>( ) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Messages, initialElement = NavTarget.Messages(plugins.filterIsInstance<Inputs>().firstOrNull()?.focusedEventId),
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
overlay = Overlay( overlay = Overlay(
@ -88,12 +90,16 @@ class MessagesFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
data class Inputs(val focusedEventId: EventId?) : NodeInputs
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Empty : NavTarget data object Empty : NavTarget
@Parcelize @Parcelize
data object Messages : NavTarget data class Messages(
val focusedEventId: EventId? = null,
) : NavTarget
@Parcelize @Parcelize
data class MediaViewer( data class MediaViewer(
@ -149,6 +155,10 @@ class MessagesFlowNode @AssistedInject constructor(
callback?.onUserDataClicked(userId) callback?.onUserDataClicked(userId)
} }
override fun onPermalinkClicked(data: PermalinkData) {
callback?.onPermalinkClicked(data)
}
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
} }
@ -181,7 +191,10 @@ class MessagesFlowNode @AssistedInject constructor(
ElementCallActivity.start(context, inputs) ElementCallActivity.start(context, inputs)
} }
} }
createNode<MessagesNode>(buildContext, listOf(callback)) val params = MessagesNode.Inputs(
focusedEventId = navTarget.focusedEventId,
)
createNode<MessagesNode>(buildContext, listOf(callback, params))
} }
is NavTarget.MediaViewer -> { is NavTarget.MediaViewer -> {
val inputs = MediaViewerNode.Inputs( val inputs = MediaViewerNode.Inputs(

View file

@ -34,7 +34,10 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.androidutils.system.toast
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -42,6 +45,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -58,15 +62,21 @@ class MessagesNode @AssistedInject constructor(
private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer, private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
@ApplicationContext
private val context: Context,
) : Node(buildContext, plugins = plugins), MessagesNavigator { ) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(this) private val presenter = presenterFactory.create(this)
private val callback = plugins<Callback>().firstOrNull() private val callback = plugins<Callback>().firstOrNull()
// TODO Handle navigation to the Event
data class Inputs(val focusedEventId: EventId?) : NodeInputs
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomDetailsClicked() fun onRoomDetailsClicked()
fun onEventClicked(event: TimelineItem.Event): Boolean fun onEventClicked(event: TimelineItem.Event): Boolean
fun onPreviewAttachments(attachments: ImmutableList<Attachment>) fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onUserDataClicked(userId: UserId) fun onUserDataClicked(userId: UserId)
fun onPermalinkClicked(data: PermalinkData)
fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
fun onForwardEventClicked(eventId: EventId) fun onForwardEventClicked(eventId: EventId)
fun onReportMessage(eventId: EventId, senderId: UserId) fun onReportMessage(eventId: EventId, senderId: UserId)
@ -109,16 +119,12 @@ class MessagesNode @AssistedInject constructor(
) { ) {
when (val permalink = permalinkParser.parse(url)) { when (val permalink = permalinkParser.parse(url)) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
// Open the room member profile, it will fallback to
// the user profile if the user is not in the room
callback?.onUserDataClicked(permalink.userId) callback?.onUserDataClicked(permalink.userId)
} }
is PermalinkData.RoomLink -> { is PermalinkData.RoomLink -> {
// TODO Implement room link handling handleRoomLinkClicked(permalink)
}
is PermalinkData.EventIdAliasLink -> {
// TODO Implement room and Event link handling
}
is PermalinkData.EventIdLink -> {
// TODO Implement room and Event link handling
} }
is PermalinkData.FallbackLink, is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> { is PermalinkData.RoomEmailInviteLink -> {
@ -127,6 +133,20 @@ class MessagesNode @AssistedInject constructor(
} }
} }
private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink) {
if (room.matches(roomLink.roomIdOrAlias)) {
if (roomLink.eventId != null) {
// TODO Handle navigation to the Event
context.toast("TODO Handle navigation to the Event ${roomLink.eventId}")
} else {
// Click on the same room, ignore
context.toast("Already viewing this room!")
}
} else {
callback?.onPermalinkClicked(roomLink)
}
}
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
callback?.onShowEventDebugInfoClicked(eventId, debugInfo) callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
} }

View file

@ -258,7 +258,7 @@ class MessagesPresenter @AssistedInject constructor(
private fun MatrixRoomInfo.avatarData(): AvatarData { private fun MatrixRoomInfo.avatarData(): AvatarData {
return AvatarData( return AvatarData(
id = id, id = id.value,
name = name, name = name,
url = avatarUrl ?: room.avatarUrl, url = avatarUrl ?: room.avatarUrl,
size = AvatarSize.TimelineRoom size = AvatarSize.TimelineRoom

View file

@ -720,7 +720,7 @@ class MessagesPresenterTest {
private fun TestScope.createMessagesPresenter( private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) givenRoomInfo(aRoomInfo(id = roomId, name = ""))
}, },
navigator: FakeMessagesNavigator = FakeMessagesNavigator(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),

View file

@ -201,7 +201,7 @@ class TypingNotificationPresenterTest {
private fun createPresenter( private fun createPresenter(
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) givenRoomInfo(aRoomInfo(id = roomId, name = ""))
}, },
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore( sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(
isRenderTypingNotificationsEnabled = true isRenderTypingNotificationsEnabled = true

View file

@ -0,0 +1,28 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.roomaliasresolver.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,43 @@
/*
* 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.features.roomaliasesolver.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
interface RoomAliasResolverEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun params(params: Params): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onAliasResolved(roomId: RoomId)
}
data class Params(
val roomAlias: RoomAlias
) : NodeInputs
}

View file

@ -0,0 +1,53 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.roomaliasresolver.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.roomaliasresolver.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor)
}

View file

@ -0,0 +1,49 @@
/*
* 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.features.roomaliasresolver.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomAliasResolverEntryPoint @Inject constructor() : RoomAliasResolverEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomAliasResolverEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : RoomAliasResolverEntryPoint.NodeBuilder {
override fun callback(callback: RoomAliasResolverEntryPoint.Callback): RoomAliasResolverEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun params(params: RoomAliasResolverEntryPoint.Params): RoomAliasResolverEntryPoint.NodeBuilder {
plugins += params
return this
}
override fun build(): Node {
return parentNode.createNode<RoomAliasResolverNode>(buildContext, plugins)
}
}
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.features.roomaliasresolver.impl
sealed interface RoomAliasResolverEvents {
data object Retry : RoomAliasResolverEvents
}

View file

@ -0,0 +1,59 @@
/*
* 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.features.roomaliasresolver.impl
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.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
class RoomAliasResolverNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: RoomAliasResolverPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
private val inputs = inputs<RoomAliasResolverEntryPoint.Params>()
private val presenter = presenterFactory.create(
inputs.roomAlias
)
private fun onAliasResolved(roomId: RoomId) {
plugins<RoomAliasResolverEntryPoint.Callback>().forEach { it.onAliasResolved(roomId) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
RoomAliasResolverView(
state = state,
onAliasResolved = ::onAliasResolved,
onBackPressed = ::navigateUp,
modifier = modifier
)
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.features.roomaliasresolver.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class RoomAliasResolverPresenter @AssistedInject constructor(
@Assisted private val roomAlias: RoomAlias,
private val matrixClient: MatrixClient,
) : Presenter<RoomAliasResolverState> {
interface Factory {
fun create(
roomAlias: RoomAlias,
): RoomAliasResolverPresenter
}
@Composable
override fun present(): RoomAliasResolverState {
val coroutineScope = rememberCoroutineScope()
val resolveState: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
LaunchedEffect(Unit) {
resolveAlias(resolveState)
}
fun handleEvents(event: RoomAliasResolverEvents) {
when (event) {
RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState)
}
}
return RoomAliasResolverState(
roomAlias = roomAlias,
resolveState = resolveState.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.resolveAlias(resolveState: MutableState<AsyncData<RoomId>>) = launch {
suspend {
matrixClient.resolveRoomAlias(roomAlias).getOrThrow()
}.runCatchingUpdatingState(resolveState)
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.features.roomaliasresolver.impl
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
data class RoomAliasResolverState(
val roomAlias: RoomAlias,
val resolveState: AsyncData<RoomId>,
val eventSink: (RoomAliasResolverEvents) -> Unit
)

View file

@ -0,0 +1,47 @@
/*
* 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.features.roomaliasresolver.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
open class RoomAliasResolverStateProvider : PreviewParameterProvider<RoomAliasResolverState> {
override val values: Sequence<RoomAliasResolverState>
get() = sequenceOf(
aRoomAliasResolverState(),
aRoomAliasResolverState(
resolveState = AsyncData.Loading(),
),
aRoomAliasResolverState(
resolveState = AsyncData.Failure(Exception("Error")),
),
)
}
fun aRoomAliasResolverState(
roomAlias: RoomAlias = A_ROOM_ALIAS,
resolveState: AsyncData<RoomId> = AsyncData.Uninitialized,
eventSink: (RoomAliasResolverEvents) -> Unit = {}
) = RoomAliasResolverState(
roomAlias = roomAlias,
resolveState = resolveState,
eventSink = eventSink,
)
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")

View file

@ -0,0 +1,161 @@
/*
* 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.features.roomaliasresolver.impl
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomAliasResolverView(
state: RoomAliasResolverState,
onBackPressed: () -> Unit,
onAliasResolved: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
val latestOnAliasResolved by rememberUpdatedState(onAliasResolved)
LaunchedEffect(state.resolveState) {
if (state.resolveState is AsyncData.Success) {
latestOnAliasResolved(state.resolveState.data)
}
}
HeaderFooterPage(
modifier = modifier,
paddingValues = PaddingValues(16.dp),
topBar = {
RoomAliasResolverTopBar(onBackClicked = onBackPressed)
},
content = {
RoomAliasResolverContent(state = state)
},
footer = {
RoomAliasResolverFooter(
state = state,
)
}
)
}
@Composable
private fun RoomAliasResolverFooter(
state: RoomAliasResolverState,
modifier: Modifier = Modifier,
) {
when (state.resolveState) {
is AsyncData.Failure -> {
Button(
text = stringResource(CommonStrings.action_retry),
onClick = {
state.eventSink(RoomAliasResolverEvents.Retry)
},
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium,
)
}
is AsyncData.Loading -> {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
CircularProgressIndicator()
}
}
AsyncData.Uninitialized,
is AsyncData.Success -> Unit
}
}
@Composable
private fun RoomAliasResolverContent(
state: RoomAliasResolverState,
modifier: Modifier = Modifier,
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = {
RoomPreviewTitleAtom(state.roomAlias.value)
},
subtitle = {
},
description = {
if (state.resolveState.isFailure()) {
Text(
text = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error,
)
}
},
memberCount = {
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomAliasResolverTopBar(
onBackClicked: () -> Unit,
) {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClicked)
},
title = {},
)
}
@PreviewsDayNight
@Composable
internal fun RoomAliasResolverViewPreview(@PreviewParameter(RoomAliasResolverStateProvider::class) state: RoomAliasResolverState) = ElementPreview {
RoomAliasResolverView(
state = state,
onAliasResolved = { },
onBackPressed = { }
)
}

View file

@ -0,0 +1,43 @@
/*
* 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.features.roomaliasresolver.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.features.roomaliasresolver.impl.RoomAliasResolverPresenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
@Module
@ContributesTo(SessionScope::class)
object RoomAliasResolverModule {
@Provides
fun providesJoinRoomPresenterFactory(
client: MatrixClient,
): RoomAliasResolverPresenter.Factory {
return object : RoomAliasResolverPresenter.Factory {
override fun create(roomAlias: RoomAlias): RoomAliasResolverPresenter {
return RoomAliasResolverPresenter(
roomAlias = roomAlias,
matrixClient = client,
)
}
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_alias_resolver_resolve_alias_failure">"Failed to resolve room alias."</string>
</resources>

View file

@ -0,0 +1,94 @@
/*
* 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.features.roomaliasresolver.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class RoomAliasResolverPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - resolve alias to roomId`() = runTest {
val client = FakeMatrixClient(
resolveRoomAliasResult = { Result.success(A_ROOM_ID) }
)
val presenter = createPresenter(matrixClient = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
assertThat(awaitItem().resolveState.isLoading()).isTrue()
val resultState = awaitItem()
assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS)
assertThat(resultState.resolveState.dataOrNull()).isEqualTo(A_ROOM_ID)
}
}
@Test
fun `present - resolve alias error and retry`() = runTest {
val client = FakeMatrixClient(
resolveRoomAliasResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createPresenter(matrixClient = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
assertThat(awaitItem().resolveState.isLoading()).isTrue()
val resultState = awaitItem()
assertThat(resultState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
resultState.eventSink(RoomAliasResolverEvents.Retry)
val retryLoadingState = awaitItem()
assertThat(retryLoadingState.resolveState.isLoading()).isTrue()
val retryState = awaitItem()
assertThat(retryState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
}
}
private fun createPresenter(
roomAlias: RoomAlias = A_ROOM_ALIAS,
matrixClient: MatrixClient = FakeMatrixClient(),
) = RoomAliasResolverPresenter(
roomAlias = roomAlias,
matrixClient = matrixClient,
)
}

View file

@ -128,7 +128,7 @@ class RoomDetailsPresenter @Inject constructor(
val roomMemberDetailsState = roomMemberDetailsPresenter?.present() val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
return RoomDetailsState( return RoomDetailsState(
roomId = room.roomId.value, roomId = room.roomId,
roomName = roomName, roomName = roomName,
roomAlias = room.alias, roomAlias = room.alias,
roomAvatarUrl = roomAvatar, roomAvatarUrl = roomAvatar,

View file

@ -18,13 +18,15 @@ package io.element.android.features.roomdetails.impl
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
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.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
data class RoomDetailsState( data class RoomDetailsState(
val roomId: String, val roomId: RoomId,
val roomName: String, val roomName: String,
val roomAlias: String?, val roomAlias: RoomAlias?,
val roomAvatarUrl: String?, val roomAvatarUrl: String?,
val roomTopic: RoomTopicState, val roomTopic: RoomTopicState,
val memberCount: Long, val memberCount: Long,

View file

@ -21,6 +21,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
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.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -71,9 +73,9 @@ fun aDmRoomMember(
) )
fun aRoomDetailsState( fun aRoomDetailsState(
roomId: String = "a room id", roomId: RoomId = RoomId("!aRoomId:domain.com"),
roomName: String = "Marketing", roomName: String = "Marketing",
roomAlias: String? = "#marketing:domain.com", roomAlias: RoomAlias? = RoomAlias("#marketing:domain.com"),
roomAvatarUrl: String? = null, roomAvatarUrl: String? = null,
roomTopic: RoomTopicState = RoomTopicState.ExistingTopic( roomTopic: RoomTopicState = RoomTopicState.ExistingTopic(
"Welcome to #marketing, home of the Marketing team " + "Welcome to #marketing, home of the Marketing team " +

View file

@ -78,6 +78,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
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.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
@ -302,9 +304,9 @@ private fun MainActionsSection(
@Composable @Composable
private fun RoomHeaderSection( private fun RoomHeaderSection(
avatarUrl: String?, avatarUrl: String?,
roomId: String, roomId: RoomId,
roomName: String, roomName: String,
roomAlias: String?, roomAlias: RoomAlias?,
openAvatarPreview: (url: String) -> Unit, openAvatarPreview: (url: String) -> Unit,
) { ) {
Column( Column(
@ -314,7 +316,7 @@ private fun RoomHeaderSection(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Avatar( Avatar(
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader), avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
modifier = Modifier modifier = Modifier
.size(70.dp) .size(70.dp)
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
@ -329,7 +331,7 @@ private fun RoomHeaderSection(
if (roomAlias != null) { if (roomAlias != null) {
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text( Text(
text = roomAlias, text = roomAlias.value,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -117,7 +117,7 @@ class RoomDetailsPresenterTests {
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(room.roomId.value) assertThat(initialState.roomId).isEqualTo(room.roomId)
assertThat(initialState.roomName).isEqualTo(room.name) assertThat(initialState.roomName).isEqualTo(room.name)
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))

View file

@ -20,6 +20,7 @@ import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.RoomId
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -29,7 +30,7 @@ import kotlinx.parcelize.Parcelize
data class RoomDescription( data class RoomDescription(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
val alias: String?, val alias: RoomAlias?,
val topic: String?, val topic: String?,
val avatarUrl: String?, val avatarUrl: String?,
val joinRule: JoinRule, val joinRule: JoinRule,
@ -42,14 +43,14 @@ data class RoomDescription(
} }
@IgnoredOnParcel @IgnoredOnParcel
val computedName = name ?: alias ?: roomId.value val computedName = name ?: alias?.value ?: roomId.value
@IgnoredOnParcel @IgnoredOnParcel
val computedDescription: String val computedDescription: String
get() { get() {
return when { return when {
topic != null -> topic topic != null -> topic
name != null && alias != null -> alias name != null && alias != null -> alias.value
name == null && alias == null -> "" name == null && alias == null -> ""
else -> roomId.value else -> roomId.value
} }

View file

@ -19,6 +19,7 @@ package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
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.RoomId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -69,7 +70,7 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
roomId = RoomId("!exa:matrix.org"), roomId = RoomId("!exa:matrix.org"),
name = "Element X Android", name = "Element X Android",
topic = "Element X is a secure, private and decentralized messenger.", topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-android:matrix.org", alias = RoomAlias("#element-x-android:matrix.org"),
avatarUrl = null, avatarUrl = null,
joinRule = RoomDescription.JoinRule.PUBLIC, joinRule = RoomDescription.JoinRule.PUBLIC,
numberOfMembers = 2765, numberOfMembers = 2765,
@ -78,7 +79,7 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
roomId = RoomId("!exi:matrix.org"), roomId = RoomId("!exi:matrix.org"),
name = "Element X iOS", name = "Element X iOS",
topic = "Element X is a secure, private and decentralized messenger.", topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-ios:matrix.org", alias = RoomAlias("#element-x-ios:matrix.org"),
avatarUrl = null, avatarUrl = null,
joinRule = RoomDescription.JoinRule.UNKNOWN, joinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers = 356, numberOfMembers = 356,

View file

@ -65,6 +65,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber import timber.log.Timber
@ -198,13 +199,13 @@ private fun NameAndTimestampRow(
private fun InviteSubtitle( private fun InviteSubtitle(
isDirect: Boolean, isDirect: Boolean,
inviteSender: InviteSender?, inviteSender: InviteSender?,
canonicalAlias: String?, canonicalAlias: RoomAlias?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val subtitle = if (isDirect) { val subtitle = if (isDirect) {
inviteSender?.userId?.value inviteSender?.userId?.value
} else { } else {
canonicalAlias canonicalAlias?.value
} }
if (subtitle != null) { if (subtitle != null) {
Text( Text(

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
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.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@ -27,7 +28,7 @@ data class RoomListRoomSummary(
val displayType: RoomSummaryDisplayType, val displayType: RoomSummaryDisplayType,
val roomId: RoomId, val roomId: RoomId,
val name: String, val name: String,
val canonicalAlias: String?, val canonicalAlias: RoomAlias?,
val numberOfUnreadMessages: Int, val numberOfUnreadMessages: Int,
val numberOfUnreadMentions: Int, val numberOfUnreadMentions: Int,
val numberOfUnreadNotifications: Int, val numberOfUnreadNotifications: Int,

View file

@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@ -88,7 +89,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
userId = "@alice:matrix.org", userId = "@alice:matrix.org",
displayName = "Alice", displayName = "Alice",
), ),
canonicalAlias = "#alias:matrix.org", canonicalAlias = RoomAlias("#alias:matrix.org"),
), ),
aRoomListRoomSummary( aRoomListRoomSummary(
name = "Bob", name = "Bob",
@ -129,7 +130,7 @@ internal fun aRoomListRoomSummary(
isFavorite: Boolean = false, isFavorite: Boolean = false,
inviteSender: InviteSender? = null, inviteSender: InviteSender? = null,
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
canonicalAlias: String? = null, canonicalAlias: RoomAlias? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = id, id = id,
roomId = RoomId(id), roomId = RoomId(id),

View file

@ -0,0 +1,37 @@
/*
* 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.designsystem.atomic.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewDescriptionAtom(description: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = description,
style = ElementTheme.typography.fontBodySmRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}

View file

@ -0,0 +1,34 @@
/*
* 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.designsystem.atomic.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
}

View file

@ -0,0 +1,34 @@
/*
* 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.designsystem.atomic.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewTitleAtom(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
)
}

View file

@ -0,0 +1,74 @@
/*
* 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.designsystem.atomic.molecules
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewMembersCountMolecule(
memberCount: Long,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
.widthIn(min = 48.dp)
.padding(start = 2.dp, end = 6.dp, top = 2.dp, bottom = 2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
imageVector = CompoundIcons.UserProfile(),
contentDescription = null,
tint = ElementTheme.colors.iconSecondary,
)
Text(
text = "$memberCount",
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textSecondary,
)
}
}
@PreviewsDayNight
@Composable
internal fun RoomPreviewMembersCountMoleculePreview() = ElementPreview {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
RoomPreviewMembersCountMolecule(memberCount = 1)
RoomPreviewMembersCountMolecule(memberCount = 888)
RoomPreviewMembersCountMolecule(memberCount = 123_456)
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.designsystem.atomic.organisms
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun RoomPreviewOrganism(
avatar: @Composable () -> Unit,
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit,
modifier: Modifier = Modifier,
description: @Composable (() -> Unit)? = null,
memberCount: @Composable (() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
avatar()
Spacer(modifier = Modifier.height(16.dp))
title()
Spacer(modifier = Modifier.height(8.dp))
subtitle()
Spacer(modifier = Modifier.height(8.dp))
if (memberCount != null) {
memberCount()
}
Spacer(modifier = Modifier.height(8.dp))
if (description != null) {
description()
}
Spacer(modifier = Modifier.height(24.dp))
}
}

View file

@ -17,7 +17,9 @@
package io.element.android.libraries.matrix.api package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.ProgressCallback 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.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.SessionId
import io.element.android.libraries.matrix.api.core.UserId 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.CreateRoomParameters
@ -30,6 +32,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
@ -98,4 +101,6 @@ interface MatrixClient : Closeable {
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview>
} }

View file

@ -0,0 +1,31 @@
/*
* 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 io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
@JvmInline
value class RoomAlias(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isRoomAlias(value)) {
error("`$value` is not a valid room alias.\n Example room alias: `#room_alias:domain`.")
}
}
override fun toString(): String = value
}

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

@ -20,8 +20,10 @@ import android.net.Uri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId 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.core.UserId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/** /**
* This sealed class represents all the permalink cases. * This sealed class represents all the permalink cases.
@ -29,36 +31,11 @@ import kotlinx.collections.immutable.ImmutableList
*/ */
@Immutable @Immutable
sealed interface PermalinkData { sealed interface PermalinkData {
sealed interface RoomLink : PermalinkData { data class RoomLink(
val viaParameters: ImmutableList<String> val roomIdOrAlias: RoomIdOrAlias,
} val eventId: EventId? = null,
val viaParameters: ImmutableList<String> = persistentListOf()
data class RoomIdLink( ) : PermalinkData
val roomId: RoomId,
override val viaParameters: ImmutableList<String>
) : RoomLink
data class RoomAliasLink(
val roomAlias: String,
override val viaParameters: ImmutableList<String>
) : RoomLink
sealed interface EventLink : PermalinkData {
val eventId: EventId
val viaParameters: ImmutableList<String>
}
data class EventIdLink(
val roomId: RoomId,
override val eventId: EventId,
override val viaParameters: ImmutableList<String>
) : EventLink
data class EventIdAliasLink(
val roomAlias: String,
override val eventId: EventId,
override val viaParameters: ImmutableList<String>
) : EventLink
/* /*
* &room_name=Team2 * &room_name=Team2

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback 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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
@ -45,8 +46,8 @@ interface MatrixRoom : Closeable {
val roomId: RoomId val roomId: RoomId
val name: String? val name: String?
val displayName: String val displayName: String
val alias: String? val alias: RoomAlias?
val alternativeAliases: List<String> val alternativeAliases: List<RoomAlias>
val topic: String? val topic: String?
val avatarUrl: String? val avatarUrl: String?
val isEncrypted: Boolean val isEncrypted: Boolean

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
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.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -24,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableMap
@Immutable @Immutable
data class MatrixRoomInfo( data class MatrixRoomInfo(
val id: String, val id: RoomId,
val name: String?, val name: String?,
val topic: String?, val topic: String?,
val avatarUrl: String?, val avatarUrl: String?,
@ -33,7 +35,7 @@ data class MatrixRoomInfo(
val isSpace: Boolean, val isSpace: Boolean,
val isTombstoned: Boolean, val isTombstoned: Boolean,
val isFavorite: Boolean, val isFavorite: Boolean,
val canonicalAlias: String?, val canonicalAlias: RoomAlias?,
val alternativeAliases: ImmutableList<String>, val alternativeAliases: ImmutableList<String>,
val currentUserMembership: CurrentUserMembership, val currentUserMembership: CurrentUserMembership,
val latestEvent: EventTimelineItem?, val latestEvent: EventTimelineItem?,

View file

@ -23,5 +23,5 @@ sealed interface Mention {
data class User(val userId: UserId) : Mention data class User(val userId: UserId) : Mention
data object AtRoom : Mention data object AtRoom : Mention
data class Room(val roomId: RoomId) : Mention data class Room(val roomId: RoomId) : Mention
data class RoomAlias(val roomAlias: String?) : Mention data class RoomAlias(val roomAlias: RoomAlias?) : Mention
} }

View file

@ -0,0 +1,34 @@
/*
* 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.room.alias
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.MatrixRoom
/**
* Return true if the given roomIdOrAlias is the same room as this room.
*/
fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean {
return when (roomIdOrAlias) {
is RoomIdOrAlias.Id -> {
roomIdOrAlias.roomId == roomId
}
is RoomIdOrAlias.Alias -> {
roomIdOrAlias.roomAlias == alias || roomIdOrAlias.roomAlias in alternativeAliases
}
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.room.preview
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
data class RoomPreview(
/** The room id for this room. */
val roomId: RoomId,
/** The canonical alias for the room. */
val canonicalAlias: RoomAlias?,
/** The room's name, if set. */
val name: String?,
/** The room's topic, if set. */
val topic: String?,
/** The MXC URI to the room's avatar, if set. */
val avatarUrl: String?,
/** The number of joined members. */
val numberOfJoinedMembers: Long,
/** The room type (space, custom) or nothing, if it's a regular room. */
val roomType: String?,
/** Is the history world-readable for this room? */
val isHistoryWorldReadable: Boolean,
/** Is the room joined by the current user? */
val isJoined: Boolean,
/** Is the current user invited to this room? */
val isInvited: Boolean,
/** is the join rule public for this room? */
val isPublic: Boolean,
/** Can we knock (or restricted-knock) to this room? */
val canKnock: Boolean,
)

View file

@ -16,13 +16,14 @@
package io.element.android.libraries.matrix.api.roomdirectory package io.element.android.libraries.matrix.api.roomdirectory
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.RoomId
data class RoomDescription( data class RoomDescription(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
val topic: String?, val topic: String?,
val alias: String?, val alias: RoomAlias?,
val avatarUrl: String?, val avatarUrl: String?,
val joinRule: JoinRule, val joinRule: JoinRule,
val isWorldReadable: Boolean, val isWorldReadable: Boolean,

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
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.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
@ -37,7 +38,7 @@ sealed interface RoomSummary {
data class RoomSummaryDetails( data class RoomSummaryDetails(
val roomId: RoomId, val roomId: RoomId,
val name: String, val name: String,
val canonicalAlias: String?, val canonicalAlias: RoomAlias?,
val isDirect: Boolean, val isDirect: Boolean,
val avatarUrl: String?, val avatarUrl: String?,
val lastMessage: RoomMessage?, val lastMessage: RoomMessage?,

View file

@ -23,7 +23,9 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.MatrixClient 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.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.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.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomPreset
@ -37,6 +39,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
@ -460,6 +463,24 @@ class RustMatrixClient(
} }
} }
@Suppress("TooGenericExceptionThrown")
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) {
runCatching {
// TODO Waiting for SDK to be released
throw Exception("Not implemented")
// client.resolveRoomAlias(roomAlias.value).let(::RoomId)
}
}
@Suppress("TooGenericExceptionThrown")
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching {
// TODO Waiting for SDK to be released
throw Exception("Not implemented")
// client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
}
}
override fun syncService(): SyncService = rustSyncService override fun syncService(): SyncService = rustSyncService
override fun sessionVerificationService(): SessionVerificationService = verificationService override fun sessionVerificationService(): SessionVerificationService = verificationService

View file

@ -20,8 +20,10 @@ import android.net.Uri
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
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.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter import io.element.android.libraries.matrix.api.permalink.MatrixToConverter
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@ -59,24 +61,24 @@ class DefaultPermalinkParser @Inject constructor(
} else { } else {
val viaParameters = result.via.toImmutableList() val viaParameters = result.via.toImmutableList()
when (val id = result.id) { when (val id = result.id) {
is MatrixId.Room -> PermalinkData.RoomIdLink(
roomId = RoomId(id.id),
viaParameters = viaParameters,
)
is MatrixId.User -> PermalinkData.UserLink( is MatrixId.User -> PermalinkData.UserLink(
userId = UserId(id.id), userId = UserId(id.id),
) )
is MatrixId.RoomAlias -> PermalinkData.RoomAliasLink( is MatrixId.Room -> PermalinkData.RoomLink(
roomAlias = id.alias, roomIdOrAlias = RoomId(id.id).toRoomIdOrAlias(),
viaParameters = viaParameters, viaParameters = viaParameters,
) )
is MatrixId.EventOnRoomId -> PermalinkData.EventIdLink( is MatrixId.RoomAlias -> PermalinkData.RoomLink(
roomId = RoomId(id.roomId), roomIdOrAlias = RoomAlias(id.alias).toRoomIdOrAlias(),
viaParameters = viaParameters,
)
is MatrixId.EventOnRoomId -> PermalinkData.RoomLink(
roomIdOrAlias = RoomId(id.roomId).toRoomIdOrAlias(),
eventId = EventId(id.eventId), eventId = EventId(id.eventId),
viaParameters = viaParameters, viaParameters = viaParameters,
) )
is MatrixId.EventOnRoomAlias -> PermalinkData.EventIdAliasLink( is MatrixId.EventOnRoomAlias -> PermalinkData.RoomLink(
roomAlias = id.alias, roomIdOrAlias = RoomAlias(id.alias).toRoomIdOrAlias(),
eventId = EventId(id.eventId), eventId = EventId(id.eventId),
viaParameters = viaParameters, viaParameters = viaParameters,
) )

View file

@ -16,6 +16,8 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
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.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership 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.MatrixRoomInfo
@ -35,7 +37,7 @@ class MatrixRoomInfoMapper(
) { ) {
fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.use { fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.use {
return MatrixRoomInfo( return MatrixRoomInfo(
id = it.id, id = RoomId(it.id),
name = it.name, name = it.name,
topic = it.topic, topic = it.topic,
avatarUrl = it.avatarUrl, avatarUrl = it.avatarUrl,
@ -44,7 +46,7 @@ class MatrixRoomInfoMapper(
isSpace = it.isSpace, isSpace = it.isSpace,
isTombstoned = it.isTombstoned, isTombstoned = it.isTombstoned,
isFavorite = it.isFavourite, isFavorite = it.isFavourite,
canonicalAlias = it.canonicalAlias, canonicalAlias = it.canonicalAlias?.let(::RoomAlias),
alternativeAliases = it.alternativeAliases.toImmutableList(), alternativeAliases = it.alternativeAliases.toImmutableList(),
currentUserMembership = it.membership.map(), currentUserMembership = it.membership.map(),
latestEvent = it.latestEvent?.use(timelineItemMapper::map), latestEvent = it.latestEvent?.use(timelineItemMapper::map),

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback 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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
@ -205,11 +206,11 @@ class RustMatrixRoom(
override val isEncrypted: Boolean override val isEncrypted: Boolean
get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false) get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false)
override val alias: String? override val alias: RoomAlias?
get() = runCatching { innerRoom.canonicalAlias() }.getOrDefault(null) get() = runCatching { innerRoom.canonicalAlias()?.let(::RoomAlias) }.getOrDefault(null)
override val alternativeAliases: List<String> override val alternativeAliases: List<RoomAlias>
get() = runCatching { innerRoom.alternativeAliases() }.getOrDefault(emptyList()) get() = runCatching { innerRoom.alternativeAliases().map { RoomAlias(it) } }.getOrDefault(emptyList())
override val isPublic: Boolean override val isPublic: Boolean
get() = runCatching { innerRoom.isPublic() }.getOrDefault(false) get() = runCatching { innerRoom.isPublic() }.getOrDefault(false)

View file

@ -25,16 +25,16 @@ import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
object RoomMemberMapper { object RoomMemberMapper {
fun map(roomMember: RustRoomMember): RoomMember = RoomMember( fun map(roomMember: RustRoomMember): RoomMember = RoomMember(
UserId(roomMember.userId), userId = UserId(roomMember.userId),
roomMember.displayName, displayName = roomMember.displayName,
roomMember.avatarUrl, avatarUrl = roomMember.avatarUrl,
mapMembership(roomMember.membership), membership = mapMembership(roomMember.membership),
roomMember.isNameAmbiguous, isNameAmbiguous = roomMember.isNameAmbiguous,
roomMember.powerLevel, powerLevel = roomMember.powerLevel,
roomMember.normalizedPowerLevel, normalizedPowerLevel = roomMember.normalizedPowerLevel,
roomMember.isIgnored, isIgnored = roomMember.isIgnored,
mapRole(roomMember.suggestedRoleForPowerLevel), role = mapRole(roomMember.suggestedRoleForPowerLevel),
) )
fun mapRole(role: RoomMemberRole): RoomMember.Role = fun mapRole(role: RoomMemberRole): RoomMember.Role =
when (role) { when (role) {

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.roomdirectory package io.element.android.libraries.matrix.impl.roomdirectory
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.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
@ -28,7 +29,7 @@ class RoomDescriptionMapper {
name = roomDescription.name, name = roomDescription.name,
topic = roomDescription.topic, topic = roomDescription.topic,
avatarUrl = roomDescription.avatarUrl, avatarUrl = roomDescription.avatarUrl,
alias = roomDescription.alias, alias = roomDescription.alias?.let(::RoomAlias),
joinRule = when (roomDescription.joinRule) { joinRule = when (roomDescription.joinRule) {
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
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.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
@ -33,7 +34,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
return RoomSummaryDetails( return RoomSummaryDetails(
roomId = RoomId(roomInfo.id), roomId = RoomId(roomInfo.id),
name = roomInfo.name ?: roomInfo.id, name = roomInfo.name ?: roomInfo.id,
canonicalAlias = roomInfo.canonicalAlias, canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
isDirect = roomInfo.isDirect, isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl, avatarUrl = roomInfo.avatarUrl,
numUnreadMentions = roomInfo.numUnreadMentions.toInt(), numUnreadMentions = roomInfo.numUnreadMentions.toInt(),

View file

@ -18,7 +18,9 @@ package io.element.android.libraries.matrix.test
import io.element.android.libraries.matrix.api.MatrixClient 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.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.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.SessionId
import io.element.android.libraries.matrix.api.core.UserId 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.CreateRoomParameters
@ -31,6 +33,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@ -73,6 +76,8 @@ class FakeMatrixClient(
private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val encryptionService: FakeEncryptionService = FakeEncryptionService(),
private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
private val accountManagementUrlString: Result<String?> = Result.success(null), private val accountManagementUrlString: Result<String?> = Result.success(null),
private val resolveRoomAliasResult: (RoomAlias) -> Result<RoomId> = { Result.success(A_ROOM_ID) },
private val getRoomPreviewResult: (RoomIdOrAlias) -> Result<RoomPreview> = { Result.failure(AN_EXCEPTION) },
) : MatrixClient { ) : MatrixClient {
var setDisplayNameCalled: Boolean = false var setDisplayNameCalled: Boolean = false
private set private set
@ -276,6 +281,14 @@ class FakeMatrixClient(
return Result.success(Unit) return Result.success(Unit)
} }
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = simulateLongTask {
resolveRoomAliasResult(roomAlias)
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = simulateLongTask {
getRoomPreviewResult(roomIdOrAlias)
}
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> { override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {
return Result.success(visitedRoomsId) return Result.success(visitedRoomsId)
} }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.SpaceId
@ -50,6 +51,7 @@ val A_THREAD_ID = ThreadId("\$aThreadId")
val A_THREAD_ID_2 = ThreadId("\$aThreadId2") val A_THREAD_ID_2 = ThreadId("\$aThreadId2")
val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID = EventId("\$anEventId")
val AN_EVENT_ID_2 = EventId("\$anEventId2") val AN_EVENT_ID_2 = EventId("\$anEventId2")
val A_ROOM_ALIAS = RoomAlias("#alias1:domain")
val A_TRANSACTION_ID = TransactionId("aTransactionId") val A_TRANSACTION_ID = TransactionId("aTransactionId")
const val A_UNIQUE_ID = "aUniqueId" const val A_UNIQUE_ID = "aUniqueId"

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback 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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
@ -74,8 +75,8 @@ class FakeMatrixRoom(
override val topic: String? = null, override val topic: String? = null,
override val avatarUrl: String? = null, override val avatarUrl: String? = null,
override val isEncrypted: Boolean = false, override val isEncrypted: Boolean = false,
override val alias: String? = null, override val alias: RoomAlias? = null,
override val alternativeAliases: List<String> = emptyList(), override val alternativeAliases: List<RoomAlias> = emptyList(),
override val isPublic: Boolean = true, override val isPublic: Boolean = true,
override val isSpace: Boolean = false, override val isSpace: Boolean = false,
override val isDirect: Boolean = false, override val isDirect: Boolean = false,
@ -751,7 +752,7 @@ data class EndPollInvocation(
) )
fun aRoomInfo( fun aRoomInfo(
id: String = A_ROOM_ID.value, id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME, name: String? = A_ROOM_NAME,
topic: String? = "A topic", topic: String? = "A topic",
avatarUrl: String? = AN_AVATAR_URL, avatarUrl: String? = AN_AVATAR_URL,
@ -760,7 +761,7 @@ fun aRoomInfo(
isSpace: Boolean = false, isSpace: Boolean = false,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
isFavorite: Boolean = false, isFavorite: Boolean = false,
canonicalAlias: String? = null, canonicalAlias: RoomAlias? = null,
alternativeAliases: List<String> = emptyList(), alternativeAliases: List<String> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
latestEvent: EventTimelineItem? = null, latestEvent: EventTimelineItem? = null,

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.test.room package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
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.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -72,7 +73,7 @@ fun aRoomSummaryDetails(
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
notificationMode: RoomNotificationMode? = null, notificationMode: RoomNotificationMode? = null,
inviter: RoomMember? = null, inviter: RoomMember? = null,
canonicalAlias: String? = null, canonicalAlias: RoomAlias? = null,
hasRoomCall: Boolean = false, hasRoomCall: Boolean = false,
isDm: Boolean = false, isDm: Boolean = false,
isFavorite: Boolean = false, isFavorite: Boolean = false,

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.test.roomdirectory package io.element.android.libraries.matrix.test.roomdirectory
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.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -24,7 +25,7 @@ fun aRoomDescription(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = null, name: String? = null,
topic: String? = null, topic: String? = null,
alias: String? = null, alias: RoomAlias? = null,
avatarUrl: String? = null, avatarUrl: String? = null,
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
isWorldReadable: Boolean = true, isWorldReadable: Boolean = true,

View file

@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
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.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
@ -106,7 +107,7 @@ internal fun SelectedRoomPreview() = ElementPreview {
fun aRoomSummaryDetails( fun aRoomSummaryDetails(
roomId: RoomId = RoomId("!room:domain"), roomId: RoomId = RoomId("!room:domain"),
name: String = "roomName", name: String = "roomName",
canonicalAlias: String? = null, canonicalAlias: RoomAlias? = null,
isDirect: Boolean = true, isDirect: Boolean = true,
avatarUrl: String? = null, avatarUrl: String? = null,
lastMessage: RoomMessage? = null, lastMessage: RoomMessage? = null,

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.roomselect.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
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.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
@ -65,6 +66,6 @@ private fun aForwardMessagesRoomList() = persistentListOf(
aRoomSummaryDetails( aRoomSummaryDetails(
roomId = RoomId("!room2:domain"), roomId = RoomId("!room2:domain"),
name = "Room with alias", name = "Room with alias",
canonicalAlias = "#alias:example.org", canonicalAlias = RoomAlias("#alias:example.org"),
), ),
) )

View file

@ -243,7 +243,7 @@ private fun RoomSummaryView(
// Alias // Alias
summary.canonicalAlias?.let { alias -> summary.canonicalAlias?.let { alias ->
Text( Text(
text = alias, text = alias.value,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
maxLines = 1, maxLines = 1,

View file

@ -39,8 +39,10 @@ import io.element.android.libraries.designsystem.theme.currentUserMentionPillBac
import io.element.android.libraries.designsystem.theme.currentUserMentionPillText import io.element.android.libraries.designsystem.theme.currentUserMentionPillText
import io.element.android.libraries.designsystem.theme.mentionPillBackground import io.element.android.libraries.designsystem.theme.mentionPillBackground
import io.element.android.libraries.designsystem.theme.mentionPillText import io.element.android.libraries.designsystem.theme.mentionPillText
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.SessionId 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.core.UserId
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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -139,8 +141,9 @@ internal fun MentionSpanPreview() {
return when (uriString) { return when (uriString) {
"https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink(UserId("@me:matrix.org")) "https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink(UserId("@me:matrix.org"))
"https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink(UserId("@other:matrix.org")) "https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink(UserId("@other:matrix.org"))
"https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomAliasLink( "https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomLink(
roomAlias = "#room:matrix.org", roomIdOrAlias = RoomAlias("#room:matrix.org").toRoomIdOrAlias(),
eventId = null,
viaParameters = persistentListOf(), viaParameters = persistentListOf(),
) )
else -> TODO() else -> TODO()

View file

@ -18,13 +18,14 @@ package io.element.android.libraries.textcomposer.impl.mentions
import android.graphics.Color import android.graphics.Color
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
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.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -68,9 +69,8 @@ class MentionSpanProviderTest {
@Test @Test
fun `getting mention span for a room should return a MentionSpan with normal colors`() { fun `getting mention span for a room should return a MentionSpan with normal colors`() {
permalinkParser.givenResult( permalinkParser.givenResult(
PermalinkData.RoomAliasLink( PermalinkData.RoomLink(
roomAlias = "#room:matrix.org", roomIdOrAlias = RoomAlias("#room:matrix.org").toRoomIdOrAlias(),
viaParameters = persistentListOf(),
) )
) )
val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org") val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org")
@ -81,12 +81,11 @@ class MentionSpanProviderTest {
@Test @Test
fun `getting mention span for @room should return a MentionSpan with normal colors`() { fun `getting mention span for @room should return a MentionSpan with normal colors`() {
permalinkParser.givenResult( permalinkParser.givenResult(
PermalinkData.RoomAliasLink( PermalinkData.RoomLink(
roomAlias = "#", roomIdOrAlias = RoomAlias("#room:matrix.org").toRoomIdOrAlias(),
viaParameters = persistentListOf(),
) )
) )
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "#") val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "#room:matrix.org")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor) assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor) assertThat(mentionSpan.textColor).isEqualTo(otherColor)
} }

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
size 4457

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a32da6aa9e7137262ea70c3901fb058c97daf24ff445eda23dac8640d188f124
size 25585

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c734484b6ac89c5540db52a62eb5ca17d5013c5901aea10876d769c6abb019dd
size 26263

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b2474c3058ae955f81985c02b9491cc79cd7d87001900dc449a1fb42684114f
size 11970

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b2474c3058ae955f81985c02b9491cc79cd7d87001900dc449a1fb42684114f
size 11970

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
size 4464

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f31dc397d61b8b088ece9680dd8c700a9a0d7bb6310859e82adba8c942effaf
size 21861

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6083235628afa1c7aee7dd584d14b4b3ac7cd76b077b040675b2de42adbb36b7
size 22413

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:812e571d895c87f6b8ab77638a716a28c125dc6e41310f4f5d376035d96b5dae
size 9323

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:812e571d895c87f6b8ab77638a716a28c125dc6e41310f4f5d376035d96b5dae
size 9323

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7406a2a6fa8e4dd4423a5141147ecc1aa235d4dbff980b7ef72fa2e59f502fc
size 7686

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:329ade4a3d0e1b013f03bf5d2d107ce2dacc235f7e2defc24c5cd1623330b107
size 23944

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ed85e4bb1351a1af82b64628c35aff7ef165c9df9b1ac0e1bba737a9d1e15a8
size 25797

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fcfb2b5cc790707f48dbd56f2c3f4909ebe1ae93b082309e7b0b89e82fec6c8
size 39320

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ba446e166858f6da8d79ae4af997ff142d73edebf178aadf75579b041a3d267
size 28605

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c34154223d9eef3a66fbc9c266010e3e51b293db7f4698d550638fa8912a231
size 16864

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e70d55552b46b1a34848651bf8bcf8048ba2d28c7d2d356f8c1a0f71bb6e049
size 21104

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa320f4667087c59776d08dfb9108a577e42eb51ba1e6b3d3ea6da3d55605495
size 7523

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4244c5d24935e2cf6df3cff5d31eb7980757a1e555aedbb97561c1827778e2b
size 22816

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3725b3e21eb939bebcc44b9c62f21817f1ab227dc7a9f2367b721cdb0e750720
size 25127

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e23f1d506bcfc603192d854945059b4f03e9215f20f3f7f9150728aee0c4bf7e
size 38085

Some files were not shown because too many files have changed in this diff Show more