Merge pull request #2733 from element-hq/feature/bma/disableKnock

Disable knock
This commit is contained in:
Benoit Marty 2024-04-22 16:22:48 +02:00 committed by GitHub
commit d7f0d23634
27 changed files with 279 additions and 101 deletions

View file

@ -43,8 +43,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
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.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.OnboardingBackground
import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.components.PageTitle
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

View file

@ -41,8 +41,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R import io.element.android.features.ftue.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
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.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.OnboardingBackground
import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.components.PageTitle
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.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData

View file

@ -102,12 +102,14 @@ class AcceptDeclineInvitePresenter @Inject constructor(
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch { private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
acceptedAction.runUpdatingState { acceptedAction.runUpdatingState {
client.joinRoom(roomId).onSuccess { client.joinRoom(roomId)
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) .onSuccess {
client.getRoom(roomId)?.use { room -> notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) client.getRoom(roomId)?.use { room ->
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
}
} }
} .map { roomId }
} }
} }

View file

@ -164,7 +164,7 @@ class AcceptDeclineInvitePresenterTest {
@Test @Test
fun `present - accepting invite error flow`() = runTest { fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomId: RoomId -> val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId")) Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomFailure joinRoomLambda = joinRoomFailure
@ -197,8 +197,8 @@ class AcceptDeclineInvitePresenterTest {
@Test @Test
fun `present - accepting invite success flow`() = runTest { fun `present - accepting invite success flow`() = runTest {
val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> val joinRoomSuccess = lambdaRecorder { _: RoomId ->
Result.success(roomId) Result.success(Unit)
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomSuccess joinRoomLambda = joinRoomSuccess

View file

@ -19,6 +19,8 @@ package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents { 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 ClearError : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents data object DeclineInvite : JoinRoomEvents
} }

View file

@ -49,6 +49,7 @@ class JoinRoomNode @AssistedInject constructor(
JoinRoomView( JoinRoomView(
state = state, state = state,
onBackPressed = ::navigateUp, onBackPressed = ::navigateUp,
onKnockSuccess = ::navigateUp,
modifier = modifier modifier = modifier
) )
acceptDeclineInviteView.Render( acceptDeclineInviteView.Render(

View file

@ -18,19 +18,25 @@ package io.element.android.features.joinroom.impl
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
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.setValue import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
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.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.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@ -39,6 +45,8 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor( class JoinRoomPresenter @AssistedInject constructor(
@ -46,6 +54,7 @@ class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>, @Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val knockRoom: KnockRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
interface Factory { interface Factory {
@ -58,8 +67,10 @@ class JoinRoomPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) } var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
initialValue = ContentState.Loading(roomIdOrAlias), initialValue = ContentState.Loading(roomIdOrAlias),
key1 = roomInfo, key1 = roomInfo,
@ -101,6 +112,9 @@ class JoinRoomPresenter @AssistedInject constructor(
AcceptDeclineInviteEvents.AcceptInvite(inviteData) AcceptDeclineInviteEvents.AcceptInvite(inviteData)
) )
} }
JoinRoomEvents.KnockRoom -> {
coroutineScope.knockRoom(roomId, knockAction)
}
JoinRoomEvents.DeclineInvite -> { JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
@ -110,15 +124,25 @@ class JoinRoomPresenter @AssistedInject constructor(
JoinRoomEvents.RetryFetchingContent -> { JoinRoomEvents.RetryFetchingContent -> {
retryCount++ retryCount++
} }
JoinRoomEvents.ClearError -> {
knockAction.value = AsyncAction.Uninitialized
}
} }
} }
return JoinRoomState( return JoinRoomState(
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
knockAction = knockAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.knockRoom(roomId: RoomId, knockAction: MutableState<AsyncAction<Unit>>) = launch {
knockAction.runUpdatingState {
knockRoom(roomId)
}
}
} }
private fun RoomPreview.toContentState(): ContentState { private fun RoomPreview.toContentState(): ContentState {

View file

@ -18,6 +18,7 @@ package io.element.android.features.joinroom.impl
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.ui.model.InviteSender
data class JoinRoomState( data class JoinRoomState(
val contentState: ContentState, val contentState: ContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val knockAction: AsyncAction<Unit>,
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
val joinAuthorisationStatus = when (contentState) { val joinAuthorisationStatus = when (contentState) {

View file

@ -19,6 +19,7 @@ package io.element.android.features.joinroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@ -105,10 +106,12 @@ fun aLoadedContentState(
fun aJoinRoomState( fun aJoinRoomState(
contentState: ContentState = aLoadedContentState(), contentState: ContentState = aLoadedContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (JoinRoomEvents) -> Unit = {} eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState( ) = JoinRoomState(
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
knockAction = knockAction,
eventSink = eventSink eventSink = eventSink
) )

View file

@ -16,15 +16,15 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMem
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.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
@ -60,36 +61,49 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun JoinRoomView( fun JoinRoomView(
state: JoinRoomState, state: JoinRoomState,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onKnockSuccess: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val gradientBackground = remember { LightGradientBackground() } Box(
HeaderFooterPage( modifier = modifier.fillMaxSize(),
modifier = modifier.background(gradientBackground), ) {
containerColor = Color.Transparent, LightGradientBackground()
paddingValues = PaddingValues(16.dp), HeaderFooterPage(
topBar = { containerColor = Color.Transparent,
JoinRoomTopBar(onBackClicked = onBackPressed) paddingValues = PaddingValues(16.dp),
}, topBar = {
content = { JoinRoomTopBar(onBackClicked = onBackPressed)
JoinRoomContent(contentState = state.contentState) },
}, content = {
footer = { JoinRoomContent(contentState = state.contentState)
JoinRoomFooter( },
state = state, footer = {
onAcceptInvite = { JoinRoomFooter(
state.eventSink(JoinRoomEvents.AcceptInvite) state = state,
}, onAcceptInvite = {
onDeclineInvite = { state.eventSink(JoinRoomEvents.AcceptInvite)
state.eventSink(JoinRoomEvents.DeclineInvite) },
}, onDeclineInvite = {
onJoinRoom = { state.eventSink(JoinRoomEvents.DeclineInvite)
state.eventSink(JoinRoomEvents.JoinRoom) },
}, onJoinRoom = {
onRetry = { state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvents.RetryFetchingContent) },
} onKnockRoom = {
) state.eventSink(JoinRoomEvents.KnockRoom)
} },
onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
)
}
)
}
AsyncActionView(
async = state.knockAction,
onSuccess = { onKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
) )
} }
@ -99,6 +113,7 @@ private fun JoinRoomFooter(
onAcceptInvite: () -> Unit, onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit, onDeclineInvite: () -> Unit,
onJoinRoom: () -> Unit, onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit,
onRetry: () -> Unit, onRetry: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -142,7 +157,7 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus.CanKnock -> { JoinAuthorisationStatus.CanKnock -> {
Button( Button(
text = stringResource(R.string.screen_join_room_knock_action), text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom, onClick = onKnockRoom,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Large, size = ButtonSize.Large,
) )
@ -263,6 +278,7 @@ private fun JoinRoomTopBar(
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
JoinRoomView( JoinRoomView(
state = state, state = state,
onBackPressed = { } onBackPressed = { },
onKnockSuccess = { },
) )
} }

View file

@ -35,6 +35,7 @@ object JoinRoomModule {
@Provides @Provides
fun providesJoinRoomPresenterFactory( fun providesJoinRoomPresenterFactory(
client: MatrixClient, client: MatrixClient,
knockRoom: KnockRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
): JoinRoomPresenter.Factory { ): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory {
@ -48,6 +49,7 @@ object JoinRoomModule {
roomIdOrAlias = roomIdOrAlias, roomIdOrAlias = roomIdOrAlias,
roomDescription = roomDescription, roomDescription = roomDescription,
matrixClient = client, matrixClient = client,
knockRoom = knockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
) )
} }

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.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 KnockRoom {
suspend operator fun invoke(roomId: RoomId): Result<Unit>
}
@ContributesBinding(SessionScope::class)
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId)
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.joinroom.impl
import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.libraries.matrix.api.core.RoomId
class FakeKnockRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : KnockRoom {
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
}

View file

@ -20,7 +20,9 @@ import com.google.common.truth.Truth.assertThat
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.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.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@ -269,6 +271,38 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - emit knock room event`() = runTest {
val knockRoomSuccess = lambdaRecorder { _: RoomId ->
Result.success(Unit)
}
val knockRoomFailure = lambdaRecorder { roomId: RoomId ->
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
}
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.KnockRoom)
}
awaitItem().also { state ->
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
fakeKnockRoom.lambda = knockRoomFailure
state.eventSink(JoinRoomEvents.KnockRoom)
}
awaitItem().also { state ->
assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java)
}
}
assert(knockRoomSuccess)
.isCalledOnce()
.with(value(A_ROOM_ID))
assert(knockRoomFailure)
.isCalledOnce()
.with(value(A_ROOM_ID))
}
@Test @Test
fun `present - when room is not known RoomPreview is loaded`() = runTest { fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
@ -377,6 +411,7 @@ class JoinRoomPresenterTest {
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
roomDescription: Optional<RoomDescription> = Optional.empty(), roomDescription: Optional<RoomDescription> = Optional.empty(),
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
knockRoom: KnockRoom = FakeKnockRoom(),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() } acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
): JoinRoomPresenter { ): JoinRoomPresenter {
return JoinRoomPresenter( return JoinRoomPresenter(
@ -384,6 +419,7 @@ class JoinRoomPresenterTest {
roomIdOrAlias = roomId.toRoomIdOrAlias(), roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription, roomDescription = roomDescription,
matrixClient = matrixClient, matrixClient = matrixClient,
knockRoom = knockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
) )
} }

View file

@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@ -72,11 +73,25 @@ class JoinRoomViewTest {
), ),
) )
rule.clickOn(R.string.screen_join_room_knock_action) rule.clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom) eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
} }
@Test @Test
fun `clicking on Accept invitationon IsInvited room emits the expected Event`() { fun `clicking on closing Knock error emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
knockAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
}
@Test
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( rule.setJoinRoomView(
aJoinRoomState( aJoinRoomState(
@ -118,11 +133,13 @@ class JoinRoomViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
state: JoinRoomState, state: JoinRoomState,
onBackPressed: () -> Unit = EnsureNeverCalled(), onBackPressed: () -> Unit = EnsureNeverCalled(),
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
JoinRoomView( JoinRoomView(
state = state, state = state,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
onKnockSuccess = onKnockSuccess,
) )
} }
} }

View file

@ -16,17 +16,17 @@
package io.element.android.features.roomaliasresolver.impl package io.element.android.features.roomaliasresolver.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -65,23 +65,26 @@ fun RoomAliasResolverView(
latestOnAliasResolved(state.resolveState.data) latestOnAliasResolved(state.resolveState.data)
} }
} }
val gradientBackground = remember { LightGradientBackground() } Box(
HeaderFooterPage( modifier = modifier.fillMaxSize(),
modifier = modifier.background(gradientBackground), ) {
containerColor = Color.Transparent, LightGradientBackground()
paddingValues = PaddingValues(16.dp), HeaderFooterPage(
topBar = { containerColor = Color.Transparent,
RoomAliasResolverTopBar(onBackClicked = onBackPressed) paddingValues = PaddingValues(16.dp),
}, topBar = {
content = { RoomAliasResolverTopBar(onBackClicked = onBackPressed)
RoomAliasResolverContent(state = state) },
}, content = {
footer = { RoomAliasResolverContent(state = state)
RoomAliasResolverFooter( },
state = state, footer = {
) RoomAliasResolverFooter(
} state = state,
) )
}
)
}
} }
@Composable @Composable

View file

@ -108,6 +108,7 @@ class RoomDirectoryPresenter @Inject constructor(
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch { private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
state.runUpdatingState { state.runUpdatingState {
joinRoom(roomId) joinRoom(roomId)
.map { roomId }
} }
} }

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject import javax.inject.Inject
interface JoinRoom { interface JoinRoom {
suspend operator fun invoke(roomId: RoomId): Result<RoomId> suspend operator fun invoke(roomId: RoomId): Result<Unit>
} }
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)

View file

@ -20,7 +20,7 @@ import io.element.android.features.roomdirectory.impl.root.di.JoinRoom
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
class FakeJoinRoom( class FakeJoinRoom(
var lambda: (RoomId) -> Result<RoomId> = { Result.success(it) } var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : JoinRoom { ) : JoinRoom {
override suspend fun invoke(roomId: RoomId) = lambda(roomId) override suspend fun invoke(roomId: RoomId) = lambda(roomId)
} }

View file

@ -138,11 +138,11 @@ import org.junit.Test
@Test @Test
fun `present - emit join room event`() = runTest { fun `present - emit join room event`() = runTest {
val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> val joinRoomSuccess = lambdaRecorder { _: RoomId ->
Result.success(roomId) Result.success(Unit)
} }
val joinRoomFailure = lambdaRecorder { roomId: RoomId -> val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId")) Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
} }
val fakeJoinRoom = FakeJoinRoom(joinRoomSuccess) val fakeJoinRoom = FakeJoinRoom(joinRoomSuccess)
val presenter = createRoomDirectoryPresenter(joinRoom = fakeJoinRoom) val presenter = createRoomDirectoryPresenter(joinRoom = fakeJoinRoom)
@ -171,7 +171,7 @@ import org.junit.Test
roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(
createRoomDirectoryListFactory = { FakeRoomDirectoryList() } createRoomDirectoryListFactory = { FakeRoomDirectoryList() }
), ),
joinRoom: JoinRoom = FakeJoinRoom { Result.success(it) }, joinRoom: JoinRoom = FakeJoinRoom { Result.success(Unit) },
): RoomDirectoryPresenter { ): RoomDirectoryPresenter {
return RoomDirectoryPresenter( return RoomDirectoryPresenter(
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),

View file

@ -16,46 +16,48 @@
package io.element.android.libraries.designsystem.background package io.element.android.libraries.designsystem.background
import androidx.compose.foundation.background import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RadialGradientShader import androidx.compose.ui.graphics.RadialGradientShader
import androidx.compose.ui.graphics.Shader
import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.ShaderBrush
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
class LightGradientBackground( /**
private val firstColor: Color = Color(0x1E0DBD8B), * Light gradient background for Join room screens.
private val secondColor: Color = Color(0x001273EB), */
private val ratio: Float = 642 / 775f, @Composable
) : ShaderBrush() { fun LightGradientBackground(
override fun createShader(size: Size): Shader { modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.background,
firstColor: Color = Color(0x1E0DBD8B),
secondColor: Color = Color(0x001273EB),
ratio: Float = 642 / 775f,
) {
Canvas(
modifier = modifier.fillMaxSize()
) {
val biggerDimension = size.width * 1.98f val biggerDimension = size.width * 1.98f
return RadialGradientShader( val gradientShaderBrush = ShaderBrush(
colors = listOf(firstColor, secondColor), RadialGradientShader(
center = size.center.copy(x = size.width * ratio, y = size.height * ratio), colors = listOf(firstColor, secondColor),
radius = biggerDimension / 2f, center = size.center.copy(x = size.width * ratio, y = size.height * ratio),
colorStops = listOf(0f, 0.95f) radius = biggerDimension / 2f,
colorStops = listOf(0f, 0.95f)
)
) )
drawRect(backgroundColor, size = size)
drawRect(brush = gradientShaderBrush, size = size)
} }
} }
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun LightGradientBackgroundPreview() = ElementPreview { internal fun LightGradientBackgroundPreview() = ElementPreview {
val gradientBackground = remember { LightGradientBackground()
LightGradientBackground()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBackground)
)
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.designsystem.components package io.element.android.libraries.designsystem.background
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.ShaderBrush
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.libraries.designsystem.components.drawWithLayer
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

View file

@ -65,7 +65,8 @@ interface MatrixClient : Closeable {
suspend fun setDisplayName(displayName: String): Result<Unit> suspend fun setDisplayName(displayName: String): Result<Unit>
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>
suspend fun joinRoom(roomId: RoomId): Result<RoomId> suspend fun joinRoom(roomId: RoomId): Result<Unit>
suspend fun knockRoom(roomId: RoomId): Result<Unit>
fun syncService(): SyncService fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService fun pushersService(): PushersService

View file

@ -440,7 +440,7 @@ class RustMatrixClient(
runCatching { client.removeAvatar() } runCatching { client.removeAvatar() }
} }
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) { override suspend fun joinRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.joinRoomById(roomId.value).destroy() client.joinRoomById(roomId.value).destroy()
try { try {
@ -448,10 +448,13 @@ class RustMatrixClient(
} 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")
} }
roomId
} }
} }
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
return Result.failure(NotImplementedError("Not yet implemented"))
}
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) { override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.trackRecentlyVisitedRoom(roomId.value) client.trackRecentlyVisitedRoom(roomId.value)

View file

@ -103,10 +103,12 @@ class FakeMatrixClient(
private var setDisplayNameResult: Result<Unit> = Result.success(Unit) private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
private var uploadAvatarResult: Result<Unit> = Result.success(Unit) private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
private var removeAvatarResult: Result<Unit> = Result.success(Unit) private var removeAvatarResult: Result<Unit> = Result.success(Unit)
var joinRoomLambda: (RoomId) -> Result<RoomId> = { var joinRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(it) Result.success(Unit)
}
var knockRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit)
} }
var getRoomInfoFlowLambda = { _: RoomId -> var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty()) flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
} }
@ -197,7 +199,9 @@ class FakeMatrixClient(
return removeAvatarResult return removeAvatarResult
} }
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = joinRoomLambda(roomId) override suspend fun joinRoom(roomId: RoomId): Result<Unit> = joinRoomLambda(roomId)
override suspend fun knockRoom(roomId: RoomId): Result<Unit> = knockRoomLambda(roomId)
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService