feature(room preview): Add option to forget room, improve the room preview screen for banned rooms.
Some internal refactoring was done too: - Remove RoomInfo.isPublic to only use JoinRule. - Also take into account restricted access rooms for previews.
This commit is contained in:
parent
819503b162
commit
a73bcb71d5
50 changed files with 886 additions and 357 deletions
|
|
@ -22,7 +22,7 @@ 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_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakePendingRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomPreview
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
|
|
@ -78,7 +78,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteFailure)
|
||||
getRoomPreviewResults[A_ROOM_ID] = FakeRoomPreview(declineInviteResult = declineInviteFailure)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
||||
presenter.test {
|
||||
|
|
@ -121,7 +121,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
Result.success(Unit)
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteSuccess)
|
||||
getRoomPreviewResults[A_ROOM_ID] = FakeRoomPreview(declineInviteResult = declineInviteSuccess)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(
|
||||
client = client,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ package io.element.android.features.joinroom.impl
|
|||
|
||||
sealed interface JoinRoomEvents {
|
||||
data object RetryFetchingContent : JoinRoomEvents
|
||||
data object DismissContent : JoinRoomEvents
|
||||
data object JoinRoom : JoinRoomEvents
|
||||
data object KnockRoom : JoinRoomEvents
|
||||
data object ForgetRoom : JoinRoomEvents
|
||||
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
|
||||
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
|
||||
data object ClearActionStates : JoinRoomEvents
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class JoinRoomNode @AssistedInject constructor(
|
|||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onJoinSuccess = ::navigateUp,
|
||||
onForgetSuccess = ::navigateUp,
|
||||
onCancelKnockSuccess = {},
|
||||
onKnockSuccess = {},
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -26,22 +26,27 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.exception.ErrorKind
|
||||
import io.element.android.libraries.matrix.api.getRoomInfoFlow
|
||||
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.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -58,6 +63,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
private val joinRoom: JoinRoom,
|
||||
private val knockRoom: KnockRoom,
|
||||
private val cancelKnockRoom: CancelKnockRoom,
|
||||
private val forgetRoom: ForgetRoom,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<JoinRoomState> {
|
||||
|
|
@ -79,13 +85,17 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
var knockMessage by rememberSaveable { mutableStateOf("") }
|
||||
var isDismissingContent by remember { mutableStateOf(false) }
|
||||
val contentState by produceState<ContentState>(
|
||||
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||
initialValue = ContentState.Loading,
|
||||
key1 = roomInfo,
|
||||
key2 = retryCount,
|
||||
key3 = isDismissingContent,
|
||||
) {
|
||||
when {
|
||||
isDismissingContent -> value = ContentState.Dismissing
|
||||
roomInfo.isPresent -> {
|
||||
value = roomInfo.get().toContentState()
|
||||
}
|
||||
|
|
@ -93,17 +103,17 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
value = roomDescription.get().toContentState()
|
||||
}
|
||||
else -> {
|
||||
value = ContentState.Loading(roomIdOrAlias)
|
||||
value = ContentState.Loading
|
||||
val result = matrixClient.getRoomPreviewInfo(roomIdOrAlias, serverNames)
|
||||
value = result.fold(
|
||||
onSuccess = { previewInfo ->
|
||||
previewInfo.toContentState()
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
if (throwable.message?.contains("403") == true) {
|
||||
ContentState.UnknownRoom(roomIdOrAlias)
|
||||
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
|
||||
ContentState.UnknownRoom
|
||||
} else {
|
||||
ContentState.Failure(roomIdOrAlias, throwable)
|
||||
ContentState.Failure(throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -136,18 +146,25 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
knockAction.value = AsyncAction.Uninitialized
|
||||
joinAction.value = AsyncAction.Uninitialized
|
||||
cancelKnockAction.value = AsyncAction.Uninitialized
|
||||
forgetRoomAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is JoinRoomEvents.UpdateKnockMessage -> {
|
||||
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
|
||||
}
|
||||
JoinRoomEvents.DismissContent -> {
|
||||
isDismissingContent = true
|
||||
}
|
||||
JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
|
||||
}
|
||||
}
|
||||
|
||||
return JoinRoomState(
|
||||
roomIdOrAlias = roomIdOrAlias,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
joinAction = joinAction.value,
|
||||
knockAction = knockAction.value,
|
||||
forgetAction = forgetRoomAction.value,
|
||||
cancelKnockAction = cancelKnockAction.value,
|
||||
applicationName = buildMeta.applicationName,
|
||||
knockMessage = knockMessage,
|
||||
|
|
@ -161,7 +178,13 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
roomIdOrAlias = roomIdOrAlias,
|
||||
serverNames = serverNames,
|
||||
trigger = trigger
|
||||
)
|
||||
).mapFailure {
|
||||
if (it is ClientException.MatrixApi && it.kind == ErrorKind.Forbidden) {
|
||||
JoinRoomFailures.UnauthorizedJoin
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +203,12 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.forgetRoom(forgetAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
forgetAction.runUpdatingState {
|
||||
forgetRoom.invoke(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomPreviewInfo.toContentState(): ContentState {
|
||||
|
|
@ -192,12 +221,11 @@ private fun RoomPreviewInfo.toContentState(): ContentState {
|
|||
isDm = false,
|
||||
roomType = roomType,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
joinAuthorisationStatus = when {
|
||||
// Note when isInvited, roomInfo will be used, so if this happen, it will be temporary.
|
||||
isInvited -> JoinAuthorisationStatus.IsInvited(null)
|
||||
canKnock -> JoinAuthorisationStatus.CanKnock
|
||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||
else -> JoinAuthorisationStatus.Unknown
|
||||
joinAuthorisationStatus = when (membership) {
|
||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(null)
|
||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(null)
|
||||
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||
else -> joinRule.toJoinAuthorisationStatus()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -232,17 +260,31 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
|||
isDm = isDm,
|
||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
joinAuthorisationStatus = when {
|
||||
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||
joinAuthorisationStatus = when (currentUserMembership) {
|
||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||
inviteSender = inviter?.toInviteSender()
|
||||
)
|
||||
currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||
else -> JoinAuthorisationStatus.Unknown
|
||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
|
||||
banSender = inviter?.toInviteSender()
|
||||
)
|
||||
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||
else -> joinRule.toJoinAuthorisationStatus()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
|
||||
return when (this) {
|
||||
JoinRule.Knock,
|
||||
is JoinRule.KnockRestricted -> JoinAuthorisationStatus.CanKnock
|
||||
JoinRule.Invite,
|
||||
JoinRule.Private -> JoinAuthorisationStatus.NeedInvite
|
||||
is JoinRule.Restricted -> JoinAuthorisationStatus.Restricted
|
||||
JoinRule.Public -> JoinAuthorisationStatus.CanJoin
|
||||
else -> JoinAuthorisationStatus.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun ContentState.toInviteData(): InviteData? {
|
||||
return when (this) {
|
||||
|
|
|
|||
|
|
@ -22,30 +22,49 @@ internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
|
|||
|
||||
@Immutable
|
||||
data class JoinRoomState(
|
||||
val roomIdOrAlias: RoomIdOrAlias,
|
||||
val contentState: ContentState,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
val joinAction: AsyncAction<Unit>,
|
||||
val knockAction: AsyncAction<Unit>,
|
||||
val forgetAction: AsyncAction<Unit>,
|
||||
val cancelKnockAction: AsyncAction<Unit>,
|
||||
val applicationName: String,
|
||||
private val applicationName: String,
|
||||
val knockMessage: String,
|
||||
val eventSink: (JoinRoomEvents) -> Unit
|
||||
) {
|
||||
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
|
||||
val joinAuthorisationStatus = when (contentState) {
|
||||
// Use the join authorisation status from the loaded content state
|
||||
is ContentState.Loaded -> contentState.joinAuthorisationStatus
|
||||
// Assume that if the room is unknown, the user can join it
|
||||
is ContentState.UnknownRoom -> JoinAuthorisationStatus.CanJoin
|
||||
// Otherwise assume that the user can't join the room
|
||||
else -> JoinAuthorisationStatus.Unknown
|
||||
is ContentState.Loaded -> {
|
||||
when {
|
||||
contentState.roomType == RoomType.Space -> {
|
||||
JoinAuthorisationStatus.IsSpace(applicationName)
|
||||
}
|
||||
isJoinActionUnauthorized -> {
|
||||
JoinAuthorisationStatus.Unauthorized
|
||||
}
|
||||
else -> {
|
||||
contentState.joinAuthorisationStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
is ContentState.UnknownRoom -> {
|
||||
if (isJoinActionUnauthorized) {
|
||||
JoinAuthorisationStatus.Unauthorized
|
||||
} else {
|
||||
JoinAuthorisationStatus.Unknown
|
||||
}
|
||||
}
|
||||
else -> JoinAuthorisationStatus.None
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface ContentState {
|
||||
data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||
data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState
|
||||
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||
data object Dismissing : ContentState
|
||||
data object Loading : ContentState
|
||||
data class Failure(val error: Throwable) : ContentState
|
||||
data object UnknownRoom : ContentState
|
||||
data class Loaded(
|
||||
val roomId: RoomId,
|
||||
val name: String?,
|
||||
|
|
@ -71,9 +90,19 @@ sealed interface ContentState {
|
|||
}
|
||||
|
||||
sealed interface JoinAuthorisationStatus {
|
||||
data object None : JoinAuthorisationStatus
|
||||
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
|
||||
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||
data class IsBanned(val banSender: InviteSender?) : JoinAuthorisationStatus
|
||||
data object IsKnocked : JoinAuthorisationStatus
|
||||
data object CanKnock : JoinAuthorisationStatus
|
||||
data object CanJoin : JoinAuthorisationStatus
|
||||
data object NeedInvite : JoinAuthorisationStatus
|
||||
data object Restricted : JoinAuthorisationStatus
|
||||
data object Unknown : JoinAuthorisationStatus
|
||||
data object Unauthorized : JoinAuthorisationStatus
|
||||
}
|
||||
|
||||
sealed class JoinRoomFailures : Exception() {
|
||||
data object UnauthorizedJoin : JoinRoomFailures()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||
|
||||
|
|
@ -25,10 +26,10 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
|||
override val values: Sequence<JoinRoomState>
|
||||
get() = sequenceOf(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadingContentState()
|
||||
contentState = ContentState.Loading
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = anUnknownContentState()
|
||||
contentState = ContentState.UnknownRoom
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
|
|
@ -40,6 +41,14 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
|||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
|
||||
joinAction = AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
|
||||
joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong"))
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
||||
),
|
||||
|
|
@ -52,9 +61,6 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
|||
aJoinRoomState(
|
||||
contentState = aFailureContentState()
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
roomId = RoomId("!aSpaceId:domain"),
|
||||
|
|
@ -98,23 +104,41 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
|||
name = "A knocked Room",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
|
||||
)
|
||||
)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
name = "A private room",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.NeedInvite
|
||||
)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
name = "A banned room",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
|
||||
InviteSender(
|
||||
userId = UserId("@alice:domain"),
|
||||
displayName = "Alice",
|
||||
avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender),
|
||||
membershipChangeReason = "spamming"
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
name = "A restricted room",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.Restricted,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aFailureContentState(
|
||||
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias()
|
||||
): ContentState {
|
||||
fun aFailureContentState(): ContentState {
|
||||
return ContentState.Failure(
|
||||
roomIdOrAlias = roomIdOrAlias,
|
||||
error = Exception("Error"),
|
||||
)
|
||||
}
|
||||
|
||||
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
|
||||
|
||||
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
|
||||
|
||||
fun aLoadedContentState(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String? = "Element X android",
|
||||
|
|
@ -138,19 +162,23 @@ fun aLoadedContentState(
|
|||
)
|
||||
|
||||
fun aJoinRoomState(
|
||||
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias(),
|
||||
contentState: ContentState = aLoadedContentState(),
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
knockMessage: String = "",
|
||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||
) = JoinRoomState(
|
||||
roomIdOrAlias = roomIdOrAlias,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
joinAction = joinAction,
|
||||
knockAction = knockAction,
|
||||
cancelKnockAction = cancelKnockAction,
|
||||
forgetAction = forgetAction,
|
||||
applicationName = "AppName",
|
||||
knockMessage = knockMessage,
|
||||
eventSink = eventSink
|
||||
|
|
@ -160,10 +188,12 @@ internal fun anInviteSender(
|
|||
userId: UserId = UserId("@bob:domain"),
|
||||
displayName: String = "Bob",
|
||||
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||
membershipChangeReason: String? = null,
|
||||
) = InviteSender(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarData = avatarData,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
)
|
||||
|
||||
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -31,7 +32,6 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -46,7 +46,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubti
|
|||
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.background.LightGradientBackground
|
||||
import io.element.android.libraries.designsystem.components.Announcement
|
||||
import io.element.android.libraries.designsystem.components.AnnouncementType
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -54,16 +55,17 @@ 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.SuperButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
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.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -73,30 +75,33 @@ fun JoinRoomView(
|
|||
onBackClick: () -> Unit,
|
||||
onJoinSuccess: () -> Unit,
|
||||
onKnockSuccess: () -> Unit,
|
||||
onForgetSuccess: () -> Unit,
|
||||
onCancelKnockSuccess: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
LightGradientBackground()
|
||||
HeaderFooterPage(
|
||||
containerColor = Color.Transparent,
|
||||
paddingValues = PaddingValues(16.dp),
|
||||
paddingValues = PaddingValues(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp
|
||||
),
|
||||
topBar = {
|
||||
JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
|
||||
},
|
||||
content = {
|
||||
JoinRoomContent(
|
||||
roomIdOrAlias = state.roomIdOrAlias,
|
||||
contentState = state.contentState,
|
||||
applicationName = state.applicationName,
|
||||
knockMessage = state.knockMessage,
|
||||
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
JoinRoomFooter(
|
||||
state = state,
|
||||
joinAuthorisationStatus = state.joinAuthorisationStatus,
|
||||
onAcceptInvite = {
|
||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
||||
},
|
||||
|
|
@ -112,31 +117,55 @@ fun JoinRoomView(
|
|||
onCancelKnock = {
|
||||
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
|
||||
},
|
||||
onRetry = {
|
||||
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||
onForgetRoom = {
|
||||
state.eventSink(JoinRoomEvents.ForgetRoom)
|
||||
},
|
||||
onGoBack = onBackClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (state.contentState is ContentState.Failure) {
|
||||
RetryDialog(
|
||||
title = stringResource(R.string.screen_join_room_loading_alert_title),
|
||||
content = stringResource(CommonStrings.error_network_or_server_issue),
|
||||
onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) },
|
||||
onDismiss = {
|
||||
state.eventSink(JoinRoomEvents.DismissContent)
|
||||
onBackClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
// This particular error is shown directly in the footer
|
||||
if (!state.isJoinActionUnauthorized) {
|
||||
AsyncActionView(
|
||||
async = state.joinAction,
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
onSuccess = { onJoinSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
}
|
||||
AsyncActionView(
|
||||
async = state.joinAction,
|
||||
onSuccess = { onJoinSuccess() },
|
||||
async = state.knockAction,
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
onSuccess = { onKnockSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.knockAction,
|
||||
onSuccess = { onKnockSuccess() },
|
||||
async = state.forgetAction,
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
onSuccess = { onForgetSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.cancelKnockAction,
|
||||
onSuccess = { onCancelKnockSuccess() },
|
||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||
errorMessage = {
|
||||
stringResource(CommonStrings.error_unknown)
|
||||
},
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(R.string.screen_join_room_cancel_knock_alert_description),
|
||||
|
|
@ -152,13 +181,13 @@ fun JoinRoomView(
|
|||
|
||||
@Composable
|
||||
private fun JoinRoomFooter(
|
||||
state: JoinRoomState,
|
||||
joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||
onAcceptInvite: () -> Unit,
|
||||
onDeclineInvite: () -> Unit,
|
||||
onJoinRoom: () -> Unit,
|
||||
onKnockRoom: () -> Unit,
|
||||
onCancelKnock: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onForgetRoom: () -> Unit,
|
||||
onGoBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -167,79 +196,170 @@ private fun JoinRoomFooter(
|
|||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
if (state.contentState is ContentState.Failure) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_retry),
|
||||
onClick = onRetry,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_go_back),
|
||||
onClick = onGoBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
} else {
|
||||
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||
when (joinAuthorisationStatus) {
|
||||
is JoinAuthorisationStatus.IsInvited -> {
|
||||
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanJoin -> {
|
||||
SuperButton(
|
||||
onClick = onJoinRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_join_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanKnock -> {
|
||||
SuperButton(
|
||||
onClick = onKnockRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_knock_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.IsKnocked -> {
|
||||
when (joinAuthorisationStatus) {
|
||||
is JoinAuthorisationStatus.IsInvited -> {
|
||||
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
OutlinedButton(
|
||||
text = stringResource(R.string.screen_join_room_cancel_knock_action),
|
||||
onClick = onCancelKnock,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
}
|
||||
JoinAuthorisationStatus.Unknown -> Unit
|
||||
}
|
||||
JoinAuthorisationStatus.CanJoin -> {
|
||||
SuperButton(
|
||||
onClick = onJoinRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_join_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.CanKnock -> {
|
||||
SuperButton(
|
||||
onClick = onKnockRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_knock_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
JoinAuthorisationStatus.IsKnocked -> {
|
||||
OutlinedButton(
|
||||
text = stringResource(R.string.screen_join_room_cancel_knock_action),
|
||||
onClick = onCancelKnock,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
JoinAuthorisationStatus.NeedInvite -> {
|
||||
Announcement(
|
||||
title = stringResource(R.string.screen_join_room_invite_required_message),
|
||||
description = null,
|
||||
type = AnnouncementType.Informative(isCritical = false),
|
||||
)
|
||||
}
|
||||
is JoinAuthorisationStatus.IsBanned -> JoinBannedFooter(joinAuthorisationStatus, onForgetRoom)
|
||||
JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom)
|
||||
JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom)
|
||||
JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack)
|
||||
is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack)
|
||||
JoinAuthorisationStatus.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JoinRoomContent(
|
||||
contentState: ContentState,
|
||||
private fun JoinUnauthorizedFooter(
|
||||
onOkClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Announcement(
|
||||
title = stringResource(R.string.screen_join_room_fail_message),
|
||||
description = stringResource(R.string.screen_join_room_fail_reason),
|
||||
type = AnnouncementType.Informative(isCritical = true),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_ok),
|
||||
onClick = onOkClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JoinBannedFooter(
|
||||
status: JoinAuthorisationStatus.IsBanned,
|
||||
onForgetRoom: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val banReason = status.banSender?.membershipChangeReason?.let {
|
||||
stringResource(R.string.screen_join_room_ban_reason, it)
|
||||
}
|
||||
val title = if (status.banSender != null) {
|
||||
stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName)
|
||||
} else {
|
||||
stringResource(R.string.screen_join_room_ban_message)
|
||||
}
|
||||
Announcement(
|
||||
title = title,
|
||||
description = banReason,
|
||||
type = AnnouncementType.Informative(isCritical = true),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(
|
||||
text = stringResource(R.string.screen_join_room_forget_action),
|
||||
onClick = onForgetRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JoinRestrictedFooter(
|
||||
onJoinRoom: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Announcement(
|
||||
title = stringResource(R.string.screen_join_room_join_restricted_message),
|
||||
description = null,
|
||||
type = AnnouncementType.Informative(),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
SuperButton(
|
||||
onClick = onJoinRoom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
buttonSize = ButtonSize.Large,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_join_action),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnsupportedSpaceFooter(
|
||||
applicationName: String,
|
||||
onGoBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Announcement(
|
||||
title = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||
description = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||
type = AnnouncementType.Informative(),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_ok),
|
||||
onClick = onGoBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JoinRoomContent(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
contentState: ContentState,
|
||||
knockMessage: String,
|
||||
onKnockMessageUpdate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -255,67 +375,67 @@ private fun JoinRoomContent(
|
|||
DefaultLoadedContent(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
contentState = contentState,
|
||||
applicationName = applicationName,
|
||||
knockMessage = knockMessage,
|
||||
onKnockMessageUpdate = onKnockMessageUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ContentState.UnknownRoom -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||
},
|
||||
subtitle = {
|
||||
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Loading -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
},
|
||||
subtitle = {
|
||||
PlaceholderAtom(width = 140.dp, height = 20.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.Failure -> {
|
||||
RoomPreviewOrganism(
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
when (contentState.roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> {
|
||||
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||
}
|
||||
is RoomIdOrAlias.Id -> {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
subtitle = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.error_unknown),
|
||||
textAlign = TextAlign.Center,
|
||||
color = ElementTheme.colors.textCriticalPrimary,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
is ContentState.UnknownRoom -> UnknownRoomContent()
|
||||
is ContentState.Loading -> IncompleteContent(roomIdOrAlias, isLoading = true)
|
||||
is ContentState.Dismissing -> IncompleteContent(roomIdOrAlias, isLoading = false)
|
||||
is ContentState.Failure -> IncompleteContent(roomIdOrAlias, isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnknownRoomContent(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp))
|
||||
},
|
||||
title = {
|
||||
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||
},
|
||||
subtitle = {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IncompleteContent(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RoomPreviewOrganism(
|
||||
modifier = modifier,
|
||||
avatar = {
|
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
when (roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> {
|
||||
RoomPreviewSubtitleAtom(roomIdOrAlias.identifier)
|
||||
}
|
||||
is RoomIdOrAlias.Id -> {
|
||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
subtitle = {
|
||||
if (isLoading) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
|
||||
BoxWithConstraints(
|
||||
|
|
@ -336,7 +456,6 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
|
|||
@Composable
|
||||
private fun DefaultLoadedContent(
|
||||
contentState: ContentState.Loaded,
|
||||
applicationName: String,
|
||||
knockMessage: String,
|
||||
onKnockMessageUpdate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -373,21 +492,7 @@ private fun DefaultLoadedContent(
|
|||
InviteSenderView(inviteSender = inviteSender)
|
||||
}
|
||||
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||
if (contentState.roomType == RoomType.Space) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
||||
if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
val supportingText = if (knockMessage.isNotEmpty()) {
|
||||
"${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
|
||||
|
|
@ -461,6 +566,7 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
|
|||
onBackClick = { },
|
||||
onJoinSuccess = { },
|
||||
onKnockSuccess = { },
|
||||
onForgetSuccess = { },
|
||||
onCancelKnockSuccess = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.joinroom.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ForgetRoom {
|
||||
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultForgetRoom @Inject constructor(private val client: MatrixClient) : ForgetRoom {
|
||||
override suspend fun invoke(roomId: RoomId): Result<Unit> {
|
||||
return client
|
||||
.getPendingRoom(roomId)
|
||||
?.forget()
|
||||
?: Result.failure(IllegalStateException("No pending room found"))
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ object JoinRoomModule {
|
|||
joinRoom: JoinRoom,
|
||||
knockRoom: KnockRoom,
|
||||
cancelKnockRoom: CancelKnockRoom,
|
||||
forgetRoom: ForgetRoom,
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
buildMeta: BuildMeta,
|
||||
): JoinRoomPresenter.Factory {
|
||||
|
|
@ -52,6 +53,7 @@ object JoinRoomModule {
|
|||
matrixClient = client,
|
||||
joinRoom = joinRoom,
|
||||
knockRoom = knockRoom,
|
||||
forgetRoom = forgetRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
buildMeta = buildMeta,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
<string name="screen_join_room_knock_message_description">"Message (optional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
|
||||
<string name="screen_join_room_loading_alert_message">"We could not display the room preview. This may be due to network or server issues."</string>
|
||||
<string name="screen_join_room_loading_alert_title">"We couldn’t display this room preview"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.joinroom.impl
|
||||
|
||||
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeForgetRoom(
|
||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||
) : ForgetRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||
lambda(roomId)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
|
@ -24,9 +25,11 @@ 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.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.exception.ErrorKind
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
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_NAME
|
||||
|
|
@ -34,6 +37,7 @@ import io.element.android.libraries.matrix.test.A_SERVER_LIST
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
|
|
@ -49,6 +53,7 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import java.util.Optional
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class JoinRoomPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
|
@ -58,12 +63,10 @@ class JoinRoomPresenterTest {
|
|||
val presenter = createJoinRoomPresenter()
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||
assertThat(state.contentState).isEqualTo(ContentState.Loading)
|
||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(state.applicationName).isEqualTo("AppName")
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -226,9 +229,52 @@ class JoinRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is joined with unauthorized error, then the authorisation status is unauthorized`() = runTest {
|
||||
val roomDescription = aRoomDescription()
|
||||
val presenter = createJoinRoomPresenter(
|
||||
roomDescription = Optional.of(roomDescription),
|
||||
joinRoomLambda = { _, _, _ ->
|
||||
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden"))
|
||||
},
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.JoinRoom)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin))
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true)
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
|
|
@ -246,8 +292,8 @@ class JoinRoomPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest {
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false)
|
||||
fun `present - when room is left and join rule null then join authorization is equal to Unknown`() = runTest {
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
|
|
@ -327,6 +373,20 @@ class JoinRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room preview join rule is Private then join authorization is equal to NeedInvite`() = runTest {
|
||||
val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.UNKNOWN)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
roomDescription = Optional.of(roomDescription)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - emit knock room event`() = runTest {
|
||||
val knockMessage = "Knock message"
|
||||
|
|
@ -405,24 +465,58 @@ class JoinRoomPresenterTest {
|
|||
.with(value(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - emit forget room event`() = runTest {
|
||||
val forgetRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val forgetRoomFailure = lambdaRecorder { _: RoomId ->
|
||||
Result.failure<Unit>(RuntimeException("Failed to forget room"))
|
||||
}
|
||||
val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess)
|
||||
val presenter = createJoinRoomPresenter(forgetRoom = fakeForgetRoom)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.ForgetRoom)
|
||||
}
|
||||
|
||||
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
fakeForgetRoom.lambda = forgetRoomFailure
|
||||
state.eventSink(JoinRoomEvents.ForgetRoom)
|
||||
}
|
||||
|
||||
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.forgetAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
}
|
||||
assert(forgetRoomFailure)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
assert(forgetRoomSuccess)
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.success(
|
||||
RoomPreviewInfo(
|
||||
aRoomPreviewInfo(
|
||||
roomId = A_ROOM_ID,
|
||||
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||
name = "Room name",
|
||||
topic = "Room topic",
|
||||
avatarUrl = "avatarUrl",
|
||||
numberOfJoinedMembers = 2,
|
||||
roomType = RoomType.Room,
|
||||
isSpace = false,
|
||||
isHistoryWorldReadable = false,
|
||||
isJoined = false,
|
||||
isInvited = false,
|
||||
isPublic = true,
|
||||
canKnock = false,
|
||||
joinRule = JoinRule.Public,
|
||||
currentUserMembership = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -450,6 +544,86 @@ class JoinRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded as Private `() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.success(
|
||||
aRoomPreviewInfo(joinRule = JoinRule.Private)
|
||||
)
|
||||
}
|
||||
)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
matrixClient = client
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded as KnockRestricted `() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.success(
|
||||
aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList()))
|
||||
)
|
||||
}
|
||||
)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
matrixClient = client
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanKnock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded as Restricted `() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.success(
|
||||
aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList()))
|
||||
)
|
||||
}
|
||||
)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
matrixClient = client
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Restricted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded as Space `() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.success(
|
||||
aRoomPreviewInfo(isSpace = true)
|
||||
)
|
||||
}
|
||||
)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
matrixClient = client
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsSpace("AppName"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
|
|
@ -464,35 +638,27 @@ class JoinRoomPresenterTest {
|
|||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.contentState).isEqualTo(
|
||||
ContentState.Failure(
|
||||
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||
error = AN_EXCEPTION
|
||||
)
|
||||
ContentState.Failure(error = AN_EXCEPTION)
|
||||
)
|
||||
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.contentState).isEqualTo(
|
||||
ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())
|
||||
)
|
||||
assertThat(state.contentState).isEqualTo(ContentState.Loading)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.contentState).isEqualTo(
|
||||
ContentState.Failure(
|
||||
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||
error = AN_EXCEPTION
|
||||
)
|
||||
ContentState.Failure(error = AN_EXCEPTION)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
|
||||
fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewInfoResult = { _, _ ->
|
||||
Result.failure(Exception("403"))
|
||||
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden"))
|
||||
}
|
||||
)
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -501,11 +667,7 @@ class JoinRoomPresenterTest {
|
|||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.contentState).isEqualTo(
|
||||
ContentState.UnknownRoom(
|
||||
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||
)
|
||||
)
|
||||
assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -521,6 +683,7 @@ class JoinRoomPresenterTest {
|
|||
},
|
||||
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
|
||||
forgetRoom: ForgetRoom = FakeForgetRoom(),
|
||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
||||
): JoinRoomPresenter {
|
||||
|
|
@ -534,6 +697,7 @@ class JoinRoomPresenterTest {
|
|||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||
knockRoom = knockRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
forgetRoom = forgetRoom,
|
||||
buildMeta = buildMeta,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||
)
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class JoinRoomViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on Go back when a space is displayed invokes the expected callback`() {
|
||||
fun `clicking on ok when a space is displayed invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
rule.setJoinRoomView(
|
||||
|
|
@ -188,9 +188,38 @@ class JoinRoomViewTest {
|
|||
),
|
||||
onBackClick = it
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_go_back)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on ok when user is unauthorized the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
rule.setJoinRoomView(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(),
|
||||
joinAction = AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onBackClick = it
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on forget when user is banned invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||
rule.setJoinRoomView(
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null)),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_join_room_forget_action)
|
||||
eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
|
||||
|
|
@ -199,6 +228,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
|||
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
onForgetSuccess: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
JoinRoomView(
|
||||
|
|
@ -206,7 +236,8 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
|||
onBackClick = onBackClick,
|
||||
onJoinSuccess = onJoinSuccess,
|
||||
onKnockSuccess = onKnockSuccess,
|
||||
onCancelKnockSuccess = onCancelKnockSuccess
|
||||
onForgetSuccess = onForgetSuccess,
|
||||
onCancelKnockSuccess = onCancelKnockSuccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ internal fun SuggestionsPickerViewPreview() {
|
|||
normalizedPowerLevel = 0L,
|
||||
isIgnored = false,
|
||||
role = RoomMember.Role.USER,
|
||||
membershipChangeReason = null,
|
||||
)
|
||||
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||
SuggestionsPickerView(
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ package io.element.android.features.roomaliasresolver.impl
|
|||
|
||||
sealed interface RoomAliasResolverEvents {
|
||||
data object Retry : RoomAliasResolverEvents
|
||||
data object DismissError : RoomAliasResolverEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor(
|
|||
fun handleEvents(event: RoomAliasResolverEvents) {
|
||||
when (event) {
|
||||
RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState)
|
||||
RoomAliasResolverEvents.DismissError -> resolveState.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor(
|
|||
suspend {
|
||||
matrixClient.resolveRoomAlias(roomAlias)
|
||||
.getOrThrow()
|
||||
.getOrElse { error("Failed to resolve room alias $roomAlias") }
|
||||
.getOrElse { throw RoomAliasResolverFailures.UnknownAlias }
|
||||
}.runCatchingUpdatingState(resolveState)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,3 +18,7 @@ data class RoomAliasResolverState(
|
|||
val resolveState: AsyncData<ResolvedRoomAlias>,
|
||||
val eventSink: (RoomAliasResolverEvents) -> Unit
|
||||
)
|
||||
|
||||
sealed class RoomAliasResolverFailures : Exception() {
|
||||
data object UnknownAlias : RoomAliasResolverFailures()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
|
||||
open class RoomAliasResolverStateProvider : PreviewParameterProvider<RoomAliasResolverState> {
|
||||
|
|
@ -17,10 +18,10 @@ open class RoomAliasResolverStateProvider : PreviewParameterProvider<RoomAliasRe
|
|||
get() = sequenceOf(
|
||||
aRoomAliasResolverState(),
|
||||
aRoomAliasResolverState(
|
||||
resolveState = AsyncData.Loading(),
|
||||
resolveState = AsyncData.Failure(ClientException.Generic("Something went wrong")),
|
||||
),
|
||||
aRoomAliasResolverState(
|
||||
resolveState = AsyncData.Failure(Exception("Error")),
|
||||
resolveState = AsyncData.Failure(RoomAliasResolverFailures.UnknownAlias),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@
|
|||
|
||||
package io.element.android.features.roomaliasresolver.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -21,25 +20,22 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.compound.theme.ElementTheme
|
||||
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.atoms.RoomPreviewSubtitleAtom
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||
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.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
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.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -50,66 +46,72 @@ fun RoomAliasResolverView(
|
|||
onSuccess: (ResolvedRoomAlias) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val latestOnSuccess by rememberUpdatedState(onSuccess)
|
||||
LaunchedEffect(state.resolveState) {
|
||||
if (state.resolveState is AsyncData.Success) {
|
||||
latestOnSuccess(state.resolveState.data)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
LightGradientBackground()
|
||||
HeaderFooterPage(
|
||||
containerColor = Color.Transparent,
|
||||
paddingValues = PaddingValues(16.dp),
|
||||
paddingValues = PaddingValues(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp
|
||||
),
|
||||
topBar = {
|
||||
RoomAliasResolverTopBar(onBackClick = onBackClick)
|
||||
},
|
||||
content = {
|
||||
RoomAliasResolverContent(state = state)
|
||||
RoomAliasResolverContent(roomAlias = state.roomAlias, isLoading = state.resolveState.isLoading())
|
||||
},
|
||||
footer = {
|
||||
RoomAliasResolverFooter(
|
||||
state = state,
|
||||
)
|
||||
)
|
||||
ResolvedRoomAliasView(
|
||||
resolvedRoomAlias = state.resolveState,
|
||||
onSuccess = onSuccess,
|
||||
onRetry = { state.eventSink(RoomAliasResolverEvents.Retry) },
|
||||
onDismissError = {
|
||||
state.eventSink(RoomAliasResolverEvents.DismissError)
|
||||
onBackClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomAliasResolverFooter(
|
||||
state: RoomAliasResolverState,
|
||||
modifier: Modifier = Modifier,
|
||||
private fun ResolvedRoomAliasView(
|
||||
resolvedRoomAlias: AsyncData<ResolvedRoomAlias>,
|
||||
onSuccess: (ResolvedRoomAlias) -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
) {
|
||||
when (state.resolveState) {
|
||||
is AsyncData.Failure -> {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_retry),
|
||||
onClick = {
|
||||
state.eventSink(RoomAliasResolverEvents.Retry)
|
||||
},
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Large,
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading -> {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
when (resolvedRoomAlias) {
|
||||
is AsyncData.Success -> {
|
||||
val latestOnSuccess by rememberUpdatedState(onSuccess)
|
||||
LaunchedEffect(Unit) {
|
||||
latestOnSuccess(resolvedRoomAlias.data)
|
||||
}
|
||||
}
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Success -> Unit
|
||||
is AsyncData.Failure -> {
|
||||
if (resolvedRoomAlias.error is RoomAliasResolverFailures.UnknownAlias) {
|
||||
ErrorDialog(
|
||||
title = stringResource(id = R.string.screen_join_room_loading_alert_title),
|
||||
content = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure),
|
||||
onSubmit = onDismissError
|
||||
)
|
||||
} else {
|
||||
RetryDialog(
|
||||
title = stringResource(id = R.string.screen_join_room_loading_alert_title),
|
||||
content = stringResource(id = CommonStrings.error_network_or_server_issue),
|
||||
onRetry = onRetry,
|
||||
onDismiss = onDismissError
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomAliasResolverContent(
|
||||
state: RoomAliasResolverState,
|
||||
roomAlias: RoomAlias,
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
RoomPreviewOrganism(
|
||||
|
|
@ -118,20 +120,13 @@ private fun RoomAliasResolverContent(
|
|||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||
},
|
||||
title = {
|
||||
RoomPreviewTitleAtom(state.roomAlias.value)
|
||||
RoomPreviewSubtitleAtom(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 = ElementTheme.colors.textCriticalPrimary,
|
||||
)
|
||||
if (isLoading) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
},
|
||||
memberCount = {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_loading_alert_title">"We couldn’t display this room preview"</string>
|
||||
<string name="screen_room_alias_resolver_resolve_alias_failure">"Failed to resolve room alias."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ fun aDmRoomMember(
|
|||
normalizedPowerLevel: Long = powerLevel,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.USER,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
|
|
@ -74,6 +75,7 @@ fun aDmRoomMember(
|
|||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason
|
||||
)
|
||||
|
||||
fun aRoomDetailsState(
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ fun aRoomMember(
|
|||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.USER,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
|
|
@ -136,6 +137,7 @@ fun aRoomMember(
|
|||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
)
|
||||
|
||||
fun aRoomMemberList() = persistentListOf(
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ fun aMatrixRoom(
|
|||
topic = topic,
|
||||
avatarUrl = avatarUrl,
|
||||
isDirect = isDirect,
|
||||
isPublic = isPublic,
|
||||
joinRule = joinRule,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ class RoomDetailsPresenterTest {
|
|||
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
|
||||
val roomInfo = aRoomInfo(
|
||||
name = A_ROOM_NAME,
|
||||
isPublic = true,
|
||||
topic = A_ROOM_TOPIC,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
pinnedEventIds = listOf(AN_EVENT_ID),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class RoomMembersModerationPresenterTest {
|
|||
canBanResult = { Result.success(true) },
|
||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
||||
).apply {
|
||||
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
|
||||
givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2))
|
||||
}
|
||||
val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
|
||||
presenter.test {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ internal fun anInviteSender(
|
|||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarData = avatarData,
|
||||
membershipChangeReason = null,
|
||||
)
|
||||
|
||||
internal fun aRoomListRoomSummary(
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@
|
|||
<string name="screen_session_verification_cancelled_subtitle">"Something doesn’t seem right. Either the request timed out or the request was denied."</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Confirm that the emojis below match those shown on your other session."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Compare emojis"</string>
|
||||
<string name="screen_session_verification_compare_emojis_user_subtitle">"Confirm that the emojis below match those shown on the other user’s device."</string>
|
||||
<string name="screen_session_verification_compare_numbers_subtitle">"Confirm that the numbers below match those shown on your other session."</string>
|
||||
<string name="screen_session_verification_compare_numbers_title">"Compare numbers"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."</string>
|
||||
<string name="screen_session_verification_complete_user_subtitle">"Now you can trust the identity of this user when sending or receiving messages."</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Enter recovery key"</string>
|
||||
<string name="screen_session_verification_failed_subtitle">"Either the request timed out, the request was denied, or there was a verification mismatch."</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Prove it’s you in order to access your encrypted message history."</string>
|
||||
|
|
@ -37,8 +39,14 @@
|
|||
<string name="screen_session_verification_they_match">"They match"</string>
|
||||
<string name="screen_session_verification_use_another_device_subtitle">"Make sure you have the app open in the other device before starting verification from here."</string>
|
||||
<string name="screen_session_verification_use_another_device_title">"Open the app on another verified device"</string>
|
||||
<string name="screen_session_verification_user_initiator_subtitle">"For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate."</string>
|
||||
<string name="screen_session_verification_user_initiator_title">"Verify this user?"</string>
|
||||
<string name="screen_session_verification_user_responder_subtitle">"For extra security, another user wants to verify your identity. You’ll be shown a set of emojis to compare."</string>
|
||||
<string name="screen_session_verification_waiting_another_device_subtitle">"You should see a popup on the other device. Start the verification from there now."</string>
|
||||
<string name="screen_session_verification_waiting_another_device_title">"Start verification on the other device"</string>
|
||||
<string name="screen_session_verification_waiting_other_device_title">"Waiting for the other device"</string>
|
||||
<string name="screen_session_verification_waiting_other_user_title">"Waiting for the other user"</string>
|
||||
<string name="screen_session_verification_waiting_subtitle">"Once accepted you’ll be able to continue with the verification."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accept the request to start the verification process in your other session to continue."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"Waiting to accept request"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
|||
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.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
|
|
@ -55,7 +55,7 @@ interface MatrixClient : Closeable {
|
|||
val sessionCoroutineScope: CoroutineScope
|
||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
suspend fun getPendingRoom(roomId: RoomId): PendingRoom?
|
||||
suspend fun getPendingRoom(roomId: RoomId): RoomPreview?
|
||||
suspend fun findDM(userId: UserId): RoomId?
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ data class MatrixRoomInfo(
|
|||
val topic: String?,
|
||||
val avatarUrl: String?,
|
||||
val isDirect: Boolean,
|
||||
val isPublic: Boolean,
|
||||
val joinRule: JoinRule?,
|
||||
val isSpace: Boolean,
|
||||
val isTombstoned: Boolean,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ data class RoomMember(
|
|||
val normalizedPowerLevel: Long,
|
||||
val isIgnored: Boolean,
|
||||
val role: Role,
|
||||
val membershipChangeReason: String?,
|
||||
) {
|
||||
/**
|
||||
* Role of the RoomMember, based on its [powerLevel].
|
||||
|
|
|
|||
|
|
@ -10,11 +10,16 @@ package io.element.android.libraries.matrix.api.room
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
/** A reference to a room the current user has knocked to or has been invited to, with the ability to leave the room. */
|
||||
interface PendingRoom : AutoCloseable {
|
||||
/** A reference to a room either invited, knocked or banned. */
|
||||
interface RoomPreview : AutoCloseable {
|
||||
val sessionId: SessionId
|
||||
val roomId: RoomId
|
||||
|
||||
/** Leave the room ie.decline invite or cancel knock. */
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Forget the room if we had access to it, and it was left or banned.
|
||||
*/
|
||||
suspend fun forget(): Result<Unit>
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@ 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
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
|
||||
data class RoomPreviewInfo(
|
||||
/** The room id for this room. */
|
||||
|
|
@ -28,12 +30,8 @@ data class RoomPreviewInfo(
|
|||
val roomType: RoomType,
|
||||
/** 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,
|
||||
/** the membership of the current user. */
|
||||
val membership: CurrentUserMembership?,
|
||||
/** The room's join rule. */
|
||||
val joinRule: JoinRule,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.core.bool.orFalse
|
|||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
|
|
@ -32,9 +33,9 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
|||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
|
|
@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
|
||||
|
|
@ -261,8 +263,8 @@ class RustMatrixClient(
|
|||
return roomFactory.create(roomId)
|
||||
}
|
||||
|
||||
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||
return roomFactory.createPendingRoom(roomId)
|
||||
override suspend fun getPendingRoom(roomId: RoomId): RoomPreview? {
|
||||
return roomFactory.createRoomPreview(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -393,7 +395,7 @@ class RustMatrixClient(
|
|||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
|
||||
override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
|
|
@ -407,7 +409,7 @@ class RustMatrixClient(
|
|||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
null
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
}
|
||||
|
||||
override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> = withContext(
|
||||
|
|
@ -421,7 +423,7 @@ class RustMatrixClient(
|
|||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
null
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
}
|
||||
|
||||
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
|
|
@ -456,7 +458,7 @@ class RustMatrixClient(
|
|||
}.use { roomPreview ->
|
||||
RoomPreviewInfoMapper.map(roomPreview.info())
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
}
|
||||
|
||||
override fun syncService(): SyncService = rustSyncService
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ class MatrixRoomInfoMapper {
|
|||
topic = it.topic,
|
||||
avatarUrl = it.avatarUrl,
|
||||
isDirect = it.isDirect,
|
||||
isPublic = it.isPublic,
|
||||
joinRule = it.joinRule?.map(),
|
||||
isSpace = it.isSpace,
|
||||
isTombstoned = it.isTombstoned,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ 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.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||
|
|
@ -28,7 +28,6 @@ import kotlinx.coroutines.NonCancellable
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListException
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
|
|
@ -36,7 +35,6 @@ import timber.log.Timber
|
|||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
|
||||
private const val CACHE_SIZE = 16
|
||||
private val PENDING_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED)
|
||||
|
||||
class RustRoomFactory(
|
||||
private val sessionId: SessionId,
|
||||
|
|
@ -125,7 +123,7 @@ class RustRoomFactory(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun createPendingRoom(roomId: RoomId): PendingRoom? = withContext(dispatcher) {
|
||||
suspend fun createRoomPreview(roomId: RoomId): RoomPreview? = withContext(dispatcher) {
|
||||
if (isDestroyed) {
|
||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||
return@withContext null
|
||||
|
|
@ -135,17 +133,17 @@ class RustRoomFactory(
|
|||
Timber.d("Room not found for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
if (roomListItem.membership() !in PENDING_MEMBERSHIPS) {
|
||||
Timber.d("Room $roomId is not in pending state")
|
||||
if (roomListItem.membership() !in RustRoomPreview.ALLOWED_MEMBERSHIPS) {
|
||||
Timber.d("Room $roomId is not in allowed membership")
|
||||
return@withContext null
|
||||
}
|
||||
val innerRoom = try {
|
||||
roomListItem.previewRoom(via = emptyList())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get pending room for $roomId")
|
||||
Timber.e(e, "Failed to get room preview for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
RustPendingRoom(
|
||||
RustRoomPreview(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
inner = innerRoom,
|
||||
|
|
|
|||
|
|
@ -9,22 +9,31 @@ package io.element.android.libraries.matrix.impl.room
|
|||
|
||||
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.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview as InnerRoomPreview
|
||||
|
||||
class RustPendingRoom(
|
||||
class RustRoomPreview(
|
||||
override val sessionId: SessionId,
|
||||
override val roomId: RoomId,
|
||||
private val inner: RoomPreview,
|
||||
private val inner: InnerRoomPreview,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
) : PendingRoom {
|
||||
) : RoomPreview {
|
||||
companion object {
|
||||
val ALLOWED_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED, Membership.BANNED)
|
||||
}
|
||||
|
||||
override suspend fun leave(): Result<Unit> = runCatching {
|
||||
inner.leave()
|
||||
}.onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(roomId)
|
||||
}
|
||||
|
||||
override suspend fun forget(): Result<Unit> = runCatching {
|
||||
inner.forget()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
inner.destroy()
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ object RoomMemberMapper {
|
|||
normalizedPowerLevel = roomMember.normalizedPowerLevel,
|
||||
isIgnored = roomMember.isIgnored,
|
||||
role = mapRole(roomMember.suggestedRoleForPowerLevel),
|
||||
membershipChangeReason = roomMember.membershipChangeReason
|
||||
)
|
||||
|
||||
fun mapRole(role: RoomMemberRole): RoomMember.Role =
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ import io.element.android.libraries.core.bool.orFalse
|
|||
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.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.map
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.RoomPreviewInfo as RustRoomPreviewInfo
|
||||
|
||||
object RoomPreviewInfoMapper {
|
||||
|
|
@ -27,10 +26,8 @@ object RoomPreviewInfoMapper {
|
|||
numberOfJoinedMembers = info.numJoinedMembers.toLong(),
|
||||
roomType = info.roomType.map(),
|
||||
isHistoryWorldReadable = info.isHistoryWorldReadable.orFalse(),
|
||||
isJoined = info.membership == Membership.JOINED,
|
||||
isInvited = info.membership == Membership.INVITED,
|
||||
isPublic = info.joinRule == JoinRule.Public,
|
||||
canKnock = info.joinRule == JoinRule.Knock
|
||||
membership = info.membership?.map(),
|
||||
joinRule = info.joinRule.map(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ class MatrixRoomInfoMapperTest {
|
|||
topic = "topic",
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
isDirect = true,
|
||||
isPublic = false,
|
||||
isSpace = false,
|
||||
isTombstoned = false,
|
||||
isFavorite = false,
|
||||
|
|
@ -167,7 +166,6 @@ class MatrixRoomInfoMapperTest {
|
|||
topic = null,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
isPublic = true,
|
||||
joinRule = null,
|
||||
isSpace = false,
|
||||
isTombstoned = false,
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@
|
|||
package io.element.android.libraries.matrix.impl.room.preview
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
|
||||
|
||||
class RoomPreviewInfoMapperTest {
|
||||
@Test
|
||||
|
|
@ -23,7 +25,7 @@ class RoomPreviewInfoMapperTest {
|
|||
assertThat(
|
||||
RoomPreviewInfoMapper.map(
|
||||
info = aRustRoomPreviewInfo(
|
||||
membership = null,
|
||||
membership = Membership.JOINED,
|
||||
)
|
||||
)
|
||||
).isEqualTo(
|
||||
|
|
@ -36,10 +38,8 @@ class RoomPreviewInfoMapperTest {
|
|||
numberOfJoinedMembers = 1L,
|
||||
roomType = RoomType.Room,
|
||||
isHistoryWorldReadable = true,
|
||||
isJoined = false,
|
||||
isInvited = false,
|
||||
isPublic = true,
|
||||
canKnock = false,
|
||||
membership = CurrentUserMembership.JOINED,
|
||||
joinRule = JoinRule.Public,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ class RoomPreviewInfoMapperTest {
|
|||
info = aRustRoomPreviewInfo(
|
||||
canonicalAlias = null,
|
||||
membership = Membership.JOINED,
|
||||
joinRule = JoinRule.Knock,
|
||||
joinRule = RustJoinRule.Knock,
|
||||
)
|
||||
)
|
||||
).isEqualTo(
|
||||
|
|
@ -64,10 +64,8 @@ class RoomPreviewInfoMapperTest {
|
|||
numberOfJoinedMembers = 1L,
|
||||
roomType = RoomType.Room,
|
||||
isHistoryWorldReadable = true,
|
||||
isJoined = true,
|
||||
isInvited = false,
|
||||
isPublic = false,
|
||||
canKnock = true,
|
||||
membership = CurrentUserMembership.JOINED,
|
||||
joinRule = JoinRule.Knock,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
|
|||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
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.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
|
|
@ -105,7 +105,7 @@ class FakeMatrixClient(
|
|||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
private var findDmResult: RoomId? = A_ROOM_ID
|
||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||
val getPendingRoomResults = mutableMapOf<RoomId, PendingRoom>()
|
||||
val getRoomPreviewResults = mutableMapOf<RoomId, RoomPreview>()
|
||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
||||
|
|
@ -132,8 +132,8 @@ class FakeMatrixClient(
|
|||
return getRoomResults[roomId]
|
||||
}
|
||||
|
||||
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||
return getPendingRoomResults[roomId]
|
||||
override suspend fun getPendingRoom(roomId: RoomId): RoomPreview? {
|
||||
return getRoomPreviewResults[roomId]
|
||||
}
|
||||
|
||||
override suspend fun findDM(userId: UserId): RoomId? {
|
||||
|
|
|
|||
|
|
@ -9,20 +9,25 @@ package io.element.android.libraries.matrix.test.room
|
|||
|
||||
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.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomPreview
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakePendingRoom(
|
||||
class FakeRoomPreview(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
|
||||
) : PendingRoom {
|
||||
private val declineInviteResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val forgetRoomResult: () -> Result<Unit> = { lambdaError() },
|
||||
) : RoomPreview {
|
||||
override suspend fun leave(): Result<Unit> = simulateLongTask {
|
||||
declineInviteResult()
|
||||
}
|
||||
|
||||
override suspend fun forget(): Result<Unit> = simulateLongTask {
|
||||
forgetRoomResult()
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
|
|
@ -34,7 +34,6 @@ fun aRoomInfo(
|
|||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = AN_AVATAR_URL,
|
||||
isDirect: Boolean = false,
|
||||
isPublic: Boolean = true,
|
||||
joinRule: JoinRule? = JoinRule.Public,
|
||||
isSpace: Boolean = false,
|
||||
isTombstoned: Boolean = false,
|
||||
|
|
@ -67,7 +66,6 @@ fun aRoomInfo(
|
|||
topic = topic,
|
||||
avatarUrl = avatarUrl,
|
||||
isDirect = isDirect,
|
||||
isPublic = isPublic,
|
||||
joinRule = joinRule,
|
||||
isSpace = isSpace,
|
||||
isTombstoned = isTombstoned,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ fun aRoomMember(
|
|||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.USER,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
|
|
@ -31,4 +32,5 @@ fun aRoomMember(
|
|||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test.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.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
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_TOPIC
|
||||
|
||||
fun aRoomPreviewInfo(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String? = A_ROOM_NAME,
|
||||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = AN_AVATAR_URL,
|
||||
joinRule: JoinRule = JoinRule.Public,
|
||||
isSpace: Boolean = false,
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
currentUserMembership: CurrentUserMembership? = null,
|
||||
numberOfJoinedMembers: Long = 1,
|
||||
isHistoryWorldReadable: Boolean = true,
|
||||
) = RoomPreviewInfo(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
topic = topic,
|
||||
avatarUrl = avatarUrl,
|
||||
joinRule = joinRule,
|
||||
canonicalAlias = canonicalAlias,
|
||||
numberOfJoinedMembers = numberOfJoinedMembers,
|
||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||
isHistoryWorldReadable = isHistoryWorldReadable,
|
||||
membership = currentUserMembership,
|
||||
)
|
||||
|
|
@ -47,7 +47,6 @@ fun aRoomSummary(
|
|||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = null,
|
||||
isDirect: Boolean = false,
|
||||
isPublic: Boolean = true,
|
||||
joinRule: JoinRule? = JoinRule.Public,
|
||||
isSpace: Boolean = false,
|
||||
isTombstoned: Boolean = false,
|
||||
|
|
@ -82,7 +81,6 @@ fun aRoomSummary(
|
|||
topic = topic,
|
||||
avatarUrl = avatarUrl,
|
||||
isDirect = isDirect,
|
||||
isPublic = isPublic,
|
||||
joinRule = joinRule,
|
||||
isSpace = isSpace,
|
||||
isTombstoned = isTombstoned,
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ internal fun InviteSenderViewPreview() = ElementPreview {
|
|||
id = "@bob:example.com",
|
||||
name = "Bob",
|
||||
url = null,
|
||||
size = AvatarSize.InviteSender
|
||||
)
|
||||
size = AvatarSize.InviteSender,
|
||||
),
|
||||
membershipChangeReason = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ data class InviteSender(
|
|||
val userId: UserId,
|
||||
val displayName: String,
|
||||
val avatarData: AvatarData,
|
||||
val membershipChangeReason: String?,
|
||||
) {
|
||||
@Composable
|
||||
fun annotatedString(): AnnotatedString {
|
||||
|
|
@ -52,4 +53,5 @@ fun RoomMember.toInviteSender() = InviteSender(
|
|||
userId = userId,
|
||||
displayName = displayName ?: "",
|
||||
avatarData = getAvatarData(size = AvatarSize.InviteSender),
|
||||
membershipChangeReason = membershipChangeReason
|
||||
)
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_verified">"Verified"</string>
|
||||
<string name="common_verify_device">"Verify device"</string>
|
||||
<string name="common_verify_identity">"Verify identity"</string>
|
||||
<string name="common_verify_user">"Verify user"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_voice_message">"Voice message"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
|
|
@ -297,6 +298,7 @@ Reason: %1$s."</string>
|
|||
<string name="error_missing_location_auth_android">"%1$s does not have permission to access your location. You can enable access in Settings."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s does not have permission to access your location. Enable access below."</string>
|
||||
<string name="error_missing_microphone_voice_rationale_android">"%1$s does not have permission to access your microphone. Enable access to record a voice message."</string>
|
||||
<string name="error_network_or_server_issue">"This may be due to network or server issues."</string>
|
||||
<string name="error_room_address_already_exists">"This room address already exists. Please try editing the room address field or change the room name"</string>
|
||||
<string name="error_room_address_invalid_symbols">"Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Some messages have not been sent"</string>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@
|
|||
{
|
||||
"name" : ":features:roomaliasresolver:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_alias_resolver_.*"
|
||||
"screen_room_alias_resolver_.*",
|
||||
"screen.join_room.loading_alert_title"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue