Merge pull request #2731 from element-hq/feature/bma/roomPreview2

Improve room preview rendering
This commit is contained in:
Benoit Marty 2024-04-19 14:24:33 +02:00 committed by GitHub
commit 2099676a22
96 changed files with 782 additions and 170 deletions

View file

@ -23,6 +23,11 @@ plugins {
android { android {
namespace = "io.element.android.features.joinroom.impl" namespace = "io.element.android.features.joinroom.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
anvil { anvil {
@ -46,10 +51,13 @@ dependencies {
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
} }

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
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.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.ui.model.toInviteSender
import java.util.Optional import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor( class JoinRoomPresenter @AssistedInject constructor(
@ -75,7 +76,9 @@ class JoinRoomPresenter @AssistedInject constructor(
value = ContentState.Loading(roomIdOrAlias) value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias()) val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
value = result.fold( value = result.fold(
onSuccess = { it.toContentState() }, onSuccess = { roomPreview ->
roomPreview.toContentState()
},
onFailure = { throwable -> onFailure = { throwable ->
if (throwable.message?.contains("403") == true) { if (throwable.message?.contains("403") == true) {
ContentState.UnknownRoom(roomIdOrAlias) ContentState.UnknownRoom(roomIdOrAlias)
@ -128,7 +131,8 @@ private fun RoomPreview.toContentState(): ContentState {
isDirect = false, isDirect = false,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when { joinAuthorisationStatus = when {
isInvited -> JoinAuthorisationStatus.IsInvited // Note when isInvited, roomInfo will be used, so if this happen, it will be temporary.
isInvited -> JoinAuthorisationStatus.IsInvited(null)
canKnock -> JoinAuthorisationStatus.CanKnock canKnock -> JoinAuthorisationStatus.CanKnock
isPublic -> JoinAuthorisationStatus.CanJoin isPublic -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown else -> JoinAuthorisationStatus.Unknown
@ -165,7 +169,9 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
isDirect = isDirect, isDirect = isDirect,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when { joinAuthorisationStatus = when {
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
inviteSender = inviter?.toInviteSender()
)
isPublic -> JoinAuthorisationStatus.CanJoin isPublic -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown else -> JoinAuthorisationStatus.Unknown
} }

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.ui.model.InviteSender
@Immutable @Immutable
data class JoinRoomState( data class JoinRoomState(
@ -71,9 +72,9 @@ sealed interface ContentState {
} }
} }
enum class JoinAuthorisationStatus { sealed interface JoinAuthorisationStatus {
IsInvited, data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
CanKnock, data object CanKnock : JoinAuthorisationStatus
CanJoin, data object CanJoin : JoinAuthorisationStatus
Unknown, data object Unknown : JoinAuthorisationStatus
} }

View file

@ -19,10 +19,14 @@ package io.element.android.features.joinroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.ui.model.InviteSender
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState> override val values: Sequence<JoinRoomState>
@ -48,7 +52,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
) )
), ),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
),
aJoinRoomState(
contentState = aLoadedContentState(
numberOfMembers = 123,
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()),
)
), ),
aJoinRoomState( aJoinRoomState(
contentState = aFailureContentState() contentState = aFailureContentState()
@ -102,5 +112,15 @@ fun aJoinRoomState(
eventSink = eventSink eventSink = eventSink
) )
internal fun anInviteSender(
userId: UserId = UserId("@bob:domain"),
displayName: String = "Bob",
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
) = InviteSender(
userId = userId,
displayName = displayName,
avatarData = avatarData,
)
private val A_ROOM_ID = RoomId("!exa:matrix.org") private val A_ROOM_ID = RoomId("!exa:matrix.org")
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org") private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")

View file

@ -16,13 +16,18 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -35,9 +40,11 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolec
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.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.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
@ -46,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.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
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -54,8 +62,10 @@ fun JoinRoomView(
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val gradientBackground = remember { LightGradientBackground() }
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier.background(gradientBackground),
containerColor = Color.Transparent,
paddingValues = PaddingValues(16.dp), paddingValues = PaddingValues(16.dp),
topBar = { topBar = {
JoinRoomTopBar(onBackClicked = onBackPressed) JoinRoomTopBar(onBackClicked = onBackPressed)
@ -97,41 +107,44 @@ private fun JoinRoomFooter(
text = stringResource(CommonStrings.action_retry), text = stringResource(CommonStrings.action_retry),
onClick = onRetry, onClick = onRetry,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Large,
) )
} else { } else {
val joinAuthorisationStatus = state.joinAuthorisationStatus val joinAuthorisationStatus = state.joinAuthorisationStatus
when (joinAuthorisationStatus) { when (joinAuthorisationStatus) {
JoinAuthorisationStatus.IsInvited -> { is JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton( OutlinedButton(
text = stringResource(CommonStrings.action_decline), text = stringResource(CommonStrings.action_decline),
onClick = onDeclineInvite, onClick = onDeclineInvite,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
size = ButtonSize.Medium, size = ButtonSize.Large,
) )
Button( Button(
text = stringResource(CommonStrings.action_accept), text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite, onClick = onAcceptInvite,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
size = ButtonSize.Medium, size = ButtonSize.Large,
) )
} }
} }
JoinAuthorisationStatus.CanJoin -> { JoinAuthorisationStatus.CanJoin -> {
Button( SuperButton(
text = stringResource(R.string.screen_join_room_join_action),
onClick = onJoinRoom, onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, buttonSize = ButtonSize.Large,
) ) {
Text(
text = stringResource(R.string.screen_join_room_join_action),
)
}
} }
JoinAuthorisationStatus.CanKnock -> { JoinAuthorisationStatus.CanKnock -> {
Button( Button(
text = stringResource(R.string.screen_join_room_knock_action), text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom, onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Large,
) )
} }
JoinAuthorisationStatus.Unknown -> Unit JoinAuthorisationStatus.Unknown -> Unit
@ -158,7 +171,16 @@ private fun JoinRoomContent(
RoomPreviewSubtitleAtom(contentState.computedSubtitle) RoomPreviewSubtitleAtom(contentState.computedSubtitle)
}, },
description = { description = {
RoomPreviewDescriptionAtom(contentState.topic ?: "") 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 ?: "")
}
}, },
memberCount = { memberCount = {
if (contentState.showMemberCount) { if (contentState.showMemberCount) {

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
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.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -33,6 +34,8 @@ 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.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
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.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
@ -101,7 +104,31 @@ class JoinRoomPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null))
}
}
}
@Test
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED,
inviter = inviter,
)
val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender))
} }
} }
} }

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.joinroom.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class JoinRoomViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invoke the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce {
rule.setJoinRoomView(
aJoinRoomState(
eventSink = eventsRecorder,
),
onBackPressed = it
)
rule.pressBack()
}
}
@Test
fun `clicking on Join room on CanJoin room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_join_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
}
@Test
fun `clicking on Knock room on CanKnock room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
}
@Test
fun `clicking on Accept invitationon IsInvited room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite)
}
@Test
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite)
}
@Test
fun `clicking on Retry when an error occurs emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aFailureContentState(),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
state: JoinRoomState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
) {
setContent {
JoinRoomView(
state = state,
onBackPressed = onBackPressed,
)
}
}

View file

@ -23,6 +23,11 @@ plugins {
android { android {
namespace = "io.element.android.features.roomaliasresolver.impl" namespace = "io.element.android.features.roomaliasresolver.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
anvil { anvil {
@ -44,10 +49,13 @@ dependencies {
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
} }

View file

@ -16,6 +16,7 @@
package io.element.android.features.roomaliasresolver.impl package io.element.android.features.roomaliasresolver.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -25,8 +26,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -36,6 +39,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
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.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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -61,9 +65,10 @@ fun RoomAliasResolverView(
latestOnAliasResolved(state.resolveState.data) latestOnAliasResolved(state.resolveState.data)
} }
} }
val gradientBackground = remember { LightGradientBackground() }
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier.background(gradientBackground),
containerColor = Color.Transparent,
paddingValues = PaddingValues(16.dp), paddingValues = PaddingValues(16.dp),
topBar = { topBar = {
RoomAliasResolverTopBar(onBackClicked = onBackPressed) RoomAliasResolverTopBar(onBackClicked = onBackPressed)
@ -92,7 +97,7 @@ private fun RoomAliasResolverFooter(
state.eventSink(RoomAliasResolverEvents.Retry) state.eventSink(RoomAliasResolverEvents.Retry)
}, },
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Large,
) )
} }
is AsyncData.Loading -> { is AsyncData.Loading -> {

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomAliasResolverViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invokes the expected callback`() {
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
ensureCalledOnce {
rule.setRoomAliasResolverView(
aRoomAliasResolverState(
eventSink = eventsRecorder,
),
onBackPressed = it
)
rule.pressBack()
}
}
@Test
fun `clicking on Retry emits the expected Event`() {
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>()
rule.setRoomAliasResolverView(
aRoomAliasResolverState(
resolveState = AsyncData.Failure(Exception("Error")),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry)
}
@Test
fun `success state invokes the expected Callback`() {
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
ensureCalledOnceWithParam(A_ROOM_ID) {
rule.setRoomAliasResolverView(
aRoomAliasResolverState(
resolveState = AsyncData.Success(A_ROOM_ID),
eventSink = eventsRecorder,
),
onAliasResolved = it,
)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomAliasResolverView(
state: RoomAliasResolverState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onAliasResolved: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
RoomAliasResolverView(
state = state,
onBackPressed = onBackPressed,
onAliasResolved = onAliasResolved,
)
}
}

View file

@ -23,10 +23,10 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.InviteSender
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.RoomSummaryDisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
import io.element.android.features.roomlist.impl.model.anInviteSender
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -88,11 +88,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
name = "Room Invited", name = "Room Invited",
avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem), avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem),
id = "!roomId:domain", id = "!roomId:domain",
inviteSender = InviteSender( inviteSender = anInviteSender(),
userId = UserId("@bob:domain"),
displayName = "Bob",
avatarData = AvatarData("@bob:domain", "Bob", size = AvatarSize.InviteSender),
),
displayType = RoomSummaryDisplayType.INVITE, displayType = RoomSummaryDisplayType.INVITE,
), ),
aRoomListRoomSummary( aRoomListRoomSummary(

View file

@ -47,7 +47,6 @@ 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.RoomListEvents import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.model.InviteSender
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
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
@ -67,6 +66,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber import timber.log.Timber
@ -96,7 +97,10 @@ internal fun RoomSummaryRow(
InviteSubtitle(isDirect = room.isDirect, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias) InviteSubtitle(isDirect = room.isDirect, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
if (!room.isDirect && room.inviteSender != null) { if (!room.isDirect && room.inviteSender != null) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
InviteSenderRow(sender = room.inviteSender) InviteSenderView(
modifier = Modifier.fillMaxWidth(),
inviteSender = room.inviteSender,
)
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
InviteButtonsRow( InviteButtonsRow(
@ -290,24 +294,6 @@ private fun InviteNameAndIndicatorRow(
} }
} }
@Composable
private fun InviteSenderRow(
sender: InviteSender,
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.fillMaxWidth(),
) {
Avatar(avatarData = sender.avatarData)
Text(
text = sender.annotatedString(),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
)
}
}
@Composable @Composable
private fun InviteButtonsRow( private fun InviteButtonsRow(
onAcceptClicked: () -> Unit, onAcceptClicked: () -> Unit,

View file

@ -16,7 +16,6 @@
package io.element.android.features.roomlist.impl.datasource package io.element.android.features.roomlist.impl.datasource
import io.element.android.features.roomlist.impl.model.InviteSender
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.RoomSummaryDisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.core.extensions.orEmpty
@ -27,6 +26,7 @@ import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
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.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.toInviteSender
import javax.inject.Inject import javax.inject.Inject
class RoomListRoomSummaryFactory @Inject constructor( class RoomListRoomSummaryFactory @Inject constructor(
@ -83,18 +83,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
hasRoomCall = roomSummary.details.hasRoomCall, hasRoomCall = roomSummary.details.hasRoomCall,
isDirect = roomSummary.details.isDirect, isDirect = roomSummary.details.isDirect,
isFavorite = roomSummary.details.isFavorite, isFavorite = roomSummary.details.isFavorite,
inviteSender = roomSummary.details.inviter?.run { inviteSender = roomSummary.details.inviter?.toInviteSender(),
InviteSender(
userId = userId,
displayName = displayName ?: "",
avatarData = AvatarData(
id = userId.value,
name = displayName,
url = avatarUrl,
size = AvatarSize.InviteSender,
),
)
},
isDm = roomSummary.details.isDm, isDm = roomSummary.details.isDm,
canonicalAlias = roomSummary.details.canonicalAlias, canonicalAlias = roomSummary.details.canonicalAlias,
displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) { displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) {

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
@Immutable @Immutable
data class RoomListRoomSummary( data class RoomListRoomSummary(
@ -42,7 +43,7 @@ data class RoomListRoomSummary(
val isDm: Boolean, val isDm: Boolean,
val isFavorite: Boolean, val isFavorite: Boolean,
val inviteSender: InviteSender?, val inviteSender: InviteSender?,
) { ) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread || isMarkedUnread ||

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> { open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary> override val values: Sequence<RoomListRoomSummary>
@ -86,7 +87,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
aRoomListRoomSummary( aRoomListRoomSummary(
displayType = RoomSummaryDisplayType.INVITE, displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender( inviteSender = anInviteSender(
userId = "@alice:matrix.org", userId = UserId("@alice:matrix.org"),
displayName = "Alice", displayName = "Alice",
), ),
canonicalAlias = RoomAlias("#alias:matrix.org"), canonicalAlias = RoomAlias("#alias:matrix.org"),
@ -95,7 +96,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
name = "Bob", name = "Bob",
displayType = RoomSummaryDisplayType.INVITE, displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender( inviteSender = anInviteSender(
userId = "@bob:matrix.org", userId = UserId("@bob:matrix.org"),
displayName = "Bob", displayName = "Bob",
), ),
isDirect = true, isDirect = true,
@ -105,12 +106,13 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
} }
internal fun anInviteSender( internal fun anInviteSender(
userId: String, userId: UserId = UserId("@bob:domain"),
displayName: String, displayName: String = "Bob",
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
) = InviteSender( ) = InviteSender(
userId = UserId(userId), userId = userId,
displayName = displayName, displayName = displayName,
avatarData = AvatarData(userId, displayName, size = AvatarSize.InviteSender), avatarData = avatarData,
) )
internal fun aRoomListRoomSummary( internal fun aRoomListRoomSummary(

View file

@ -21,9 +21,12 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -32,6 +35,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@ -49,14 +53,15 @@ import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
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.modifiers.applyIf import io.element.android.libraries.designsystem.modifiers.applyIf
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.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.copy import io.element.android.libraries.designsystem.utils.copy
@ -179,8 +184,8 @@ private fun RoomListSearchContent(
if (state.displayRoomDirectorySearch) { if (state.displayRoomDirectorySearch) {
RoomDirectorySearchButton( RoomDirectorySearchButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp), .padding(vertical = 24.dp, horizontal = 16.dp),
onClick = onRoomDirectorySearchClicked onClick = onRoomDirectorySearchClicked
) )
} }
@ -207,12 +212,24 @@ private fun RoomDirectorySearchButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Button( SuperButton(
text = stringResource(id = R.string.screen_roomlist_room_directory_button_title),
leadingIcon = IconSource.Vector(CompoundIcons.ListBulleted()),
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
) buttonSize = ButtonSize.Large,
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = CompoundIcons.ListBulleted(),
contentDescription = null,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.screen_roomlist_room_directory_button_title),
)
}
}
} }
@PreviewsDayNight @PreviewsDayNight

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.search
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomListSearchViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on 'Browse all rooms' invokes the expected callback`() {
val eventsRecorder = EventsRecorder<RoomListSearchEvents>(expectEvents = false)
ensureCalledOnce {
rule.setRoomListSearchView(
aRoomListSearchState(
isSearchActive = true,
isRoomDirectorySearchEnabled = true,
eventSink = eventsRecorder,
),
onRoomDirectorySearchClicked = it,
)
rule.clickOn(R.string.screen_roomlist_room_directory_button_title)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListSearchView(
state: RoomListSearchState,
eventSink: (RoomListEvents) -> Unit = EventsRecorder(expectEvents = false),
onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomListSearchView(
state = state,
eventSink = eventSink,
onRoomClicked = onRoomClicked,
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
)
}
}

View file

@ -154,7 +154,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:1.4.2" molecule-runtime = "app.cash.molecule:molecule-runtime:1.4.2"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.14" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.15"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -42,8 +41,7 @@ fun RoomPreviewMembersCountMolecule(
Row( Row(
modifier = modifier modifier = modifier
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
.widthIn(min = 48.dp) .padding(start = 2.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
.padding(start = 2.dp, end = 6.dp, top = 2.dp, bottom = 2.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {

View file

@ -23,9 +23,11 @@ import androidx.compose.foundation.layout.consumeWindowInsets
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.padding import androidx.compose.foundation.layout.padding
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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -36,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
/** /**
* @param modifier Classical modifier. * @param modifier Classical modifier.
* @param paddingValues padding values to apply to the content. * @param paddingValues padding values to apply to the content.
* @param containerColor color of the container. Set to [Color.Transparent] if you provide a background in the [modifier].
* @param background optional background component. * @param background optional background component.
* @param topBar optional topBar. * @param topBar optional topBar.
* @param header optional header. * @param header optional header.
@ -46,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
fun HeaderFooterPage( fun HeaderFooterPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(20.dp), paddingValues: PaddingValues = PaddingValues(20.dp),
containerColor: Color = MaterialTheme.colorScheme.background,
background: @Composable () -> Unit = {}, background: @Composable () -> Unit = {},
topBar: @Composable () -> Unit = {}, topBar: @Composable () -> Unit = {},
header: @Composable () -> Unit = {}, header: @Composable () -> Unit = {},
@ -55,6 +59,7 @@ fun HeaderFooterPage(
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = topBar, topBar = topBar,
containerColor = containerColor,
) { padding -> ) { padding ->
Box { Box {
background() background()

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.background
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RadialGradientShader
import androidx.compose.ui.graphics.Shader
import androidx.compose.ui.graphics.ShaderBrush
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
class LightGradientBackground(
private val firstColor: Color = Color(0x1E0DBD8B),
private val secondColor: Color = Color(0x001273EB),
private val ratio: Float = 642 / 775f,
) : ShaderBrush() {
override fun createShader(size: Size): Shader {
val biggerDimension = size.width * 1.98f
return RadialGradientShader(
colors = listOf(firstColor, secondColor),
center = size.center.copy(x = size.width * ratio, y = size.height * ratio),
radius = biggerDimension / 2f,
colorStops = listOf(0f, 0.95f)
)
}
}
@PreviewsDayNight
@Composable
internal fun LightGradientBackgroundPreview() = ElementPreview {
val gradientBackground = remember {
LightGradientBackground()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBackground)
)
}

View file

@ -61,6 +61,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.map import io.element.android.libraries.matrix.impl.room.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
@ -463,21 +464,15 @@ class RustMatrixClient(
} }
} }
@Suppress("TooGenericExceptionThrown")
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) { override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) {
runCatching { runCatching {
// TODO Waiting for SDK to be released client.resolveRoomAlias(roomAlias.value).let(::RoomId)
throw Exception("Not implemented")
// client.resolveRoomAlias(roomAlias.value).let(::RoomId)
} }
} }
@Suppress("TooGenericExceptionThrown")
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) { override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching { runCatching {
// TODO Waiting for SDK to be released client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
throw Exception("Not implemented")
// client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
} }
} }

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room.preview
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
object RoomPreviewMapper {
fun map(roomPreview: RustRoomPreview): RoomPreview {
return RoomPreview(
roomId = RoomId(roomPreview.roomId),
canonicalAlias = roomPreview.canonicalAlias?.let(::RoomAlias),
name = roomPreview.name,
topic = roomPreview.topic,
avatarUrl = roomPreview.avatarUrl,
numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
roomType = roomPreview.roomType,
isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
isJoined = roomPreview.isJoined,
isInvited = roomPreview.isInvited,
isPublic = roomPreview.isPublic,
canKnock = roomPreview.canKnock
)
}
}

View file

@ -34,6 +34,7 @@ dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.core) implementation(projects.libraries.core)

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.InviteSender
@Composable
fun InviteSenderView(
inviteSender: InviteSender,
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Avatar(avatarData = inviteSender.avatarData)
Text(
text = inviteSender.annotatedString(),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
)
}
}
@PreviewsDayNight
@Composable
internal fun InviteSenderViewPreview() = ElementPreview {
InviteSenderView(
inviteSender = InviteSender(
userId = UserId("@bob:example.com"),
displayName = "Bob",
avatarData = AvatarData(
id = "@bob:example.com",
name = "Bob",
url = null,
size = AvatarSize.InviteSender
)
)
)
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomlist.impl.model package io.element.android.libraries.matrix.ui.model
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -24,9 +24,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.ui.R
@Immutable @Immutable
data class InviteSender( data class InviteSender(
@ -54,3 +56,14 @@ data class InviteSender(
} }
} }
} }
fun RoomMember.toInviteSender() = InviteSender(
userId = userId,
displayName = displayName ?: "",
avatarData = AvatarData(
id = userId.value,
name = displayName,
url = avatarUrl,
size = AvatarSize.InviteSender,
),
)

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
</resources>

View file

@ -16,8 +16,10 @@
package io.element.android.tests.konsist package io.element.android.tests.konsist
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf
import com.lemonappdev.konsist.api.verify.assertEmpty
import com.lemonappdev.konsist.api.verify.assertTrue import com.lemonappdev.konsist.api.verify.assertTrue
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import org.junit.Test import org.junit.Test
@ -57,4 +59,15 @@ class KonsistPreviewTest {
it.hasInternalModifier it.hasInternalModifier
} }
} }
@Test
fun `Ensure that '@PreviewLightDark' is not used`() {
Konsist
.scopeFromProject()
.functions()
.withAllAnnotationsOf(PreviewLightDark::class)
.assertEmpty(
additionalMessage = "Use '@PreviewsDayNight' instead of '@PreviewLightDark', or else screenshot(s) will not be generated.",
)
}
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f7406a2a6fa8e4dd4423a5141147ecc1aa235d4dbff980b7ef72fa2e59f502fc oid sha256:a6e8a01993250ab9e5ecc869cedcd0b8f210bbe08623fbb55a1b8b0863bebb22
size 7686 size 125916

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:329ade4a3d0e1b013f03bf5d2d107ce2dacc235f7e2defc24c5cd1623330b107 oid sha256:d7c1c180f53b948992d1f14d9635bcfee13fad55e772c9d74af62a747928a540
size 23944 size 143814

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0ed85e4bb1351a1af82b64628c35aff7ef165c9df9b1ac0e1bba737a9d1e15a8 oid sha256:1dc731d4a0c2aeb383035b3781f1e319fe34445e83324458e997df22a3d09bdc
size 25797 size 145241

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0fcfb2b5cc790707f48dbd56f2c3f4909ebe1ae93b082309e7b0b89e82fec6c8 oid sha256:2678e82f1e260b67ebbca47b41a0283d89bbc1799905c32f4e90821d7f41ce6c
size 39320 size 148232

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ba446e166858f6da8d79ae4af997ff142d73edebf178aadf75579b041a3d267 oid sha256:3da95310c379b64cece500e9a942170f227dfe4c9d856690d2343ae9aaf47dc5
size 28605 size 144682

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c34154223d9eef3a66fbc9c266010e3e51b293db7f4698d550638fa8912a231 oid sha256:2128dda46d28adeabcd77ae254b98e5305c48cfc4578b53bd4b91c222a129e39
size 16864 size 151787

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9e70d55552b46b1a34848651bf8bcf8048ba2d28c7d2d356f8c1a0f71bb6e049 oid sha256:0134f458078bde571001caea88804e35e50dc89c4975bf4c7b86582662e63b08
size 21104 size 125696

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fa320f4667087c59776d08dfb9108a577e42eb51ba1e6b3d3ea6da3d55605495 oid sha256:ded3501ef083c6b8ac2da5259fab157c65dec8034e1d1fe7b03fbe826a11ebad
size 7523 size 109459

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d4244c5d24935e2cf6df3cff5d31eb7980757a1e555aedbb97561c1827778e2b oid sha256:f7a49f72bcf030670dd014469cf906a7097c07d6fad17149edee92f128ab2dc6
size 22816 size 125866

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3725b3e21eb939bebcc44b9c62f21817f1ab227dc7a9f2367b721cdb0e750720 oid sha256:c134bdb3e372eeaedc3669aaf92486cbb253fcf41b0023c03743bf7e61c72456
size 25127 size 129893

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e23f1d506bcfc603192d854945059b4f03e9215f20f3f7f9150728aee0c4bf7e oid sha256:2af528ba157c4eb2f779e50dd3b95d190c0c68aaa96899ebfa7bc9c843469b00
size 38085 size 131811

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:89c6f6644f84fc98ee0a0ced85f141cc862b1c114b2ca29874696f655926b4f2 oid sha256:124a10348cdf9e3e5aba4bbc89c93a1af30e38f58f3e44b2255e705abe5c0c8c
size 27539 size 127801

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:49c58bd2bbc86105e547bddf47c69df0017d6a4a66be0dc3f80e8cecfacf7a86 oid sha256:ae55c1be7450df7cfd40ae931af2a59b9fe8adbde79e83a3cf395a435a7b534f
size 15837 size 135115

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:740d856d28b44200e7148d297103f8c38e2159c75e182c84f1d3d0d32cfff1bf oid sha256:23826df8b3dde24c1592bf2b9160ffb3995cee09e3e704789ec9fa8bbaa42db2
size 19529 size 109429

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9651397f53e398e05c27f41f341d1ae949fbdbce05e97083bb9aef909f67fbaf oid sha256:41e7732084d84eca5fc002210fd5cba3f7cd181eab1c1dc72b9fe672a861f1f8
size 11783 size 131980

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:63c9d5c445abb20a80b45100afc59b6803f5744a9a6f83ce3d91653801fdd3eb oid sha256:8f23e1f04cb1fe9ff532b1bef04da3f61cf66a2ae468a4865c9ceceba12e36c2
size 13455 size 133562

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dfc692c20e4b8998a675b72cac9b324da4ec636df68a5611ceaa368217e50243 oid sha256:8f53c21157a6c5ccd9afe9a28154714ad97f16cffaa8d6eb41834653b217d6af
size 19618 size 128419

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:15efd8dc8b74d3e5364b3f9204a5e85e0cff8689d3b632a61368e101b2e833ae oid sha256:f9f9b0ab31815d16a4550f78380343214ee4f53f2fd070c3a1070796531d6662
size 11099 size 114522

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:18cc9896eadf78db99e6f8a96ab67a2af22ce1f7072f84cd97dee6e50fd474b2 oid sha256:b51f473b67d4d444819b373880baefd17f091056bcc72d19eab70364d0cde0c8
size 12736 size 116158

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d7db045b1e7877a6ee7b51e3996d43bcaee22ae930ade59701fd07f7a6b0eabf oid sha256:03083a5d9c59711d6db23b1e150a01f4de53cdaca22c2ab2c09bf225fb91efd6
size 18250 size 111907

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4efec5e5b41fe5b51aaf55bd6d1b4466cef98bea89b2bd1cdc385ef02bc38903 oid sha256:08a59921cc9b6a19f66655412827bc0cf9dc902bc5e8db56a41c2484fc0468c3
size 44382 size 44381

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:12f219b2949bdb38cb02cec442796022b92adeaa39ccde06e40ea7ce91192545 oid sha256:bb14249c640d8b88865fe7aaae62f55b862475a555007f5b51c1dc778d9e8f33
size 43679 size 43701

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1edf88cfd802b426e03c600b225fc0a6c97605809ee75da9c8f742dc62ece36d oid sha256:cf3f468d3b448561fd6b91a66ffdb3ce664e67ba17b5426df32051e672a6b1da
size 24554 size 24545

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:17595de1e00e0677ba42985f0ad0f68f2799c63323bbe97c7bc16b7a8ace17ed oid sha256:55c60a529e8c9230b39ae919faf9d72978f37452846560ca4df1b784c7d154eb
size 23891 size 23877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:615e00dfa47d40fd459d268d6e68c61a501be7e4a24bfe7d18f3a647da8c5f08 oid sha256:925a868134fc7d40eaa88c81d929d6b8bc0c6a7aae9cd0269339c2e5f5edfb66
size 10595 size 24167

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4693928382d102599e18c2254c58451c854c304b19706ecb02b832c92ddf15ef oid sha256:b51c1aa294a9881a549cbac7b7b1f4f6465bc2d96fbfaddbfc47feeb8d7a13c9
size 47041 size 47040

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:421e47de6c2bc7fd0a5de84b6db4bea79c7312442244ec11506164150762943b oid sha256:47b0809b3e4534125d5e2d0fb8bb4db6435e488eef312be0ecbd6d36ef1212b5
size 9787 size 24451

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:deeab9451d51a2b3403f1d5711d0cf64b762847a674ae78f4232ce02fed575f7 oid sha256:5aaf23300ca5d75620b4f3498d2a0a8f8568292c14bf81b89dcca9ce1ecb5fad
size 46014 size 46039

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:34afdcdb8cd199ff67ff628aef28757381c5f5f4bed57131f5eac2ae7423894f oid sha256:1a219527b5d2fc5e581f9e4e5026284a823f0e67b1ec33e12af526b3628d52e6
size 80573 size 80571

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:34afdcdb8cd199ff67ff628aef28757381c5f5f4bed57131f5eac2ae7423894f oid sha256:1a219527b5d2fc5e581f9e4e5026284a823f0e67b1ec33e12af526b3628d52e6
size 80573 size 80571

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5509141f2b39c0f4330ef256687b31f0d421040f58719a62aad34f6a224e1cf4 oid sha256:fc44364cd6f9862a9ee949897ca6a6368f4c7fff8f5bf9ec954e4719c6dc2730
size 90443 size 90423

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5a3ea7afa73bfd0ace6999afa7cefd0f99069808b0cb10601e7c674b7f45ea0e oid sha256:873e6d0627246b4e74407020419e5c10dbc1b774c517556d293ef37235002fde
size 80631 size 80631

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7bd9d6331dfe578c1755f3f3c5cf52bafb6b7d0bd811c122daa79adf5066e72a oid sha256:bb0cf03d1806a0312e10bc4d758102dd0b872d88106464f2112de2dc505e7e7b
size 102780 size 102780

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4cb1163e0853dfb5df0f922095a36a4d183decc24a9e4736861186dda30b77cc oid sha256:beeee93d72ab3bdd601dd03eec5beb271181a200c11c042b985b9495ded0c320
size 82859 size 82878

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4cb1163e0853dfb5df0f922095a36a4d183decc24a9e4736861186dda30b77cc oid sha256:beeee93d72ab3bdd601dd03eec5beb271181a200c11c042b985b9495ded0c320
size 82859 size 82878

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4b3263df594697683a84f15268faeb69709bd3bf0cbb4770a4ce883cad599a2e oid sha256:9a90f232c00dc9b7c1e74b16461e6b91b18f5729f771c3a60f4aa68c29513e97
size 91815 size 91791

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:89d0f544f879c7d2fad983cac29f3f58e7aac2d6e0530151b4f51ecef3628aaa oid sha256:df8723b2a7cbc1720128a0e8dfdbb10cb82394881ad216ed328b15395302e463
size 82318 size 82322

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1fce8f9d8f11b55c390fd8136f7f005ca7bc199e36c5231b9ac3b6118e2e7b5c oid sha256:c6d4e0c7fc8607c6d05997e0c1201311cbe05a69851b9828156969f2ce300246
size 104166 size 104171

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:754e5614822424e86e73a631f5303074e85dfb6bf4e6abc7f7e698e2cbc8d6ba oid sha256:0c8f59a7ab4ca881281b2859ff9fe0ac07cedb0d0c11212365cc26138edc1193
size 12850 size 12891

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d06d65a5768007020eab6b48844ea1216fcdb9b19f17f15171234f8a5dd9c517 oid sha256:62aab15deb6d98f70457e9682df2150a6fa73cd34ae8103a2e618c4d87aa4adc
size 12394 size 12398

View file

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

View file

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

View file

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

View file

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

View file

@ -224,6 +224,12 @@
"troubleshoot_notifications_screen_.*" "troubleshoot_notifications_screen_.*"
] ]
}, },
{
"name" : ":libraries:matrixui",
"includeRegex" : [
"screen_invites_invited_you"
]
},
{ {
"name" : ":features:call", "name" : ":features:call",
"includeRegex" : [ "includeRegex" : [

View file

@ -25,6 +25,9 @@ set -e
# First run the quickest script # First run the quickest script
./tools/check/check_code_quality.sh ./tools/check/check_code_quality.sh
# Check ktlint first
./gradlew ktlintCheck
# Build, test and check the project, with warning as errors # Build, test and check the project, with warning as errors
# It also check that the minimal app is compiling. # It also check that the minimal app is compiling.
./gradlew check -PallWarningsAsErrors=true ./gradlew check -PallWarningsAsErrors=true