Merge pull request #3725 from element-hq/feature/fga/knock_request_to_join
Feature: knock request to join
This commit is contained in:
commit
52053fcf38
62 changed files with 758 additions and 339 deletions
|
|
@ -94,8 +94,8 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
client.getInvitedRoom(roomId)?.use {
|
client.getPendingRoom(roomId)?.use {
|
||||||
it.declineInvite().getOrThrow()
|
it.leave().getOrThrow()
|
||||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||||
}
|
}
|
||||||
roomId
|
roomId
|
||||||
|
|
|
||||||
|
|
@ -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_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeInvitedRoom
|
import io.element.android.libraries.matrix.test.room.FakePendingRoom
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
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.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
|
|
@ -78,7 +78,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteFailure)
|
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteFailure)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -121,7 +121,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteSuccess)
|
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteSuccess)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ sealed interface JoinRoomEvents {
|
||||||
data object RetryFetchingContent : JoinRoomEvents
|
data object RetryFetchingContent : JoinRoomEvents
|
||||||
data object JoinRoom : JoinRoomEvents
|
data object JoinRoom : JoinRoomEvents
|
||||||
data object KnockRoom : JoinRoomEvents
|
data object KnockRoom : JoinRoomEvents
|
||||||
data object ClearError : JoinRoomEvents
|
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
|
||||||
|
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
|
||||||
|
data object ClearActionStates : JoinRoomEvents
|
||||||
data object AcceptInvite : JoinRoomEvents
|
data object AcceptInvite : JoinRoomEvents
|
||||||
data object DeclineInvite : JoinRoomEvents
|
data object DeclineInvite : JoinRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = ::navigateUp,
|
onBackClick = ::navigateUp,
|
||||||
onJoinSuccess = ::navigateUp,
|
onJoinSuccess = ::navigateUp,
|
||||||
onKnockSuccess = ::navigateUp,
|
onCancelKnockSuccess = ::navigateUp,
|
||||||
|
onKnockSuccess = { },
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
acceptDeclineInviteView.Render(
|
acceptDeclineInviteView.Render(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -24,6 +25,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
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.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
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.KnockRoom
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
@ -46,6 +48,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
|
private const val MAX_KNOCK_MESSAGE_LENGTH = 500
|
||||||
|
|
||||||
class JoinRoomPresenter @AssistedInject constructor(
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomId: RoomId,
|
@Assisted private val roomId: RoomId,
|
||||||
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
|
@ -55,6 +59,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val joinRoom: JoinRoom,
|
private val joinRoom: JoinRoom,
|
||||||
private val knockRoom: KnockRoom,
|
private val knockRoom: KnockRoom,
|
||||||
|
private val cancelKnockRoom: CancelKnockRoom,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
|
|
@ -75,6 +80,8 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
|
val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
|
||||||
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val knockAction: 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) }
|
||||||
|
var knockMessage by rememberSaveable { mutableStateOf("") }
|
||||||
val contentState by produceState<ContentState>(
|
val contentState by produceState<ContentState>(
|
||||||
initialValue = ContentState.Loading(roomIdOrAlias),
|
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||||
key1 = roomInfo,
|
key1 = roomInfo,
|
||||||
|
|
@ -110,7 +117,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
fun handleEvents(event: JoinRoomEvents) {
|
fun handleEvents(event: JoinRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
|
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
|
||||||
JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction)
|
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
|
||||||
JoinRoomEvents.AcceptInvite -> {
|
JoinRoomEvents.AcceptInvite -> {
|
||||||
val inviteData = contentState.toInviteData() ?: return
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
|
|
@ -123,12 +130,17 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
|
||||||
JoinRoomEvents.RetryFetchingContent -> {
|
JoinRoomEvents.RetryFetchingContent -> {
|
||||||
retryCount++
|
retryCount++
|
||||||
}
|
}
|
||||||
JoinRoomEvents.ClearError -> {
|
JoinRoomEvents.ClearActionStates -> {
|
||||||
knockAction.value = AsyncAction.Uninitialized
|
knockAction.value = AsyncAction.Uninitialized
|
||||||
joinAction.value = AsyncAction.Uninitialized
|
joinAction.value = AsyncAction.Uninitialized
|
||||||
|
cancelKnockAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
is JoinRoomEvents.UpdateKnockMessage -> {
|
||||||
|
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +150,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
joinAction = joinAction.value,
|
joinAction = joinAction.value,
|
||||||
knockAction = knockAction.value,
|
knockAction = knockAction.value,
|
||||||
|
cancelKnockAction = cancelKnockAction.value,
|
||||||
applicationName = buildMeta.applicationName,
|
applicationName = buildMeta.applicationName,
|
||||||
|
knockMessage = knockMessage,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -153,9 +167,19 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>) = launch {
|
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>, message: String) = launch {
|
||||||
knockAction.runUpdatingState {
|
knockAction.runUpdatingState {
|
||||||
knockRoom(roomId)
|
knockRoom(roomIdOrAlias, message, serverNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.cancelKnockRoom(requiresConfirmation: Boolean, cancelKnockAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
if (requiresConfirmation) {
|
||||||
|
cancelKnockAction.value = AsyncAction.ConfirmingNoParams
|
||||||
|
} else {
|
||||||
|
cancelKnockAction.runUpdatingState {
|
||||||
|
cancelKnockRoom(roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +230,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
name = name,
|
name = name,
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = activeMembersCount.toLong(),
|
numberOfMembers = activeMembersCount,
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
|
|
@ -214,6 +238,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||||
inviteSender = inviter?.toInviteSender()
|
inviteSender = inviter?.toInviteSender()
|
||||||
)
|
)
|
||||||
|
currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ data class JoinRoomState(
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val joinAction: AsyncAction<Unit>,
|
val joinAction: AsyncAction<Unit>,
|
||||||
val knockAction: AsyncAction<Unit>,
|
val knockAction: AsyncAction<Unit>,
|
||||||
|
val cancelKnockAction: AsyncAction<Unit>,
|
||||||
val applicationName: String,
|
val applicationName: String,
|
||||||
|
val knockMessage: String,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val joinAuthorisationStatus = when (contentState) {
|
val joinAuthorisationStatus = when (contentState) {
|
||||||
|
|
@ -68,6 +70,7 @@ sealed interface ContentState {
|
||||||
|
|
||||||
sealed interface JoinAuthorisationStatus {
|
sealed interface JoinAuthorisationStatus {
|
||||||
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||||
|
data object IsKnocked : JoinAuthorisationStatus
|
||||||
data object CanKnock : JoinAuthorisationStatus
|
data object CanKnock : JoinAuthorisationStatus
|
||||||
data object CanJoin : JoinAuthorisationStatus
|
data object CanJoin : JoinAuthorisationStatus
|
||||||
data object Unknown : JoinAuthorisationStatus
|
data object Unknown : JoinAuthorisationStatus
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
isDm = true,
|
isDm = true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
name = "A knocked Room",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,13 +130,17 @@ fun aJoinRoomState(
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
knockMessage: String = "",
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
joinAction = joinAction,
|
joinAction = joinAction,
|
||||||
knockAction = knockAction,
|
knockAction = knockAction,
|
||||||
|
cancelKnockAction = cancelKnockAction,
|
||||||
applicationName = "AppName",
|
applicationName = "AppName",
|
||||||
|
knockMessage = knockMessage,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,31 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
|
@ -32,20 +42,25 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescrip
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
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.organisms.RoomPreviewOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.button.SuperButton
|
import io.element.android.libraries.designsystem.components.button.SuperButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
|
@ -59,6 +74,7 @@ fun JoinRoomView(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onJoinSuccess: () -> Unit,
|
onJoinSuccess: () -> Unit,
|
||||||
onKnockSuccess: () -> Unit,
|
onKnockSuccess: () -> Unit,
|
||||||
|
onCancelKnockSuccess: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -69,12 +85,14 @@ fun JoinRoomView(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
paddingValues = PaddingValues(16.dp),
|
paddingValues = PaddingValues(16.dp),
|
||||||
topBar = {
|
topBar = {
|
||||||
JoinRoomTopBar(onBackClick = onBackClick)
|
JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
JoinRoomContent(
|
JoinRoomContent(
|
||||||
contentState = state.contentState,
|
contentState = state.contentState,
|
||||||
applicationName = state.applicationName,
|
applicationName = state.applicationName,
|
||||||
|
knockMessage = state.knockMessage,
|
||||||
|
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
|
|
@ -92,6 +110,9 @@ fun JoinRoomView(
|
||||||
onKnockRoom = {
|
onKnockRoom = {
|
||||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
},
|
},
|
||||||
|
onCancelKnock = {
|
||||||
|
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
|
||||||
|
},
|
||||||
onRetry = {
|
onRetry = {
|
||||||
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
},
|
},
|
||||||
|
|
@ -103,12 +124,30 @@ fun JoinRoomView(
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.joinAction,
|
async = state.joinAction,
|
||||||
onSuccess = { onJoinSuccess() },
|
onSuccess = { onJoinSuccess() },
|
||||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||||
)
|
)
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.knockAction,
|
async = state.knockAction,
|
||||||
onSuccess = { onKnockSuccess() },
|
onSuccess = { onKnockSuccess() },
|
||||||
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||||
|
)
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.cancelKnockAction,
|
||||||
|
onSuccess = { onCancelKnockSuccess() },
|
||||||
|
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||||
|
errorMessage = {
|
||||||
|
stringResource(CommonStrings.error_unknown)
|
||||||
|
},
|
||||||
|
confirmationDialog = {
|
||||||
|
ConfirmationDialog(
|
||||||
|
content = stringResource(R.string.screen_join_room_cancel_knock_alert_description),
|
||||||
|
title = stringResource(R.string.screen_join_room_cancel_knock_alert_title),
|
||||||
|
submitText = stringResource(R.string.screen_join_room_cancel_knock_alert_confirmation),
|
||||||
|
cancelText = stringResource(CommonStrings.action_no),
|
||||||
|
onSubmitClick = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = false)) },
|
||||||
|
onDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,63 +158,81 @@ private fun JoinRoomFooter(
|
||||||
onDeclineInvite: () -> Unit,
|
onDeclineInvite: () -> Unit,
|
||||||
onJoinRoom: () -> Unit,
|
onJoinRoom: () -> Unit,
|
||||||
onKnockRoom: () -> Unit,
|
onKnockRoom: () -> Unit,
|
||||||
|
onCancelKnock: () -> Unit,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onGoBack: () -> Unit,
|
onGoBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.contentState is ContentState.Failure) {
|
Box(
|
||||||
Button(
|
modifier = modifier
|
||||||
text = stringResource(CommonStrings.action_retry),
|
.fillMaxWidth()
|
||||||
onClick = onRetry,
|
.padding(top = 8.dp)
|
||||||
modifier = modifier.fillMaxWidth(),
|
) {
|
||||||
size = ButtonSize.Large,
|
if (state.contentState is ContentState.Failure) {
|
||||||
)
|
Button(
|
||||||
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
text = stringResource(CommonStrings.action_retry),
|
||||||
Button(
|
onClick = onRetry,
|
||||||
text = stringResource(CommonStrings.action_go_back),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = onGoBack,
|
size = ButtonSize.Large,
|
||||||
modifier = modifier.fillMaxWidth(),
|
)
|
||||||
size = ButtonSize.Large,
|
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||||
)
|
Button(
|
||||||
} else {
|
text = stringResource(CommonStrings.action_go_back),
|
||||||
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
onClick = onGoBack,
|
||||||
when (joinAuthorisationStatus) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
is JoinAuthorisationStatus.IsInvited -> {
|
size = ButtonSize.Large,
|
||||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
)
|
||||||
|
} 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 -> {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
text = stringResource(CommonStrings.action_decline),
|
text = stringResource(R.string.screen_join_room_cancel_knock_action),
|
||||||
onClick = onDeclineInvite,
|
onClick = onCancelKnock,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.LargeLowPadding,
|
size = ButtonSize.Large,
|
||||||
)
|
|
||||||
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 -> {
|
|
||||||
Button(
|
|
||||||
text = stringResource(R.string.screen_join_room_knock_action),
|
|
||||||
onClick = onKnockRoom,
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
size = ButtonSize.Large,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
JoinAuthorisationStatus.Unknown -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,132 +241,217 @@ private fun JoinRoomFooter(
|
||||||
private fun JoinRoomContent(
|
private fun JoinRoomContent(
|
||||||
contentState: ContentState,
|
contentState: ContentState,
|
||||||
applicationName: String,
|
applicationName: String,
|
||||||
|
knockMessage: String,
|
||||||
|
onKnockMessageUpdate: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (contentState) {
|
Box(modifier = modifier) {
|
||||||
is ContentState.Loaded -> {
|
when (contentState) {
|
||||||
RoomPreviewOrganism(
|
is ContentState.Loaded -> {
|
||||||
modifier = modifier,
|
when (contentState.joinAuthorisationStatus) {
|
||||||
avatar = {
|
is JoinAuthorisationStatus.IsKnocked -> {
|
||||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
IsKnockedLoadedContent()
|
||||||
},
|
}
|
||||||
title = {
|
else -> {
|
||||||
if (contentState.name != null) {
|
DefaultLoadedContent(
|
||||||
RoomPreviewTitleAtom(
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
title = contentState.name,
|
contentState = contentState,
|
||||||
|
applicationName = applicationName,
|
||||||
|
knockMessage = knockMessage,
|
||||||
|
onKnockMessageUpdate = onKnockMessageUpdate
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
RoomPreviewTitleAtom(
|
|
||||||
title = stringResource(id = CommonStrings.common_no_room_name),
|
|
||||||
fontStyle = FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subtitle = {
|
|
||||||
if (contentState.alias != null) {
|
|
||||||
RoomPreviewSubtitleAtom(contentState.alias.value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
description = {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
|
||||||
if (inviteSender != null) {
|
|
||||||
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 = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
memberCount = {
|
|
||||||
if (contentState.showMemberCount) {
|
|
||||||
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
is ContentState.UnknownRoom -> {
|
||||||
is ContentState.UnknownRoom -> {
|
RoomPreviewOrganism(
|
||||||
RoomPreviewOrganism(
|
avatar = {
|
||||||
modifier = modifier,
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
avatar = {
|
},
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
title = {
|
||||||
},
|
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||||
title = {
|
},
|
||||||
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
subtitle = {
|
||||||
},
|
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||||
subtitle = {
|
},
|
||||||
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
)
|
||||||
},
|
}
|
||||||
)
|
is ContentState.Loading -> {
|
||||||
}
|
RoomPreviewOrganism(
|
||||||
is ContentState.Loading -> {
|
avatar = {
|
||||||
RoomPreviewOrganism(
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
modifier = modifier,
|
},
|
||||||
avatar = {
|
title = {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||||
},
|
},
|
||||||
title = {
|
subtitle = {
|
||||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
PlaceholderAtom(width = 140.dp, height = 20.dp)
|
||||||
},
|
},
|
||||||
subtitle = {
|
)
|
||||||
PlaceholderAtom(width = 140.dp, height = 20.dp)
|
}
|
||||||
},
|
is ContentState.Failure -> {
|
||||||
)
|
RoomPreviewOrganism(
|
||||||
}
|
avatar = {
|
||||||
is ContentState.Failure -> {
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
RoomPreviewOrganism(
|
},
|
||||||
modifier = modifier,
|
title = {
|
||||||
avatar = {
|
when (contentState.roomIdOrAlias) {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
is RoomIdOrAlias.Alias -> {
|
||||||
},
|
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||||
title = {
|
}
|
||||||
when (contentState.roomIdOrAlias) {
|
is RoomIdOrAlias.Id -> {
|
||||||
is RoomIdOrAlias.Alias -> {
|
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||||
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,
|
||||||
subtitle = {
|
color = MaterialTheme.colorScheme.error,
|
||||||
Text(
|
)
|
||||||
text = stringResource(id = CommonStrings.error_unknown),
|
},
|
||||||
textAlign = TextAlign.Center,
|
)
|
||||||
color = MaterialTheme.colorScheme.error,
|
}
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = Modifier.sizeIn(minHeight = maxHeight * 0.7f),
|
||||||
|
iconStyle = BigIcon.Style.SuccessSolid,
|
||||||
|
title = stringResource(R.string.screen_join_room_knock_sent_title),
|
||||||
|
subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DefaultLoadedContent(
|
||||||
|
contentState: ContentState.Loaded,
|
||||||
|
applicationName: String,
|
||||||
|
knockMessage: String,
|
||||||
|
onKnockMessageUpdate: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
RoomPreviewOrganism(
|
||||||
|
modifier = modifier,
|
||||||
|
avatar = {
|
||||||
|
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
if (contentState.name != null) {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = contentState.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = stringResource(id = CommonStrings.common_no_room_name),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
if (contentState.alias != null) {
|
||||||
|
RoomPreviewSubtitleAtom(contentState.alias.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||||
|
if (inviteSender != null) {
|
||||||
|
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 = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = knockMessage,
|
||||||
|
onValueChange = onKnockMessageUpdate,
|
||||||
|
maxLines = 3,
|
||||||
|
minLines = 3,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_knock_message_description),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textPlaceholder,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memberCount = {
|
||||||
|
if (contentState.showMemberCount) {
|
||||||
|
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomTopBar(
|
private fun JoinRoomTopBar(
|
||||||
|
contentState: ContentState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(onClick = onBackClick)
|
BackButton(onClick = onBackClick)
|
||||||
},
|
},
|
||||||
title = {},
|
title = {
|
||||||
|
if (contentState is ContentState.Loaded && contentState.joinAuthorisationStatus is JoinAuthorisationStatus.IsKnocked) {
|
||||||
|
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||||
|
val titleModifier = Modifier
|
||||||
|
.clip(roundedCornerShape)
|
||||||
|
if (contentState.name != null) {
|
||||||
|
Row(
|
||||||
|
modifier = titleModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Avatar(avatarData = contentState.avatarData(AvatarSize.TimelineRoom))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
text = contentState.name,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconTitlePlaceholdersRowMolecule(
|
||||||
|
iconSize = AvatarSize.TimelineRoom.dp,
|
||||||
|
modifier = titleModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,5 +463,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
|
||||||
onBackClick = { },
|
onBackClick = { },
|
||||||
onJoinSuccess = { },
|
onJoinSuccess = { },
|
||||||
onKnockSuccess = { },
|
onKnockSuccess = { },
|
||||||
|
onCancelKnockSuccess = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE 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 CancelKnockRoom {
|
||||||
|
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId): Result<Unit> {
|
||||||
|
return client
|
||||||
|
.getPendingRoom(roomId)
|
||||||
|
?.leave()
|
||||||
|
?: Result.failure(IllegalStateException("No pending room found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ object JoinRoomModule {
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
joinRoom: JoinRoom,
|
joinRoom: JoinRoom,
|
||||||
knockRoom: KnockRoom,
|
knockRoom: KnockRoom,
|
||||||
|
cancelKnockRoom: CancelKnockRoom,
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
buildMeta: BuildMeta,
|
buildMeta: BuildMeta,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
|
|
@ -51,6 +52,7 @@ object JoinRoomModule {
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
joinRoom = joinRoom,
|
joinRoom = joinRoom,
|
||||||
knockRoom = knockRoom,
|
knockRoom = knockRoom,
|
||||||
|
cancelKnockRoom = cancelKnockRoom,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,26 @@ package io.element.android.features.joinroom.impl.di
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface KnockRoom {
|
interface KnockRoom {
|
||||||
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
suspend operator fun invoke(
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
message: String,
|
||||||
|
serverNames: List<String>,
|
||||||
|
): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
|
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
|
||||||
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId)
|
override suspend fun invoke(
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
message: String,
|
||||||
|
serverNames: List<String>
|
||||||
|
): Result<Unit> {
|
||||||
|
return client
|
||||||
|
.knockRoom(roomIdOrAlias, message, serverNames)
|
||||||
|
.map { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_cancel_knock_action">"Cancel request"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string>
|
||||||
<string name="screen_join_room_join_action">"Join room"</string>
|
<string name="screen_join_room_join_action">"Join room"</string>
|
||||||
<string name="screen_join_room_knock_action">"Send request to join"</string>
|
<string name="screen_join_room_knock_action">"Send request to join"</string>
|
||||||
|
<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_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_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_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>
|
<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 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeCancelKnockRoom(
|
||||||
|
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
|
) : CancelKnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||||
|
lambda(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeKnockRoom(
|
class FakeKnockRoom(
|
||||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
var lambda: (RoomIdOrAlias, String, List<String>) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }
|
||||||
) : KnockRoom {
|
) : KnockRoom {
|
||||||
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
override suspend fun invoke(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<Unit> = simulateLongTask {
|
||||||
lambda(roomId)
|
lambda(roomIdOrAlias, message, serverNames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
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.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
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.KnockRoom
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
@ -37,6 +38,7 @@ 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.test.room.join.FakeJoinRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -59,6 +61,8 @@ class JoinRoomPresenterTest {
|
||||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
|
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
assertThat(state.applicationName).isEqualTo("AppName")
|
assertThat(state.applicationName).isEqualTo("AppName")
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +218,7 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||||
state.eventSink(JoinRoomEvents.ClearError)
|
state.eventSink(JoinRoomEvents.ClearActionStates)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
@ -325,16 +329,20 @@ class JoinRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - emit knock room event`() = runTest {
|
fun `present - emit knock room event`() = runTest {
|
||||||
val knockRoomSuccess = lambdaRecorder { _: RoomId ->
|
val knockMessage = "Knock message"
|
||||||
|
val knockRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: String, _: List<String> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val knockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
val knockRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: String, _: List<String> ->
|
||||||
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
Result.failure<Unit>(RuntimeException("Failed to knock room $roomIdOrAlias"))
|
||||||
}
|
}
|
||||||
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
|
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
|
||||||
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
|
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
|
||||||
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
}
|
}
|
||||||
|
|
@ -353,8 +361,46 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
assert(knockRoomSuccess)
|
assert(knockRoomSuccess)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_ROOM_ID))
|
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
|
||||||
assert(knockRoomFailure)
|
assert(knockRoomFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - emit cancel knock room event`() = runTest {
|
||||||
|
val cancelKnockRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val cancelKnockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||||
|
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
||||||
|
}
|
||||||
|
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
|
||||||
|
val presenter = createJoinRoomPresenter(cancelKnockRoom = cancelKnockRoom)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(JoinRoomEvents.CancelKnock(true))
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
state.eventSink(JoinRoomEvents.CancelKnock(false))
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
cancelKnockRoom.lambda = cancelKnockRoomFailure
|
||||||
|
state.eventSink(JoinRoomEvents.CancelKnock(false))
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.cancelKnockAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(cancelKnockRoomFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
assert(cancelKnockRoomSuccess)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_ROOM_ID))
|
.with(value(A_ROOM_ID))
|
||||||
}
|
}
|
||||||
|
|
@ -474,6 +520,7 @@ class JoinRoomPresenterTest {
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
},
|
},
|
||||||
knockRoom: KnockRoom = FakeKnockRoom(),
|
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||||
|
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
||||||
): JoinRoomPresenter {
|
): JoinRoomPresenter {
|
||||||
|
|
@ -486,6 +533,7 @@ class JoinRoomPresenterTest {
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||||
knockRoom = knockRoom,
|
knockRoom = knockRoom,
|
||||||
|
cancelKnockRoom = cancelKnockRoom,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class JoinRoomViewTest {
|
||||||
rule.setJoinRoomView(
|
rule.setJoinRoomView(
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||||
|
knockMessage = "Knock knock",
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -79,7 +80,34 @@ class JoinRoomViewTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_ok)
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on cancel knock request emit the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_cancel_knock_action)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on closing Cancel Knock error emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
|
||||||
|
cancelKnockAction = AsyncAction.Failure(Exception("Error")),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -93,7 +121,7 @@ class JoinRoomViewTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_ok)
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -170,6 +198,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
|
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
|
|
@ -177,6 +206,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onJoinSuccess = onJoinSuccess,
|
onJoinSuccess = onJoinSuccess,
|
||||||
onKnockSuccess = onKnockSuccess,
|
onKnockSuccess = onKnockSuccess,
|
||||||
|
onCancelKnockSuccess = onCancelKnockSuccess
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
|
@ -38,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.roomlist.impl.R
|
||||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||||
|
|
@ -72,54 +74,86 @@ internal fun RoomSummaryRow(
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (room.displayType) {
|
Box(modifier = modifier) {
|
||||||
RoomSummaryDisplayType.PLACEHOLDER -> {
|
when (room.displayType) {
|
||||||
RoomSummaryPlaceholderRow(modifier = modifier)
|
RoomSummaryDisplayType.PLACEHOLDER -> {
|
||||||
}
|
RoomSummaryPlaceholderRow()
|
||||||
RoomSummaryDisplayType.INVITE -> {
|
}
|
||||||
RoomSummaryScaffoldRow(
|
RoomSummaryDisplayType.INVITE -> {
|
||||||
room = room,
|
RoomSummaryScaffoldRow(
|
||||||
onClick = onClick,
|
room = room,
|
||||||
onLongClick = {
|
onClick = onClick,
|
||||||
Timber.d("Long click on invite room")
|
onLongClick = {
|
||||||
},
|
Timber.d("Long click on invite room")
|
||||||
modifier = modifier
|
},
|
||||||
) {
|
) {
|
||||||
InviteNameAndIndicatorRow(name = room.name)
|
InviteNameAndIndicatorRow(name = room.name)
|
||||||
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
||||||
if (!room.isDm && room.inviteSender != null) {
|
if (!room.isDm && room.inviteSender != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
InviteSenderView(
|
InviteSenderView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
inviteSender = room.inviteSender,
|
inviteSender = room.inviteSender,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
InviteButtonsRow(
|
||||||
|
onAcceptClick = {
|
||||||
|
eventSink(RoomListEvents.AcceptInvite(room))
|
||||||
|
},
|
||||||
|
onDeclineClick = {
|
||||||
|
eventSink(RoomListEvents.DeclineInvite(room))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
InviteButtonsRow(
|
|
||||||
onAcceptClick = {
|
|
||||||
eventSink(RoomListEvents.AcceptInvite(room))
|
|
||||||
},
|
|
||||||
onDeclineClick = {
|
|
||||||
eventSink(RoomListEvents.DeclineInvite(room))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
RoomSummaryDisplayType.ROOM -> {
|
||||||
RoomSummaryDisplayType.ROOM -> {
|
RoomSummaryScaffoldRow(
|
||||||
RoomSummaryScaffoldRow(
|
room = room,
|
||||||
room = room,
|
onClick = onClick,
|
||||||
onClick = onClick,
|
onLongClick = {
|
||||||
onLongClick = {
|
eventSink(RoomListEvents.ShowContextMenu(room))
|
||||||
eventSink(RoomListEvents.ShowContextMenu(room))
|
},
|
||||||
},
|
) {
|
||||||
modifier = modifier
|
NameAndTimestampRow(
|
||||||
) {
|
name = room.name,
|
||||||
NameAndTimestampRow(
|
timestamp = room.timestamp,
|
||||||
name = room.name,
|
isHighlighted = room.isHighlighted
|
||||||
timestamp = room.timestamp,
|
)
|
||||||
isHighlighted = room.isHighlighted
|
LastMessageAndIndicatorRow(room = room)
|
||||||
)
|
}
|
||||||
LastMessageAndIndicatorRow(room = room)
|
}
|
||||||
|
RoomSummaryDisplayType.KNOCKED -> {
|
||||||
|
RoomSummaryScaffoldRow(
|
||||||
|
room = room,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
Timber.d("Long click on knocked room")
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
NameAndTimestampRow(
|
||||||
|
name = room.name,
|
||||||
|
timestamp = null,
|
||||||
|
isHighlighted = room.isHighlighted
|
||||||
|
)
|
||||||
|
if (room.canonicalAlias != null) {
|
||||||
|
Text(
|
||||||
|
text = room.canonicalAlias.value,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_join_room_knock_sent_title),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,16 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
inviteSender = roomInfo.inviter?.toInviteSender(),
|
inviteSender = roomInfo.inviter?.toInviteSender(),
|
||||||
isDm = roomInfo.isDm,
|
isDm = roomInfo.isDm,
|
||||||
canonicalAlias = roomInfo.canonicalAlias,
|
canonicalAlias = roomInfo.canonicalAlias,
|
||||||
displayType = if (roomInfo.currentUserMembership == CurrentUserMembership.INVITED) {
|
displayType = when (roomInfo.currentUserMembership) {
|
||||||
RoomSummaryDisplayType.INVITE
|
CurrentUserMembership.INVITED -> {
|
||||||
} else {
|
RoomSummaryDisplayType.INVITE
|
||||||
RoomSummaryDisplayType.ROOM
|
}
|
||||||
|
CurrentUserMembership.KNOCKED -> {
|
||||||
|
RoomSummaryDisplayType.KNOCKED
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
RoomSummaryDisplayType.ROOM
|
||||||
|
}
|
||||||
},
|
},
|
||||||
heroes = roomInfo.heroes.map { user ->
|
heroes = roomInfo.heroes.map { user ->
|
||||||
user.getAvatarData(size = AvatarSize.RoomListItem)
|
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||||
displayName = "Bob",
|
displayName = "Bob",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "A knocked room",
|
||||||
|
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||||
|
),
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "A knocked room with alias",
|
||||||
|
canonicalAlias = RoomAlias("#knockable:matrix.org"),
|
||||||
|
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
).flatten()
|
).flatten()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ package io.element.android.features.roomlist.impl.model
|
||||||
enum class RoomSummaryDisplayType {
|
enum class RoomSummaryDisplayType {
|
||||||
PLACEHOLDER,
|
PLACEHOLDER,
|
||||||
ROOM,
|
ROOM,
|
||||||
INVITE
|
INVITE,
|
||||||
|
KNOCKED,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
|
||||||
<string name="screen_invites_empty_list">"No Invites"</string>
|
<string name="screen_invites_empty_list">"No Invites"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||||
|
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
|
||||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||||
<string name="screen_migration_title">"Setting up your account."</string>
|
<string name="screen_migration_title">"Setting up your account."</string>
|
||||||
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
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.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
|
@ -52,7 +52,7 @@ interface MatrixClient : Closeable {
|
||||||
val sessionCoroutineScope: CoroutineScope
|
val sessionCoroutineScope: CoroutineScope
|
||||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||||
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
||||||
suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom?
|
suspend fun getPendingRoom(roomId: RoomId): PendingRoom?
|
||||||
suspend fun findDM(userId: UserId): RoomId?
|
suspend fun findDM(userId: UserId): RoomId?
|
||||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||||
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||||
|
|
@ -65,7 +65,7 @@ interface MatrixClient : Closeable {
|
||||||
suspend fun removeAvatar(): Result<Unit>
|
suspend fun removeAvatar(): Result<Unit>
|
||||||
suspend fun joinRoom(roomId: RoomId): Result<RoomSummary?>
|
suspend fun joinRoom(roomId: RoomId): Result<RoomSummary?>
|
||||||
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?>
|
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?>
|
||||||
suspend fun knockRoom(roomId: RoomId): Result<Unit>
|
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?>
|
||||||
fun syncService(): SyncService
|
fun syncService(): SyncService
|
||||||
fun sessionVerificationService(): SessionVerificationService
|
fun sessionVerificationService(): SessionVerificationService
|
||||||
fun pushersService(): PushersService
|
fun pushersService(): PushersService
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ 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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
/** A reference to a room the current user has been invited to, with the ability to decline the invite. */
|
/** A reference to a room the current user has knocked to or has been invited to, with the ability to leave the room. */
|
||||||
interface InvitedRoom : AutoCloseable {
|
interface PendingRoom : AutoCloseable {
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
val roomId: RoomId
|
val roomId: RoomId
|
||||||
|
|
||||||
/** Decline the invite to this room. */
|
/** Leave the room ie.decline invite or cancel knock. */
|
||||||
suspend fun declineInvite(): Result<Unit>
|
suspend fun leave(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
@ -30,8 +30,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.oidc.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
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.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
|
@ -251,24 +251,26 @@ class RustMatrixClient(
|
||||||
return roomFactory.create(roomId)
|
return roomFactory.create(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? {
|
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||||
return roomFactory.createInvitedRoom(roomId)
|
return roomFactory.createPendingRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the room to be available in the room list, with a membership for the current user of [CurrentUserMembership.JOINED].
|
* Wait for the room to be available in the room list with the correct membership for the current user.
|
||||||
* @param roomIdOrAlias the room id or alias to wait for
|
* @param roomIdOrAlias the room id or alias to wait for
|
||||||
* @param timeout the timeout to wait for the room to be available
|
* @param timeout the timeout to wait for the room to be available
|
||||||
|
* @param currentUserMembership the membership to wait for
|
||||||
* @throws TimeoutCancellationException if the room is not available after the timeout
|
* @throws TimeoutCancellationException if the room is not available after the timeout
|
||||||
*/
|
*/
|
||||||
private suspend fun awaitJoinedRoom(
|
private suspend fun awaitRoom(
|
||||||
roomIdOrAlias: RoomIdOrAlias,
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
timeout: Duration
|
timeout: Duration,
|
||||||
|
currentUserMembership: CurrentUserMembership,
|
||||||
): RoomSummary {
|
): RoomSummary {
|
||||||
return withTimeout(timeout) {
|
return withTimeout(timeout) {
|
||||||
getRoomSummaryFlow(roomIdOrAlias)
|
getRoomSummaryFlow(roomIdOrAlias)
|
||||||
.mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
|
.mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
|
||||||
.filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED }
|
.filter { roomSummary -> roomSummary.info.currentUserMembership == currentUserMembership }
|
||||||
.first()
|
.first()
|
||||||
// Ensure that the room is ready
|
// Ensure that the room is ready
|
||||||
.also { client.awaitRoomRemoteEcho(it.roomId.value) }
|
.also { client.awaitRoomRemoteEcho(it.roomId.value) }
|
||||||
|
|
@ -314,7 +316,7 @@ class RustMatrixClient(
|
||||||
val roomId = RoomId(client.createRoom(rustParams))
|
val roomId = RoomId(client.createRoom(rustParams))
|
||||||
// Wait to receive the room back from the sync but do not returns failure if it fails.
|
// Wait to receive the room back from the sync but do not returns failure if it fails.
|
||||||
try {
|
try {
|
||||||
awaitJoinedRoom(roomId.toRoomIdOrAlias(), 30.seconds)
|
awaitRoom(roomId.toRoomIdOrAlias(), 30.seconds, CurrentUserMembership.JOINED)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +371,7 @@ class RustMatrixClient(
|
||||||
runCatching {
|
runCatching {
|
||||||
client.joinRoomById(roomId.value).destroy()
|
client.joinRoomById(roomId.value).destroy()
|
||||||
try {
|
try {
|
||||||
awaitJoinedRoom(roomId.toRoomIdOrAlias(), 10.seconds)
|
awaitRoom(roomId.toRoomIdOrAlias(), 10.seconds, CurrentUserMembership.JOINED)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||||
null
|
null
|
||||||
|
|
@ -384,7 +386,7 @@ class RustMatrixClient(
|
||||||
serverNames = serverNames,
|
serverNames = serverNames,
|
||||||
).destroy()
|
).destroy()
|
||||||
try {
|
try {
|
||||||
awaitJoinedRoom(roomIdOrAlias, 10.seconds)
|
awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.JOINED)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||||
null
|
null
|
||||||
|
|
@ -392,8 +394,18 @@ class RustMatrixClient(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
|
override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> = withContext(
|
||||||
return Result.failure(NotImplementedError("Not yet implemented"))
|
sessionDispatcher
|
||||||
|
) {
|
||||||
|
runCatching {
|
||||||
|
client.knock(roomIdOrAlias.identifier).destroy()
|
||||||
|
try {
|
||||||
|
awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.KNOCKED)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,20 @@ 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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
|
|
||||||
class RustInvitedRoom(
|
class RustPendingRoom(
|
||||||
override val sessionId: SessionId,
|
override val sessionId: SessionId,
|
||||||
private val invitedRoom: Room,
|
private val inner: Room,
|
||||||
) : InvitedRoom {
|
) : PendingRoom {
|
||||||
override val roomId = RoomId(invitedRoom.id())
|
override val roomId = RoomId(inner.id())
|
||||||
|
|
||||||
override suspend fun declineInvite(): Result<Unit> = runCatching {
|
override suspend fun leave(): Result<Unit> = runCatching {
|
||||||
invitedRoom.leave()
|
inner.leave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
invitedRoom.destroy()
|
inner.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,8 +14,8 @@ import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||||
|
|
@ -35,6 +35,7 @@ import timber.log.Timber
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||||
|
|
||||||
private const val CACHE_SIZE = 16
|
private const val CACHE_SIZE = 16
|
||||||
|
private val PENDING_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED)
|
||||||
|
|
||||||
class RustRoomFactory(
|
class RustRoomFactory(
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
|
|
@ -120,7 +121,7 @@ class RustRoomFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createInvitedRoom(roomId: RoomId): InvitedRoom? = withContext(dispatcher) {
|
suspend fun createPendingRoom(roomId: RoomId): PendingRoom? = withContext(dispatcher) {
|
||||||
if (isDestroyed) {
|
if (isDestroyed) {
|
||||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
|
|
@ -130,20 +131,20 @@ class RustRoomFactory(
|
||||||
Timber.d("Room not found for $roomId")
|
Timber.d("Room not found for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
if (roomListItem.membership() != Membership.INVITED) {
|
if (roomListItem.membership() !in PENDING_MEMBERSHIPS) {
|
||||||
Timber.d("Room $roomId is not in invited state")
|
Timber.d("Room $roomId is not in pending state")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val invitedRoom = try {
|
val innerRoom = try {
|
||||||
|
// TODO use new method when available, for now it'll fail for knocked rooms
|
||||||
roomListItem.invitedRoom()
|
roomListItem.invitedRoom()
|
||||||
} catch (e: RoomListException) {
|
} catch (e: RoomListException) {
|
||||||
Timber.e(e, "Failed to get invited room for $roomId")
|
Timber.e(e, "Failed to get pending room for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
RustPendingRoom(
|
||||||
RustInvitedRoom(
|
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
invitedRoom = invitedRoom,
|
inner = innerRoom,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
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.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
|
@ -101,7 +101,7 @@ class FakeMatrixClient(
|
||||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||||
private var findDmResult: RoomId? = A_ROOM_ID
|
private var findDmResult: RoomId? = A_ROOM_ID
|
||||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||||
val getInvitedRoomResults = mutableMapOf<RoomId, InvitedRoom>()
|
val getPendingRoomResults = mutableMapOf<RoomId, PendingRoom>()
|
||||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||||
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
||||||
|
|
@ -114,8 +114,8 @@ class FakeMatrixClient(
|
||||||
var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List<String>) -> Result<RoomSummary?> = { _, _ ->
|
var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List<String>) -> Result<RoomSummary?> = { _, _ ->
|
||||||
Result.success(null)
|
Result.success(null)
|
||||||
}
|
}
|
||||||
var knockRoomLambda: (RoomId) -> Result<Unit> = {
|
var knockRoomLambda: (RoomIdOrAlias, String, List<String>) -> Result<RoomSummary?> = { _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(null)
|
||||||
}
|
}
|
||||||
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
|
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
|
||||||
flowOf<Optional<RoomSummary>>(Optional.empty())
|
flowOf<Optional<RoomSummary>>(Optional.empty())
|
||||||
|
|
@ -128,8 +128,8 @@ class FakeMatrixClient(
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? {
|
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||||
return getInvitedRoomResults[roomId]
|
return getPendingRoomResults[roomId]
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findDM(userId: UserId): RoomId? {
|
override suspend fun findDM(userId: UserId): RoomId? {
|
||||||
|
|
@ -223,7 +223,9 @@ class FakeMatrixClient(
|
||||||
return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames)
|
return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> = knockRoomLambda(roomId)
|
override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> {
|
||||||
|
return knockRoomLambda(roomIdOrAlias, message, serverNames)
|
||||||
|
}
|
||||||
|
|
||||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@ 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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_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.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeInvitedRoom(
|
class FakePendingRoom(
|
||||||
override val sessionId: SessionId = A_SESSION_ID,
|
override val sessionId: SessionId = A_SESSION_ID,
|
||||||
override val roomId: RoomId = A_ROOM_ID,
|
override val roomId: RoomId = A_ROOM_ID,
|
||||||
private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
|
private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
|
||||||
) : InvitedRoom {
|
) : PendingRoom {
|
||||||
override suspend fun declineInvite(): Result<Unit> = simulateLongTask {
|
override suspend fun leave(): Result<Unit> = simulateLongTask {
|
||||||
declineInviteResult()
|
declineInviteResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8,10 +8,11 @@
|
||||||
package io.element.android.libraries.matrix.ui.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
|
@ -30,11 +31,12 @@ fun InviteSenderView(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.padding(vertical = 2.dp)) {
|
||||||
Avatar(avatarData = inviteSender.avatarData)
|
Avatar(avatarData = inviteSender.avatarData)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = inviteSender.annotatedString(),
|
text = inviteSender.annotatedString(),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a6eee1185065c82f90cf53d7b9fd62e6de72d907606099b0536b6305ea9537f2
|
||||||
|
size 117250
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:68aa9bd1630bfe084c3cb54449946c95fdfc4fec4190453a648ffbb738c50c02
|
oid sha256:74f12ea2c5114363b809fcf4d897487cb87ecfab361952471c05d22898d0048f
|
||||||
size 118338
|
size 130010
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a89fd43e3e373474df355a970ea2dd4d525f554f509c4d34b098151fab9ea6a0
|
oid sha256:70c33e148a040ec24287f9ca48353a76e8167015f24d8249bff405e9cc9f16ff
|
||||||
size 118678
|
size 118640
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:07458d183bae7ecfb7ee61bb6f2e0abb9a4399dd373068002fec3722f575e754
|
||||||
|
size 102438
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:159cdb25a248a86f348ec72a1af680a14d6fdf16d7d268649ef324f8296b0356
|
oid sha256:6950f5e0824c964936077ecda4ff7edb8c8c6796b5a4ae304e6027e57a83cb55
|
||||||
size 104708
|
size 116382
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:69bef3b0f9f8cdb315e4bc1a18b82d12c8927fcb389ebf5975db8c254a040a9b
|
oid sha256:c3d75eca5904d605b91becebca60199f7e60e6f2bec6b9a945ce8d130cfd7e47
|
||||||
size 104737
|
size 104802
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:77536b81116242dbedd516972cb4945711dee01832b2e9133051d33ede8a8e1a
|
oid sha256:1da21d7c1ca79691ccb4e0cb7a2e076874dd3894102b7764f874d42a1be83fcf
|
||||||
size 40882
|
size 40960
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8a4e60263d9fb57f115abb852eb5a080bfcdaa5cc1c786b2b0716c8313a94955
|
oid sha256:a78abedec8a3aad14bf6368bf73d46621feaf8e6fd6e019d381077ef05856259
|
||||||
size 72153
|
size 72236
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:83ce6ad6b6d5940d95a817b9f4da70d094407f1571ed541c84457cbf59281329
|
oid sha256:fbdced976e93e3059dc1b12c4c6ea18410748b9ba20c07f545ec81d7d1dc8451
|
||||||
size 40819
|
size 40791
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:df340436aa5aab1438bd27a917273d7aa48096efbb373b28308e7a89817241ea
|
oid sha256:f60d07c4be6d75142e753dec5071f7a6d5dd15519d87a2eb491b708fa7aeea4b
|
||||||
size 70845
|
size 70917
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c530aabdc8bd8e85a60c7daf7b88c56736bc2c9e10b251e5d1312da66c79586d
|
oid sha256:7cb57e2567310265a2866565a0f1f9a70ac656d6d93b6df076ba875add699c2b
|
||||||
size 22812
|
size 22782
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e6afb5e2e6fb3766ed18412f624a98c436c45de6956b996e7d8c947f0523e5e6
|
oid sha256:89e6c33ba736594d5a69a9cade77eed957f6c59a23fbd7ce7a8af4962ef7fe66
|
||||||
size 21107
|
size 21162
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1943e0cca177b76535bcfff5572e519e70eb45d9a93b48369f174b66813b2b8c
|
||||||
|
size 11545
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4b5435505e95bb4ea71dbc96faccc4c3e52c35eac6d15ca8f05011a65deb2361
|
||||||
|
size 16913
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:427773f5702b8830f4aadf70bcd19ebcf253085de027200afd21fd33ff8a8a3f
|
oid sha256:af6a921903f5b827650ff59b7a07a53a0ee6fff8bf05b9c9caa7e40b2ff65bcc
|
||||||
size 22646
|
size 22726
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:35c0c73bc2a40f787f97d8b1d94442d379e7dc2cff708bd16d259d1d9594271d
|
oid sha256:d938c23b881918f2fc73cf52f85fb103fede4aecbe83cc5725e50ca9f271f711
|
||||||
size 20911
|
size 20925
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c92ce38a3829ce48152f956999593b90955830371aa3230ea598f9ba80560163
|
||||||
|
size 11903
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9a4e420c18dc7fec6afecee9a02c23d2ca031f57e51a7937a1217f7663b3a225
|
||||||
|
size 17196
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7fb36e51fe4e047d3b7299622ed41acf3fee6b717589972449af57b2163a25a3
|
oid sha256:57dc005cc7cceaa1f387e3a80c7676313a616c23ca4af06a5867fd438d8ed2a2
|
||||||
size 43528
|
size 43596
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:736fd7a2ad72251ce6f9cea37ff907639b5234a66f9de542041df85917b8cdb1
|
oid sha256:b46afbcc4a208a4ceeb15932569fe0569898d394452e6af6ab41d15fc2c372db
|
||||||
size 43253
|
size 43226
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c5897f6945dbf3abe154f26388987c8223f65748d90fa6b049d306c64341ea0
|
oid sha256:f12d9f5c181579e054fde00a32ce72cf032326b3c63f2424e5df696db0138368
|
||||||
size 78588
|
size 78667
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:61d92b59ab35acebcbecab11064734141bf543bd4c3fc5cddda7f7fdf4631ae5
|
oid sha256:3081dd73e3a33e786266b49de1267c401e55ac3c5b37d1b9c61749b0f7d55c29
|
||||||
size 99173
|
size 99240
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c5897f6945dbf3abe154f26388987c8223f65748d90fa6b049d306c64341ea0
|
oid sha256:f12d9f5c181579e054fde00a32ce72cf032326b3c63f2424e5df696db0138368
|
||||||
size 78588
|
size 78667
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5709694ba5ba479bb1652cea0218db591ba9aac83213b8a05d155cf73707a913
|
oid sha256:4f2e8f7d0a8d384f7958165b113f67f637efd7ab4d4e2f3db321edfdf83e8e82
|
||||||
size 79045
|
size 79079
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4bb464339f602dbf8f0a3046ebaaefbe64808351289ba70450eef0565b23ee40
|
oid sha256:3c27b870ca1639a58efcd42dfc31bbb12a935e685ca1b87e891ec88abaf8033b
|
||||||
size 98084
|
size 98148
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7fe52dfaeac33dc77da9e741102a6248a8b142fb4706bea34b4e1a9c9f0062c7
|
oid sha256:eaf6ef4d088e706e5e37a33ae7897af0ffbfda7afac486f9de2cabd7e24ac0be
|
||||||
size 86083
|
size 86150
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dfeb3170f15fc62836bed2961fb190af7ee1fea59c9f7ee1c393c7398c4dabaf
|
oid sha256:7eb4d87dd2844f81bf745f53cbe83253fe8b48471692a00dbe0f385de75c8c3a
|
||||||
size 106113
|
size 106079
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7fe52dfaeac33dc77da9e741102a6248a8b142fb4706bea34b4e1a9c9f0062c7
|
oid sha256:eaf6ef4d088e706e5e37a33ae7897af0ffbfda7afac486f9de2cabd7e24ac0be
|
||||||
size 86083
|
size 86150
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1a4e557d8e3683fb3b30b7d3264a6953f713684edd2c07493d9cb3a04da4ef45
|
oid sha256:c1014ee5cdde8ea23b6655ee805761e824bcae40d353d4208af41c8b7801998a
|
||||||
size 87017
|
size 86938
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:00492d7154e1ea7468b544a2fcb29fbc23c8b54260420451fb7f912b5226eb69
|
oid sha256:0550ab262835b885364b429000e0a887a6bfe594ba031fdbbb04fb4ec63ea475
|
||||||
size 104915
|
size 104884
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:70b2f162bfc0c397eb9701922ade58665558ea0acff25784dbef3cdd5e840d83
|
oid sha256:8257b4c0465151c099b82747c54b6d42881c4315114fc00840890fc3b6796d1b
|
||||||
size 10561
|
size 10522
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20fea6755d84bd8668ba045929fb93248c8039711fcf6bec3897c93b89542e72
|
oid sha256:4716815888abcb74ecc017722b5e4a7955ad94f90d9a4fcfe2847c21a7cd5777
|
||||||
size 10423
|
size 10373
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,8 @@
|
||||||
"banner\\.migrate_to_native_sliding_sync\\..*",
|
"banner\\.migrate_to_native_sliding_sync\\..*",
|
||||||
"full_screen_intent_banner_.*",
|
"full_screen_intent_banner_.*",
|
||||||
"screen_migration_.*",
|
"screen_migration_.*",
|
||||||
"screen_invites_.*"
|
"screen_invites_.*",
|
||||||
|
"screen\\.join_room\\.knock_sent_title"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -281,7 +282,8 @@
|
||||||
{
|
{
|
||||||
"name" : ":features:joinroom:impl",
|
"name" : ":features:joinroom:impl",
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
"screen_join_room_.*"
|
"screen_join_room_.*",
|
||||||
|
"screen\\.join_room\\..*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue