Browse Source

Merge pull request #3725 from element-hq/feature/fga/knock_request_to_join

Feature: knock request to join
pull/3731/head
ganfra 7 days ago committed by GitHub
parent
commit
98057c1c39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt
  2. 6
      features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt
  3. 4
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
  4. 3
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt
  5. 35
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
  6. 3
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
  7. 10
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
  8. 457
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
  9. 28
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt
  10. 2
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
  11. 18
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt
  12. 7
      features/joinroom/impl/src/main/res/values/localazy.xml
  13. 20
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt
  14. 8
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt
  15. 58
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
  16. 34
      features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
  17. 122
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt
  18. 14
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt
  19. 9
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt
  20. 3
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomSummaryDisplayType.kt
  21. 1
      features/roomlist/impl/src/main/res/values/localazy.xml
  22. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  23. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/PendingRoom.kt
  24. 36
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  25. 16
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustPendingRoom.kt
  26. 19
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt
  27. 16
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  28. 8
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakePendingRoom.kt
  29. 8
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt
  30. 3
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png
  31. 4
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png
  32. 4
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png
  33. 3
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png
  34. 4
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png
  35. 4
      tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png
  36. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_0_en.png
  37. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png
  38. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_0_en.png
  39. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png
  40. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_29_en.png
  41. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_31_en.png
  42. 3
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_32_en.png
  43. 3
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_33_en.png
  44. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_29_en.png
  45. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_31_en.png
  46. 3
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_32_en.png
  47. 3
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_33_en.png
  48. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_2_en.png
  49. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_2_en.png
  50. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_0_en.png
  51. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png
  52. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_1_en.png
  53. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_2_en.png
  54. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_6_en.png
  55. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_0_en.png
  56. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png
  57. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_1_en.png
  58. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_2_en.png
  59. 4
      tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_6_en.png
  60. 4
      tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_InviteSenderView_Day_0_en.png
  61. 4
      tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_InviteSenderView_Night_0_en.png
  62. 6
      tools/localazy/config.json

4
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt

@ -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

6
features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt

@ -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,

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

@ -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
} }

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

@ -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(

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

@ -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
} }

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

@ -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

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

@ -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
) )

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

@ -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)) { )
OutlinedButton( } else {
text = stringResource(CommonStrings.action_decline), val joinAuthorisationStatus = state.joinAuthorisationStatus
onClick = onDeclineInvite, when (joinAuthorisationStatus) {
modifier = Modifier.weight(1f), is JoinAuthorisationStatus.IsInvited -> {
size = ButtonSize.LargeLowPadding, ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
) OutlinedButton(
Button( text = stringResource(CommonStrings.action_decline),
text = stringResource(CommonStrings.action_accept), onClick = onDeclineInvite,
onClick = onAcceptInvite, modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding,
size = ButtonSize.LargeLowPadding, )
) Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
}
} }
} JoinAuthorisationStatus.CanJoin -> {
JoinAuthorisationStatus.CanJoin -> { SuperButton(
SuperButton( onClick = onJoinRoom,
onClick = onJoinRoom, modifier = Modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth(), buttonSize = ButtonSize.Large,
buttonSize = ButtonSize.Large, ) {
) { Text(
Text( text = stringResource(R.string.screen_join_room_join_action),
text = stringResource(R.string.screen_join_room_join_action), )
}
}
JoinAuthorisationStatus.CanKnock -> {
SuperButton(
onClick = onKnockRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_knock_action),
)
}
}
JoinAuthorisationStatus.IsKnocked -> {
OutlinedButton(
text = stringResource(R.string.screen_join_room_cancel_knock_action),
onClick = onCancelKnock,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
) )
} }
JoinAuthorisationStatus.Unknown -> Unit
} }
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 = {
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)
} }
}, else -> {
description = { DefaultLoadedContent(
Column( modifier = Modifier.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, contentState = contentState,
verticalArrangement = Arrangement.spacedBy(8.dp), applicationName = applicationName,
) { knockMessage = knockMessage,
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender onKnockMessageUpdate = onKnockMessageUpdate
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 = {
subtitle = { 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(
text = stringResource(id = CommonStrings.error_unknown), text = stringResource(R.string.screen_join_room_space_not_supported_title),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error, 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 = { },
) )
} }

28
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt

@ -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"))
}
}

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

@ -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,
) )

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

@ -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 { }
}
} }

7
features/joinroom/impl/src/main/res/values/localazy.xml

@ -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>

20
features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt

@ -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
features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt

@ -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)
} }
} }

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

@ -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
) )

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

@ -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
) )
} }
} }

122
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt

@ -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( RoomSummaryDisplayType.ROOM -> {
onAcceptClick = { RoomSummaryScaffoldRow(
eventSink(RoomListEvents.AcceptInvite(room)) room = room,
onClick = onClick,
onLongClick = {
eventSink(RoomListEvents.ShowContextMenu(room))
}, },
onDeclineClick = { ) {
eventSink(RoomListEvents.DeclineInvite(room)) NameAndTimestampRow(
} name = room.name,
) timestamp = room.timestamp,
isHighlighted = room.isHighlighted
)
LastMessageAndIndicatorRow(room = room)
}
} }
} RoomSummaryDisplayType.KNOCKED -> {
RoomSummaryDisplayType.ROOM -> { RoomSummaryScaffoldRow(
RoomSummaryScaffoldRow( room = room,
room = room, onClick = onClick,
onClick = onClick, onLongClick = {
onLongClick = { Timber.d("Long click on knocked room")
eventSink(RoomListEvents.ShowContextMenu(room)) },
}, ) {
modifier = modifier NameAndTimestampRow(
) { name = room.name,
NameAndTimestampRow( timestamp = null,
name = room.name, isHighlighted = room.isHighlighted
timestamp = room.timestamp, )
isHighlighted = room.isHighlighted if (room.canonicalAlias != null) {
) Text(
LastMessageAndIndicatorRow(room = room) 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,
)
}
} }
} }
} }

14
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt

@ -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)

9
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt

@ -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()
} }

3
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomSummaryDisplayType.kt

@ -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,
} }

1
features/roomlist/impl/src/main/res/values/localazy.xml

@ -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>

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

@ -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

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/InvitedRoom.kt → libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/PendingRoom.kt

@ -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>
} }

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

@ -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) {

16
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustInvitedRoom.kt → libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustPendingRoom.kt

@ -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()
} }
} }

19
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt

@ -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,
) )
} }

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

@ -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

8
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeInvitedRoom.kt → libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakePendingRoom.kt

@ -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
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt

@ -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,

3
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png

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

4
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png

@ -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

3
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png

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

4
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_0_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_0_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_29_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_31_en.png

@ -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

3
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_32_en.png

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

3
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_33_en.png

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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_29_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_31_en.png

@ -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

3
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_32_en.png

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

3
tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_33_en.png

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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_2_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_2_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_0_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_1_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_2_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_6_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_0_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_1_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_2_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_6_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_InviteSenderView_Day_0_en.png

@ -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

4
tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_InviteSenderView_Night_0_en.png

@ -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

6
tools/localazy/config.json

@ -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…
Cancel
Save