Browse Source

Implement Knock action (until the SDK level)

pull/2733/head
Benoit Marty 5 months ago committed by Benoit Marty
parent
commit
797c912ccd
  1. 2
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
  2. 1
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt
  3. 24
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
  4. 2
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
  5. 3
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
  6. 17
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
  7. 2
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
  8. 32
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt
  9. 26
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt
  10. 36
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
  11. 21
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
  12. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  13. 4
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  14. 6
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

2
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt

@ -19,6 +19,8 @@ package io.element.android.features.joinroom.impl @@ -19,6 +19,8 @@ package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents
data object KnockRoom : JoinRoomEvents
data object ClearError : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents
}

1
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt

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

24
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt

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

2
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt

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

3
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt

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

17
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt

@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMem @@ -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.pages.HeaderFooterPage
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.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
@ -60,6 +61,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @@ -60,6 +61,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun JoinRoomView(
state: JoinRoomState,
onBackPressed: () -> Unit,
onKnockSuccess: () -> Unit,
modifier: Modifier = Modifier,
) {
val gradientBackground = remember { LightGradientBackground() }
@ -85,12 +87,21 @@ fun JoinRoomView( @@ -85,12 +87,21 @@ fun JoinRoomView(
onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom)
},
onKnockRoom = {
state.eventSink(JoinRoomEvents.KnockRoom)
},
onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
)
}
)
AsyncActionView(
async = state.knockAction,
onSuccess = { onKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
)
}
@Composable
@ -99,6 +110,7 @@ private fun JoinRoomFooter( @@ -99,6 +110,7 @@ private fun JoinRoomFooter(
onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit,
onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -142,7 +154,7 @@ private fun JoinRoomFooter( @@ -142,7 +154,7 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus.CanKnock -> {
Button(
text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom,
onClick = onKnockRoom,
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
@ -263,6 +275,7 @@ private fun JoinRoomTopBar( @@ -263,6 +275,7 @@ private fun JoinRoomTopBar(
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
JoinRoomView(
state = state,
onBackPressed = { }
onBackPressed = { },
onKnockSuccess = { },
)
}

2
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt

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

32
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt

@ -0,0 +1,32 @@ @@ -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)
}

26
features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt

@ -0,0 +1,26 @@ @@ -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)
}

36
features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt

@ -20,7 +20,9 @@ import com.google.common.truth.Truth.assertThat @@ -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.AcceptDeclineInviteState
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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
@ -269,6 +271,38 @@ class JoinRoomPresenterTest { @@ -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
fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient(
@ -377,6 +411,7 @@ class JoinRoomPresenterTest { @@ -377,6 +411,7 @@ class JoinRoomPresenterTest {
roomId: RoomId = A_ROOM_ID,
roomDescription: Optional<RoomDescription> = Optional.empty(),
matrixClient: MatrixClient = FakeMatrixClient(),
knockRoom: KnockRoom = FakeKnockRoom(),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
): JoinRoomPresenter {
return JoinRoomPresenter(
@ -384,6 +419,7 @@ class JoinRoomPresenterTest { @@ -384,6 +419,7 @@ class JoinRoomPresenterTest {
roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription,
matrixClient = matrixClient,
knockRoom = knockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
)
}

21
features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt

@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity @@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
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.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@ -72,11 +73,25 @@ class JoinRoomViewTest { @@ -72,11 +73,25 @@ class JoinRoomViewTest {
),
)
rule.clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
}
@Test
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 invitationon IsInvited room emits the expected Event`() {
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
@ -118,11 +133,13 @@ class JoinRoomViewTest { @@ -118,11 +133,13 @@ class JoinRoomViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
state: JoinRoomState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
) {
setContent {
JoinRoomView(
state = state,
onBackPressed = onBackPressed,
onKnockSuccess = onKnockSuccess,
)
}
}

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

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

4
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -451,6 +451,10 @@ class RustMatrixClient( @@ -451,6 +451,10 @@ class RustMatrixClient(
}
}
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) {
runCatching {
client.trackRecentlyVisitedRoom(roomId.value)

6
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -106,7 +106,9 @@ class FakeMatrixClient( @@ -106,7 +106,9 @@ class FakeMatrixClient(
var joinRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit)
}
var knockRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit)
}
var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
}
@ -199,6 +201,8 @@ class FakeMatrixClient( @@ -199,6 +201,8 @@ class FakeMatrixClient(
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 pushersService(): PushersService = pushersService

Loading…
Cancel
Save