Merge pull request #5378 from element-hq/feature/fga/join_space

Feature : Join Space (WIP)
This commit is contained in:
Benoit Marty 2025-09-24 13:50:07 +02:00 committed by GitHub
commit e12ac9f2ff
94 changed files with 848 additions and 464 deletions

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@ -36,3 +37,11 @@ fun RoomInfo.toInviteData(): InviteData {
isDm = isDm, isDm = isDm,
) )
} }
fun SpaceRoom.toInviteData(): InviteData {
return InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = false,
)
}

View file

@ -43,4 +43,5 @@ dependencies {
testImplementation(projects.features.invite.test) testImplementation(projects.features.invite.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.previewutils)
} }

View file

@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@ -42,17 +43,21 @@ import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind import io.element.android.libraries.matrix.api.exception.ErrorKind
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.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@Inject @Inject
class JoinRoomPresenter( class JoinRoomPresenter(
@ -80,6 +85,8 @@ class JoinRoomPresenter(
): JoinRoomPresenter ): JoinRoomPresenter
} }
private val spaceList = matrixClient.spaceService.spaceRoomList(roomId)
@Composable @Composable
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -87,6 +94,9 @@ class JoinRoomPresenter(
val roomInfo by remember { val roomInfo by remember {
matrixClient.getRoomInfoFlow(roomId) matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty()) }.collectAsState(initial = Optional.empty())
val spaceRoom by remember {
spaceList.currentSpaceFlow()
}.collectAsState()
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) } val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
@ -96,55 +106,41 @@ class JoinRoomPresenter(
val hideInviteAvatars by matrixClient.rememberHideInvitesAvatar() val hideInviteAvatars by matrixClient.rememberHideInvitesAvatar()
val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() } val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() }
val contentState by produceState<ContentState>( var contentState by remember {
initialValue = ContentState.Loading, mutableStateOf<ContentState>(ContentState.Loading)
key1 = roomInfo, }
key2 = retryCount, LaunchedEffect(roomInfo, retryCount, isDismissingContent, spaceRoom) {
key3 = isDismissingContent,
) {
when { when {
isDismissingContent -> value = ContentState.Dismissing isDismissingContent -> contentState = ContentState.Dismissing
roomInfo.isPresent -> { roomInfo.isPresent -> {
val notJoinedRoom = matrixClient.getRoomPreview(roomIdOrAlias, serverNames).getOrNull() val notJoinedRoom = matrixClient.getRoomPreview(roomIdOrAlias, serverNames).getOrNull()
val (sender, reason) = when (roomInfo.get().currentUserMembership) { val membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull()
CurrentUserMembership.BANNED -> {
// Workaround to get info about the sender for banned rooms
// TODO re-do this once we have a better API in the SDK
val membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull()
membershipDetails?.senderMember to membershipDetails?.currentUserMember?.membershipChangeReason
}
CurrentUserMembership.INVITED -> {
roomInfo.get().inviter to null
}
else -> null to null
}
val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers
value = roomInfo.get().toContentState( contentState = roomInfo.get().toContentState(
membershipSender = sender,
joinedMembersCountOverride = joinedMembersCountOverride, joinedMembersCountOverride = joinedMembersCountOverride,
reason = reason, membershipDetails = membershipDetails,
childrenCount = spaceRoom.getOrNull()?.childrenCount,
) )
} }
spaceRoom.isPresent -> {
val spaceRoom = spaceRoom.get()
// Only use this state when space is not locally known
contentState = if (spaceRoom.state != null) {
ContentState.Loading
} else {
spaceRoom.toContentState()
}
}
roomDescription.isPresent -> { roomDescription.isPresent -> {
value = roomDescription.get().toContentState() contentState = roomDescription.get().toContentState()
} }
else -> { else -> {
value = ContentState.Loading contentState = ContentState.Loading
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames) val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
value = result.fold( contentState = result.fold(
onSuccess = { preview -> onSuccess = { preview ->
val membershipInfo = when (preview.previewInfo.membership) { val membershipDetails = preview.membershipDetails().getOrNull()
CurrentUserMembership.INVITED, preview.previewInfo.toContentState(membershipDetails)
CurrentUserMembership.BANNED,
CurrentUserMembership.KNOCKED -> {
preview.membershipDetails().getOrNull()
}
else -> null
}
preview.previewInfo.toContentState(
senderMember = membershipInfo?.senderMember,
reason = membershipInfo?.currentUserMember?.membershipChangeReason,
)
}, },
onFailure = { throwable -> onFailure = { throwable ->
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) { if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
@ -252,30 +248,56 @@ class JoinRoomPresenter(
} }
} }
private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState { private fun RoomPreviewInfo.toContentState(membershipDetails: RoomMembershipDetails?): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
roomId = roomId, roomId = roomId,
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = numberOfJoinedMembers, numberOfMembers = numberOfJoinedMembers,
isDm = false,
roomType = roomType,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (membership) { joinAuthorisationStatus = computeJoinAuthorisationStatus(
CurrentUserMembership.INVITED -> { membership,
JoinAuthorisationStatus.IsInvited( membershipDetails,
inviteData = toInviteData(), joinRule,
inviteSender = senderMember?.toInviteSender() { toInviteData() }
) ),
} joinRule = joinRule,
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason) details = when (roomType) {
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked is RoomType.Other,
else -> joinRule.toJoinAuthorisationStatus() RoomType.Room -> LoadedDetails.Room(
isDm = false,
)
RoomType.Space -> LoadedDetails.Space(
childrenCount = 0,
heroes = persistentListOf(),
)
} }
) )
} }
private fun SpaceRoom.toContentState(): ContentState {
return ContentState.Loaded(
roomId = roomId,
name = name,
topic = topic,
alias = canonicalAlias,
numberOfMembers = numJoinedMembers.toLong(),
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = computeJoinAuthorisationStatus(
membership = state,
membershipDetails = null,
joinRule = joinRule,
inviteData = { toInviteData() }
),
joinRule = joinRule,
details = LoadedDetails.Space(
childrenCount = childrenCount,
heroes = heroes.toPersistentList(),
)
)
}
@VisibleForTesting @VisibleForTesting
internal fun RoomDescription.toContentState(): ContentState { internal fun RoomDescription.toContentState(): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
@ -284,22 +306,29 @@ internal fun RoomDescription.toContentState(): ContentState {
topic = topic, topic = topic,
alias = alias, alias = alias,
numberOfMembers = numberOfMembers, numberOfMembers = numberOfMembers,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (joinRule) { joinAuthorisationStatus = when (joinRule) {
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown else -> JoinAuthorisationStatus.Unknown
} },
joinRule = when (joinRule) {
RoomDescription.JoinRule.KNOCK -> JoinRule.Knock
RoomDescription.JoinRule.PUBLIC -> JoinRule.Public
RoomDescription.JoinRule.RESTRICTED -> JoinRule.Restricted(persistentListOf())
RoomDescription.JoinRule.KNOCK_RESTRICTED -> JoinRule.KnockRestricted(persistentListOf())
RoomDescription.JoinRule.INVITE -> JoinRule.Invite
RoomDescription.JoinRule.UNKNOWN -> null
},
details = LoadedDetails.Room(isDm = false)
) )
} }
@VisibleForTesting @VisibleForTesting
internal fun RoomInfo.toContentState( internal fun RoomInfo.toContentState(
membershipSender: RoomMember?,
joinedMembersCountOverride: Long?, joinedMembersCountOverride: Long?,
reason: String?, membershipDetails: RoomMembershipDetails?,
childrenCount: Int?,
): ContentState { ): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
roomId = id, roomId = id,
@ -307,24 +336,49 @@ internal fun RoomInfo.toContentState(
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount, numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount,
isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (currentUserMembership) { joinAuthorisationStatus = computeJoinAuthorisationStatus(
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( membership = currentUserMembership,
inviteData = toInviteData(), membershipDetails = membershipDetails,
inviteSender = membershipSender?.toInviteSender(), joinRule = joinRule,
inviteData = { toInviteData() }
),
joinRule = joinRule,
details = if (isSpace) {
LoadedDetails.Space(
childrenCount = childrenCount ?: 0,
heroes = heroes,
) )
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned( } else {
banSender = membershipSender?.toInviteSender(), LoadedDetails.Room(
reason = reason, isDm = isDm,
) )
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked },
else -> joinRule.toJoinAuthorisationStatus()
}
) )
} }
private fun computeJoinAuthorisationStatus(
membership: CurrentUserMembership?,
membershipDetails: RoomMembershipDetails?,
joinRule: JoinRule?,
inviteData: () -> InviteData,
): JoinAuthorisationStatus {
return when (membership) {
CurrentUserMembership.INVITED -> {
JoinAuthorisationStatus.IsInvited(
inviteData = inviteData(),
inviteSender = membershipDetails?.senderMember?.toInviteSender()
)
}
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
membershipDetails?.senderMember?.toInviteSender(),
membershipDetails?.membershipChangeReason
)
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
else -> joinRule.toJoinAuthorisationStatus()
}
}
private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus { private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
return when (this) { return when (this) {
JoinRule.Knock, JoinRule.Knock,

View file

@ -16,9 +16,11 @@ 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.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500 internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@ -41,9 +43,6 @@ data class JoinRoomState(
val joinAuthorisationStatus = when (contentState) { val joinAuthorisationStatus = when (contentState) {
is ContentState.Loaded -> { is ContentState.Loaded -> {
when { when {
contentState.roomType == RoomType.Space -> {
JoinAuthorisationStatus.IsSpace(applicationName)
}
isJoinActionUnauthorized -> { isJoinActionUnauthorized -> {
JoinAuthorisationStatus.Unauthorized JoinAuthorisationStatus.Unauthorized
} }
@ -77,12 +76,13 @@ sealed interface ContentState {
val topic: String?, val topic: String?,
val alias: RoomAlias?, val alias: RoomAlias?,
val numberOfMembers: Long?, val numberOfMembers: Long?,
val isDm: Boolean,
val roomType: RoomType,
val roomAvatarUrl: String?, val roomAvatarUrl: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus, val joinAuthorisationStatus: JoinAuthorisationStatus,
val joinRule: JoinRule?,
val details: LoadedDetails,
) : ContentState { ) : ContentState {
val showMemberCount = numberOfMembers != null val showMemberCount = numberOfMembers != null
val isSpace = details is LoadedDetails.Space
fun avatarData(size: AvatarSize): AvatarData { fun avatarData(size: AvatarSize): AvatarData {
return AvatarData( return AvatarData(
@ -95,9 +95,20 @@ sealed interface ContentState {
} }
} }
@Immutable
sealed interface LoadedDetails {
data class Room(
val isDm: Boolean,
) : LoadedDetails
data class Space(
val childrenCount: Int,
val heroes: ImmutableList<MatrixUser>,
) : LoadedDetails
}
sealed interface JoinAuthorisationStatus { sealed interface JoinAuthorisationStatus {
data object None : JoinAuthorisationStatus data object None : JoinAuthorisationStatus
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
data object IsKnocked : JoinAuthorisationStatus data object IsKnocked : JoinAuthorisationStatus

View file

@ -20,9 +20,11 @@ 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.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.exception.ClientException import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toPersistentList
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState> override val values: Sequence<JoinRoomState>
@ -77,13 +79,17 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
name = "A space", name = "A space",
alias = null, alias = null,
topic = "This is the topic of a space", topic = "This is the topic of a space",
roomType = RoomType.Space, details = aLoadedDetailsSpace(
childrenCount = 42,
),
) )
), ),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState( contentState = aLoadedContentState(
name = "A DM", name = "A DM",
isDm = true, details = aLoadedDetailsRoom(
isDm = true,
),
) )
), ),
aJoinRoomState( aJoinRoomState(
@ -156,20 +162,34 @@ fun aLoadedContentState(
alias: RoomAlias? = RoomAlias("#exa:matrix.org"), alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
topic: String? = "Element X is a secure, private and decentralized messenger.", topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null, numberOfMembers: Long? = null,
isDm: Boolean = false,
roomType: RoomType = RoomType.Room,
roomAvatarUrl: String? = null, roomAvatarUrl: String? = null,
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
joinRule: JoinRule? = null,
details: LoadedDetails = aLoadedDetailsRoom(isDm = false),
) = ContentState.Loaded( ) = ContentState.Loaded(
roomId = roomId, roomId = roomId,
name = name, name = name,
alias = alias, alias = alias,
topic = topic, topic = topic,
numberOfMembers = numberOfMembers, numberOfMembers = numberOfMembers,
isDm = isDm,
roomType = roomType,
roomAvatarUrl = roomAvatarUrl, roomAvatarUrl = roomAvatarUrl,
joinAuthorisationStatus = joinAuthorisationStatus joinAuthorisationStatus = joinAuthorisationStatus,
joinRule = joinRule,
details = details,
)
fun aLoadedDetailsRoom(
isDm: Boolean = false,
) = LoadedDetails.Room(
isDm = isDm
)
fun aLoadedDetailsSpace(
childrenCount: Int = 0,
heroes: List<MatrixUser> = emptyList(),
) = LoadedDetails.Space(
childrenCount = childrenCount,
heroes = heroes.toPersistentList()
) )
fun aJoinRoomState( fun aJoinRoomState(

View file

@ -7,21 +7,20 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.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.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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -38,6 +37,7 @@ 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
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
@ -65,14 +65,21 @@ 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.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconSource
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.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
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.theme.placeholderBackground
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.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.ui.components.SpaceInfoRow
import io.element.android.libraries.matrix.ui.components.SpaceMembersView
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 kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun JoinRoomView( fun JoinRoomView(
@ -92,7 +99,7 @@ fun JoinRoomView(
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentPadding = PaddingValues( contentPadding = PaddingValues(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 32.dp vertical = 24.dp
), ),
topBar = { topBar = {
JoinRoomTopBar( JoinRoomTopBar(
@ -220,12 +227,14 @@ private fun JoinRoomFooter(
onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) }, onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding, size = ButtonSize.LargeLowPadding,
leadingIcon = IconSource.Vector(CompoundIcons.Close())
) )
Button( Button(
text = stringResource(CommonStrings.action_accept), text = stringResource(CommonStrings.action_accept),
onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) }, onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding, size = ButtonSize.LargeLowPadding,
leadingIcon = IconSource.Vector(CompoundIcons.Check())
) )
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -278,7 +287,6 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom) JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom) JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack) JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack)
is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack)
JoinAuthorisationStatus.None -> Unit JoinAuthorisationStatus.None -> Unit
} }
} }
@ -358,28 +366,6 @@ private fun JoinRestrictedFooter(
} }
} }
@Composable
private fun UnsupportedSpaceFooter(
applicationName: String,
onGoBack: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Announcement(
title = stringResource(R.string.screen_join_room_space_not_supported_title),
description = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
type = AnnouncementType.Informative(),
)
Spacer(Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_ok),
onClick = onGoBack,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
}
}
@Composable @Composable
private fun JoinRoomContent( private fun JoinRoomContent(
roomIdOrAlias: RoomIdOrAlias, roomIdOrAlias: RoomIdOrAlias,
@ -397,19 +383,40 @@ private fun JoinRoomContent(
IsKnockedLoadedContent() IsKnockedLoadedContent()
} }
else -> { else -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender horizontalAlignment = Alignment.CenterHorizontally,
if (inviteSender != null) { modifier = Modifier.verticalScroll(rememberScrollState())
InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages) ) {
Spacer(modifier = Modifier.height(32.dp))
}
DefaultLoadedContent( DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState, contentState = contentState,
knockMessage = knockMessage,
hideAvatarImage = hideAvatarsImages, hideAvatarImage = hideAvatarsImages,
onKnockMessageUpdate = onKnockMessageUpdate
) )
when (contentState.joinAuthorisationStatus) {
is JoinAuthorisationStatus.IsInvited -> {
val inviteSender = contentState.joinAuthorisationStatus.inviteSender
if (inviteSender != null) {
Spacer(Modifier.height(16.dp))
InvitedByView(inviteSender, hideAvatarsImages)
}
}
is JoinAuthorisationStatus.CanKnock -> {
Spacer(modifier = Modifier.height(24.dp))
val supportingText = if (knockMessage.isNotEmpty()) {
"${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
} else {
stringResource(R.string.screen_join_room_knock_message_description)
}
TextField(
value = knockMessage,
onValueChange = onKnockMessageUpdate,
maxLines = 3,
minLines = 3,
modifier = Modifier.fillMaxWidth(),
supportingText = supportingText
)
}
else -> Unit
}
} }
} }
} }
@ -422,6 +429,45 @@ private fun JoinRoomContent(
} }
} }
@Composable
private fun InvitedByView(
sender: InviteSender,
hideAvatarImage: Boolean,
modifier: Modifier = Modifier
) {
Column(
modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.screen_join_room_invited_by),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary
)
Spacer(Modifier.height(8.dp))
Avatar(
avatarData = sender.avatarData,
avatarType = AvatarType.User,
hideImage = hideAvatarImage,
forcedAvatarSize = AvatarSize.RoomPreviewInviter.dp
)
Spacer(Modifier.height(8.dp))
Text(
text = sender.displayName,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary
)
Spacer(Modifier.height(4.dp))
Text(
text = sender.userId.value,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary
)
}
}
@Composable @Composable
private fun UnknownRoomContent( private fun UnknownRoomContent(
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -429,7 +475,21 @@ private fun UnknownRoomContent(
RoomPreviewOrganism( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp)) Box(
modifier = Modifier
.size(AvatarSize.RoomPreviewHeader.dp)
.background(
color = ElementTheme.colors.placeholderBackground,
shape = CircleShape
)
) {
Icon(
modifier = Modifier.align(Alignment.Center),
tint = ElementTheme.colors.iconPrimary,
imageVector = CompoundIcons.VisibilityOff(),
contentDescription = null,
)
}
}, },
title = { title = {
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview)) RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
@ -448,7 +508,7 @@ private fun IncompleteContent(
RoomPreviewOrganism( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
}, },
title = { title = {
when (roomIdOrAlias) { when (roomIdOrAlias) {
@ -471,43 +531,32 @@ private fun IncompleteContent(
@Composable @Composable
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints( IconTitleSubtitleMolecule(
modifier = modifier modifier = modifier.padding(horizontal = 8.dp),
.fillMaxHeight() iconStyle = BigIcon.Style.SuccessSolid,
.padding(horizontal = 16.dp), title = stringResource(R.string.screen_join_room_knock_sent_title),
contentAlignment = Alignment.Center, subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
) { )
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 @Composable
private fun DefaultLoadedContent( private fun DefaultLoadedContent(
contentState: ContentState.Loaded, contentState: ContentState.Loaded,
knockMessage: String,
hideAvatarImage: Boolean, hideAvatarImage: Boolean,
onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
RoomPreviewOrganism( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
Avatar( Avatar(
contentState.avatarData(AvatarSize.RoomHeader), contentState.avatarData(AvatarSize.RoomPreviewHeader),
hideImage = hideAvatarImage, hideImage = hideAvatarImage,
avatarType = AvatarType.Room(), avatarType = if (contentState.isSpace) AvatarType.Space() else AvatarType.Room(),
) )
}, },
title = { title = {
if (contentState.name != null) { if (contentState.name != null) {
RoomPreviewTitleAtom( RoomPreviewTitleAtom(title = contentState.name)
title = contentState.name,
)
} else { } else {
RoomPreviewTitleAtom( RoomPreviewTitleAtom(
title = stringResource(id = CommonStrings.common_no_room_name), title = stringResource(id = CommonStrings.common_no_room_name),
@ -516,37 +565,32 @@ private fun DefaultLoadedContent(
} }
}, },
subtitle = { subtitle = {
if (contentState.alias != null) { when {
RoomPreviewSubtitleAtom(contentState.alias.value) contentState.details is LoadedDetails.Space -> {
} SpaceInfoRow(
}, joinRule = contentState.joinRule ?: JoinRule.Public,
description = { numberOfRooms = contentState.details.childrenCount,
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp))
val supportingText = if (knockMessage.isNotEmpty()) {
"${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
} else {
stringResource(R.string.screen_join_room_knock_message_description)
}
TextField(
value = knockMessage,
onValueChange = onKnockMessageUpdate,
maxLines = 3,
minLines = 3,
modifier = Modifier.fillMaxWidth(),
supportingText = supportingText
) )
} }
contentState.alias != null -> {
RoomPreviewSubtitleAtom(contentState.alias.value)
}
} }
}, },
description = {
RoomPreviewDescriptionAtom(
contentState.topic ?: "",
maxLines = if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanJoin) Int.MAX_VALUE else 2
)
},
memberCount = { memberCount = {
if (contentState.showMemberCount) { if (contentState.showMemberCount) {
MembersCountMolecule(memberCount = contentState.numberOfMembers?.toInt() ?: 0) val membersCount = contentState.numberOfMembers?.toInt() ?: 0
if (contentState.isSpace) {
SpaceMembersView(persistentListOf(), membersCount)
} else {
MembersCountMolecule(memberCount = membersCount)
}
} }
} }
) )

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"This room is either invite-only or there might be restrictions to access at space level."</string> <string name="screen_join_room_fail_reason">"This room is either invite-only or there might be restrictions to access at space level."</string>
<string name="screen_join_room_forget_action">"Forget this room"</string> <string name="screen_join_room_forget_action">"Forget this room"</string>
<string name="screen_join_room_invite_required_message">"You need an invite in order to join this room"</string> <string name="screen_join_room_invite_required_message">"You need an invite in order to join this room"</string>
<string name="screen_join_room_invited_by">"Invited by"</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_join_restricted_message">"You may need to be invited or be a member of a space in order to join."</string> <string name="screen_join_room_join_restricted_message">"You may need to be invited or be a member of a space in order to join."</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>

View file

@ -28,13 +28,11 @@ 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.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.api.exception.ClientException import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind import io.element.android.libraries.matrix.api.exception.ErrorKind
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.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
@ -50,8 +48,12 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomPreview import io.element.android.libraries.matrix.test.room.aRoomPreview
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
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.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.libraries.previewutils.room.aSpaceRoom
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.any
import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.assert
@ -90,6 +92,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo() val roomInfo = aRoomInfo()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -107,7 +112,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomInfo.topic) assertThat(contentState.topic).isEqualTo(roomInfo.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.joinedMembersCount) assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.joinedMembersCount)
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect) assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = roomInfo.isDirect))
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
} }
} }
@ -118,6 +123,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -142,7 +150,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest { 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 inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender() val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo( val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED, currentUserMembership = CurrentUserMembership.INVITED,
@ -151,7 +159,21 @@ class JoinRoomPresenterTest {
) )
val inviteData = roomInfo.toInviteData() val inviteData = roomInfo.toInviteData()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ ->
Result.success(
aRoomPreview(
info = aRoomPreviewInfo(
numberOfJoinedMembers = 5,
),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
)
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -169,6 +191,137 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when space is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender()
val spaceHero = aMatrixUser()
val roomInfo = aRoomInfo(
isSpace = true,
currentUserMembership = CurrentUserMembership.INVITED,
joinedMembersCount = 5,
inviter = inviter,
heroes = listOf(spaceHero),
)
val inviteData = roomInfo.toInviteData()
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
aRoomPreview(
info = aRoomPreviewInfo(
numberOfJoinedMembers = 5,
),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
)
},
spaceService = FakeSpaceService(
spaceRoomListResult = {
FakeSpaceRoomList(
initialSpaceFlowValue = aSpaceRoom(
childrenCount = 3,
)
)
},
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient
)
presenter.test {
skipItems(2)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender))
assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5)
// Space details are provided
assertThat(state.contentState.details).isEqualTo(
LoadedDetails.Space(
childrenCount = 3,
heroes = persistentListOf(spaceHero),
)
)
}
}
}
@Test
fun `present - space is invited - no room info`() = runTest {
val spaceHero = aMatrixUser()
val spaceRoom = aSpaceRoom(
childrenCount = 3,
heroes = listOf(spaceHero),
)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(Exception("Error"))
},
spaceService = FakeSpaceService(
spaceRoomListResult = {
FakeSpaceRoomList(
initialSpaceFlowValue = spaceRoom,
)
},
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.ofNullable(null))
}
}
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
// Space details are provided
assertThat((state.contentState as ContentState.Loaded).details).isEqualTo(
LoadedDetails.Space(
childrenCount = 3,
heroes = persistentListOf(spaceHero),
)
)
}
}
}
@Test
fun `present - space is invited - no room info - space room state set`() = runTest {
val spaceRoom = aSpaceRoom(
state = CurrentUserMembership.INVITED,
)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(Exception("Error"))
},
spaceService = FakeSpaceService(
spaceRoomListResult = {
FakeSpaceRoomList(
initialSpaceFlowValue = spaceRoom,
)
},
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.ofNullable(null))
}
}
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient
)
presenter.test {
awaitItem().also { state ->
// Space details are provided
assertThat(state.contentState).isInstanceOf(ContentState.Loading::class.java)
}
}
}
@Test @Test
fun `present - when room is invited read the number of member from the room preview`() = runTest { fun `present - when room is invited read the number of member from the room preview`() = runTest {
val roomInfo = aRoomInfo( val roomInfo = aRoomInfo(
@ -182,10 +335,16 @@ class JoinRoomPresenterTest {
aRoomPreview( aRoomPreview(
info = aRoomPreviewInfo( info = aRoomPreviewInfo(
numberOfJoinedMembers = 10, numberOfJoinedMembers = 10,
) ),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
) )
) )
}, },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -209,7 +368,11 @@ class JoinRoomPresenterTest {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder) anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
} }
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
} }
@ -244,6 +407,9 @@ class JoinRoomPresenterTest {
} }
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
@ -272,6 +438,9 @@ class JoinRoomPresenterTest {
fun `present - when room is joined with error, it is possible to clear the error`() = runTest { fun `present - when room is joined with error, it is possible to clear the error`() = runTest {
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
@ -334,16 +503,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.BANNED, currentUserMembership = CurrentUserMembership.BANNED,
), ),
roomMembershipDetails = { roomMembershipDetails = {
Result.success( Result.success(aRoomMembershipDetails())
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
} }
) )
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -371,6 +538,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public) val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -392,6 +562,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null) val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply { ).apply {
getRoomInfoFlowLambda = { _ -> getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomInfo))
@ -423,7 +596,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomDescription.topic) assertThat(contentState.topic).isEqualTo(roomDescription.topic)
assertThat(contentState.alias).isEqualTo(roomDescription.alias) assertThat(contentState.alias).isEqualTo(roomDescription.alias)
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers) assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
assertThat(contentState.isDm).isFalse() assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = false))
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
} }
} }
@ -497,6 +670,9 @@ class JoinRoomPresenterTest {
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess) val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
@ -542,6 +718,9 @@ class JoinRoomPresenterTest {
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess) val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
@ -586,6 +765,9 @@ class JoinRoomPresenterTest {
val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess) val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess)
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
@ -634,10 +816,16 @@ class JoinRoomPresenterTest {
isHistoryWorldReadable = false, isHistoryWorldReadable = false,
joinRule = JoinRule.Public, joinRule = JoinRule.Public,
currentUserMembership = null, currentUserMembership = null,
) ),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
) )
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -652,10 +840,10 @@ class JoinRoomPresenterTest {
topic = "Room topic", topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"), alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2, numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl", roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin,
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
) )
) )
} }
@ -681,16 +869,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.INVITED, currentUserMembership = CurrentUserMembership.INVITED,
), ),
roomMembershipDetails = { roomMembershipDetails = {
Result.success( Result.success(aRoomMembershipDetails())
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
} }
) )
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -705,8 +891,6 @@ class JoinRoomPresenterTest {
topic = "Room topic", topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"), alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2, numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl", roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
inviteData = InviteData( inviteData = InviteData(
@ -724,7 +908,9 @@ class JoinRoomPresenterTest {
), ),
membershipChangeReason = null, membershipChangeReason = null,
), ),
) ),
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
) )
) )
} }
@ -751,15 +937,15 @@ class JoinRoomPresenterTest {
), ),
roomMembershipDetails = { roomMembershipDetails = {
Result.success( Result.success(
RoomMembershipDetails( aRoomMembershipDetails(),
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
) )
} }
) )
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -774,8 +960,6 @@ class JoinRoomPresenterTest {
topic = "Room topic", topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"), alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2, numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl", roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
banSender = InviteSender( banSender = InviteSender(
@ -789,7 +973,9 @@ class JoinRoomPresenterTest {
membershipChangeReason = null, membershipChangeReason = null,
), ),
reason = null, reason = null,
) ),
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
) )
) )
} }
@ -815,16 +1001,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.KNOCKED, currentUserMembership = CurrentUserMembership.KNOCKED,
), ),
roomMembershipDetails = { roomMembershipDetails = {
Result.success( Result.success(aRoomMembershipDetails())
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
} }
) )
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -839,10 +1023,10 @@ class JoinRoomPresenterTest {
topic = "Room topic", topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"), alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2, numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl", roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked,
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
) )
) )
} }
@ -854,9 +1038,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.success( Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Private)) aRoomPreview(
info = aRoomPreviewInfo(joinRule = JoinRule.Private),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -874,9 +1066,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.success( Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom"))) aRoomPreview(
info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom")),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -894,9 +1094,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.success( Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Invite)) aRoomPreview(
info = aRoomPreviewInfo(joinRule = JoinRule.Invite),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -914,9 +1122,19 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.success( Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(persistentListOf()))) aRoomPreview(
info = aRoomPreviewInfo(
joinRule = JoinRule.KnockRestricted(persistentListOf())
),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
}
)
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -934,9 +1152,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.success( Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf()))) aRoomPreview(
info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf())),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
) )
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -949,32 +1175,15 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when room is not known RoomPreview is loaded as Space`() = runTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(isSpace = true))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsSpace("AppName"))
}
}
}
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest { fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -1004,7 +1213,10 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -1029,7 +1241,10 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> getNotJoinedRoomResult = { _, _ ->
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null)) Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null))
} },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
matrixClient = client matrixClient = client
@ -1068,7 +1283,11 @@ internal fun createJoinRoomPresenter(
roomDescription: Optional<RoomDescription> = Optional.empty(), roomDescription: Optional<RoomDescription> = Optional.empty(),
serverNames: List<String> = emptyList(), serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite, trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
),
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ -> joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit) Result.success(Unit)
}, },
@ -1095,3 +1314,8 @@ internal fun createJoinRoomPresenter(
seenInvitesStore = seenInvitesStore, seenInvitesStore = seenInvitesStore,
) )
} }
private fun aRoomMembershipDetails() = RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)

View file

@ -14,7 +14,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.test.anInviteData import io.element.android.features.invite.test.anInviteData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
@ -218,21 +217,6 @@ class JoinRoomViewTest {
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
} }
@Test
fun `clicking on ok when a space is displayed invokes the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce {
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(roomType = RoomType.Space),
eventSink = eventsRecorder,
),
onBackClick = it
)
rule.clickOn(CommonStrings.action_ok)
}
}
@Test @Test
fun `clicking on ok when user is unauthorized the expected callback`() { fun `clicking on ok when user is unauthorized the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)

View file

@ -117,7 +117,7 @@ private fun RoomAliasResolverContent(
RoomPreviewOrganism( RoomPreviewOrganism(
modifier = modifier, modifier = modifier,
avatar = { avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
}, },
title = { title = {
RoomPreviewSubtitleAtom(roomAlias.value) RoomPreviewSubtitleAtom(roomAlias.value)

View file

@ -396,10 +396,10 @@ private fun RoomHeaderSection(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Avatar( Avatar(
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader), avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomDetailsHeader),
avatarType = AvatarType.Room( avatarType = AvatarType.Room(
heroes = heroes.map { user -> heroes = heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomHeader) user.getAvatarData(size = AvatarSize.RoomDetailsHeader)
}.toPersistentList(), }.toPersistentList(),
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
), ),

View file

@ -29,6 +29,8 @@ import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@Inject @Inject
class SpacePresenter( class SpacePresenter(
@ -64,7 +66,7 @@ class SpacePresenter(
} }
}.collectAsState() }.collectAsState()
val currentSpace by remember { spaceRoomList.currentSpaceFlow() }.collectAsState(null) val currentSpace by remember { spaceRoomList.currentSpaceFlow() }.collectAsState(Optional.empty())
fun handleEvents(event: SpaceEvents) { fun handleEvents(event: SpaceEvents) {
when (event) { when (event) {
@ -72,7 +74,7 @@ class SpacePresenter(
} }
} }
return SpaceState( return SpaceState(
currentSpace = currentSpace, currentSpace = currentSpace.getOrNull(),
children = children.toPersistentList(), children = children.toPersistentList(),
seenSpaceInvites = seenSpaceInvites, seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,

View file

@ -18,7 +18,15 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
override val values: Sequence<SpaceState> override val values: Sequence<SpaceState>
get() = sequenceOf( get() = sequenceOf(
aSpaceState(), aSpaceState(),
aSpaceState(hasMoreToLoad = true), aSpaceState(
parentSpace = aSpaceRoom(
name = null,
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
),
hasMoreToLoad = true,
),
aSpaceState( aSpaceState(
hasMoreToLoad = true, hasMoreToLoad = true,
children = aListOfSpaceRooms(), children = aListOfSpaceRooms(),

View file

@ -183,7 +183,7 @@ private fun SpaceAvatarAndNameRow(
.semantics { .semantics {
heading() heading()
}, },
text = name ?: stringResource(CommonStrings.common_no_room_name), text = name ?: stringResource(CommonStrings.common_no_space_name),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { name == null }, fontStyle = FontStyle.Italic.takeIf { name == null },
maxLines = 1, maxLines = 1,

View file

@ -15,14 +15,18 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun RoomPreviewDescriptionAtom(description: String, modifier: Modifier = Modifier) { fun RoomPreviewDescriptionAtom(
description: String,
modifier: Modifier = Modifier,
maxLines: Int = Int.MAX_VALUE,
) {
Text( Text(
modifier = modifier, modifier = modifier,
text = description, text = description,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodyMdRegular,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textPrimary,
maxLines = 3, maxLines = maxLines,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }

View file

@ -18,7 +18,7 @@ fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
Text( Text(
modifier = modifier, modifier = modifier,
text = subtitle, text = subtitle,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
) )

View file

@ -23,7 +23,7 @@ fun RoomPreviewTitleAtom(
Text( Text(
modifier = modifier, modifier = modifier,
text = title, text = title,
style = ElementTheme.typography.fontHeadingMdBold, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontStyle = fontStyle, fontStyle = fontStyle,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,

View file

@ -34,14 +34,13 @@ fun RoomPreviewOrganism(
title() title()
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
subtitle() subtitle()
Spacer(modifier = Modifier.height(8.dp))
if (memberCount != null) { if (memberCount != null) {
Spacer(modifier = Modifier.height(8.dp))
memberCount() memberCount()
} }
Spacer(modifier = Modifier.height(8.dp))
if (description != null) { if (description != null) {
Spacer(modifier = Modifier.height(16.dp))
description() description()
} }
Spacer(modifier = Modifier.height(24.dp))
} }
} }

View file

@ -14,7 +14,7 @@ enum class AvatarSize(val dp: Dp) {
CurrentUserTopBar(32.dp), CurrentUserTopBar(32.dp),
IncomingCall(140.dp), IncomingCall(140.dp),
RoomHeader(96.dp), RoomDetailsHeader(96.dp),
RoomListItem(52.dp), RoomListItem(52.dp),
SpaceListItem(52.dp), SpaceListItem(52.dp),
@ -69,5 +69,7 @@ enum class AvatarSize(val dp: Dp) {
OrganizationHeader(64.dp), OrganizationHeader(64.dp),
SpaceHeader(64.dp), SpaceHeader(64.dp),
RoomPreviewHeader(64.dp),
RoomPreviewInviter(56.dp),
SpaceMember(24.dp), SpaceMember(24.dp),
} }

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.api.spaces
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
interface SpaceRoomList { interface SpaceRoomList {
sealed interface PaginationStatus { sealed interface PaginationStatus {
@ -16,7 +17,7 @@ interface SpaceRoomList {
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
} }
fun currentSpaceFlow(): Flow<SpaceRoom?> fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>>
val spaceRoomsFlow: Flow<List<SpaceRoom>> val spaceRoomsFlow: Flow<List<SpaceRoom>>
val paginationStatusFlow: StateFlow<PaginationStatus> val paginationStatusFlow: StateFlow<PaginationStatus>

View file

@ -13,13 +13,14 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList( class RustSpaceRoomList(
@ -31,7 +32,7 @@ class RustSpaceRoomList(
) : SpaceRoomList { ) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>() private val inner = CompletableDeferred<InnerSpaceRoomList>()
override fun currentSpaceFlow(): Flow<SpaceRoom?> { override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> {
return spaceRoomCache.getSpaceRoomFlow(roomId) return spaceRoomCache.getSpaceRoomFlow(roomId)
} }

View file

@ -10,9 +10,10 @@ package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.coroutine.mapState
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.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import java.util.Optional
/** /**
* An in memory cache of space rooms. * An in memory cache of space rooms.
@ -20,8 +21,8 @@ import kotlinx.coroutines.flow.update
*/ */
class SpaceRoomCache { class SpaceRoomCache {
private val inMemoryCache = MutableStateFlow<Map<RoomId, SpaceRoom?>>(emptyMap()) private val inMemoryCache = MutableStateFlow<Map<RoomId, SpaceRoom?>>(emptyMap())
fun getSpaceRoomFlow(roomId: RoomId): Flow<SpaceRoom?> { fun getSpaceRoomFlow(roomId: RoomId): StateFlow<Optional<SpaceRoom>> {
return inMemoryCache.mapState { it[roomId] } return inMemoryCache.mapState { Optional.ofNullable(it[roomId]) }
} }
fun update(spaceRooms: List<SpaceRoom>) { fun update(spaceRooms: List<SpaceRoom>) {

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import kotlin.jvm.optionals.getOrNull
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomListTest { class RustSpaceRoomListTest {
@ -93,10 +94,10 @@ class RustSpaceRoomListTest {
spaceRoomCache = spaceRoomCache, spaceRoomCache = spaceRoomCache,
) )
sut.currentSpaceFlow().test { sut.currentSpaceFlow().test {
assertThat(awaitItem()).isNull() assertThat(awaitItem().getOrNull()).isNull()
val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID) val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID)
spaceRoomCache.update(listOf(spaceRoom)) spaceRoomCache.update(listOf(spaceRoom))
assertThat(awaitItem()).isEqualTo(spaceRoom) assertThat(awaitItem().getOrNull()).isEqualTo(spaceRoom)
} }
} }

View file

@ -21,7 +21,7 @@ class SpaceRoomCacheTest {
fun `getSpaceRoomFlow emits items`() = runTest { fun `getSpaceRoomFlow emits items`() = runTest {
val sut = SpaceRoomCache() val sut = SpaceRoomCache()
sut.getSpaceRoomFlow(A_ROOM_ID).test { sut.getSpaceRoomFlow(A_ROOM_ID).test {
assertThat(awaitItem()).isNull() assertThat(awaitItem().isEmpty).isTrue()
val room = aSpaceRoom( val room = aSpaceRoom(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
roomType = RoomType.Room, roomType = RoomType.Room,
@ -34,7 +34,7 @@ class SpaceRoomCacheTest {
roomType = RoomType.Space, roomType = RoomType.Space,
) )
sut.update(listOf(space)) sut.update(listOf(space))
assertThat(awaitItem()).isEqualTo(space) assertThat(awaitItem().get()).isEqualTo(space)
val spaceOther = aSpaceRoom( val spaceOther = aSpaceRoom(
roomId = A_ROOM_ID_2, roomId = A_ROOM_ID_2,
roomType = RoomType.Space, roomType = RoomType.Space,

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import java.util.Optional
class FakeSpaceRoomList( class FakeSpaceRoomList(
initialSpaceFlowValue: SpaceRoom? = null, initialSpaceFlowValue: SpaceRoom? = null,
@ -22,11 +23,11 @@ class FakeSpaceRoomList(
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading, initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
private val paginateResult: () -> Result<Unit> = { lambdaError() }, private val paginateResult: () -> Result<Unit> = { lambdaError() },
) : SpaceRoomList { ) : SpaceRoomList {
private val currentSpaceMutableStateFlow: MutableStateFlow<SpaceRoom?> = MutableStateFlow(initialSpaceFlowValue) private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
override fun currentSpaceFlow(): Flow<SpaceRoom?> = currentSpaceMutableStateFlow.asStateFlow() override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
fun emitCurrentSpace(value: SpaceRoom?) { fun emitCurrentSpace(value: SpaceRoom?) {
currentSpaceMutableStateFlow.value = value currentSpaceMutableStateFlow.value = Optional.ofNullable(value)
} }
private val _spaceRoomsFlow: MutableStateFlow<List<SpaceRoom>> = MutableStateFlow(initialSpaceRoomsValue) private val _spaceRoomsFlow: MutableStateFlow<List<SpaceRoom>> = MutableStateFlow(initialSpaceRoomsValue)

View file

@ -7,19 +7,16 @@
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.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
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.text.style.TextAlign import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -29,6 +26,7 @@ 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.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -47,47 +45,45 @@ fun SpaceHeaderView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topicMaxLines: Int = Int.MAX_VALUE, topicMaxLines: Int = Int.MAX_VALUE,
) { ) {
Column( RoomPreviewOrganism(
modifier = modifier modifier = modifier.padding(24.dp),
.fillMaxWidth() avatar = {
.padding(top = 32.dp, bottom = 24.dp, start = 16.dp, end = 16.dp), Avatar(
horizontalAlignment = Alignment.CenterHorizontally, avatarData = avatarData,
verticalArrangement = Arrangement.spacedBy(16.dp) avatarType = AvatarType.Space(),
) {
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space(false),
)
name?.let {
Text(
text = name,
style = ElementTheme.typography.fontHeadingLgBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
) )
} },
if (joinRule != null) { title = {
SpaceInfoRow( if (name != null) {
joinRule = joinRule, RoomPreviewTitleAtom(title = name)
numberOfRooms = numberOfRooms, } else {
RoomPreviewTitleAtom(
title = stringResource(id = CommonStrings.common_no_space_name),
fontStyle = FontStyle.Italic
)
}
},
subtitle = {
if (joinRule != null) {
SpaceInfoRow(
joinRule = joinRule,
numberOfRooms = numberOfRooms,
)
}
},
description = if (topic.isNullOrBlank()) {
null
} else {
{ RoomPreviewDescriptionAtom(description = topic, maxLines = topicMaxLines) }
},
memberCount = {
SpaceMembersView(
heroes = heroes,
numberOfMembers = numberOfMembers,
modifier = Modifier.padding(horizontal = 32.dp),
) )
} },
SpaceMembersView( )
heroes = heroes,
numberOfMembers = numberOfMembers,
modifier = Modifier.padding(horizontal = 32.dp),
)
topic?.let {
Text(
text = topic,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
maxLines = topicMaxLines,
overflow = TextOverflow.Ellipsis,
)
}
}
} }
@PreviewsDayNight @PreviewsDayNight

View file

@ -69,6 +69,7 @@ fun SpaceRoomItemView(
onLongClick = onLongClick, onLongClick = onLongClick,
) { ) {
NameAndIndicatorRow( NameAndIndicatorRow(
isSpace = spaceRoom.isSpace,
name = spaceRoom.name, name = spaceRoom.name,
showIndicator = showUnreadIndicator showIndicator = showUnreadIndicator
) )
@ -130,6 +131,7 @@ private fun SubtitleRow(
@Composable @Composable
private fun NameAndIndicatorRow( private fun NameAndIndicatorRow(
isSpace: Boolean,
name: String?, name: String?,
showIndicator: Boolean, showIndicator: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -142,7 +144,7 @@ private fun NameAndIndicatorRow(
Text( Text(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name), text = name ?: stringResource(id = if (isSpace) CommonStrings.common_no_space_name else CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null }, fontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
maxLines = 1, maxLines = 1,

View file

@ -105,7 +105,7 @@ class DefaultNotificationConversationService(
targetSize = defaultShortcutIconSize.toLong() targetSize = defaultShortcutIconSize.toLong()
)?.let(IconCompat::createWithBitmap) )?.let(IconCompat::createWithBitmap)
?: InitialsAvatarBitmapGenerator(useDarkTheme = useDarkTheme) ?: InitialsAvatarBitmapGenerator(useDarkTheme = useDarkTheme)
.generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomHeader)) .generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomDetailsHeader))
?.let(IconCompat::createWithAdaptiveBitmap) ?.let(IconCompat::createWithAdaptiveBitmap)
val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId)) val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId))

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4053802814238aa74a5c606a4b37e0afcb923282c3e7cece737b0ec4f6828351 oid sha256:fbdebc1c9361339dd0db1051e59162ed62fa2787c46457848da5ee6a30474588
size 106332 size 106570

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:04ec1bd0bd667d5319843f480f2b7ca4b1b4364db4fbe22d85c10d291891642a oid sha256:7a6d3be47ab7d9234657d4d088389c8aadb4d9e073d8c91f4f81dded1b6662a6
size 42696 size 42160

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d02259f3163c49b5c39d38d7a84af50583f6a0ce2a66e8ad013ac18278de65c0 oid sha256:7bb7d1c08f5b2551117aa1aff8a3afbabd0f4100e0fae11cf415bd8e851c0307
size 103621 size 103835

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0d00883e68471491d8edf06eee394e4310707dbd03782f57bb7f783822790b61 oid sha256:b3f70a15def31e67e84afe7f9545d280bdbca01c6dd63864662f19976df3bf33
size 41372 size 40960

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7423c3f11ec8661d7fb3f57d7d78459e784221644229515080e0b0dc6ce59b6e oid sha256:8c88f9a3ecbcf7db13846a6074c75fca2ce6bdbe0edd850bfc139e01a4fdc0c8
size 10129 size 9956

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:08ed98b594a6730d52ab344aff6bea8a38216a609549be267f9046dd69858903 oid sha256:7fdd74d6df903a0cedfbfc2f30430b5802e4f0bc7b2711e20ae47d297066b056
size 38478 size 40297

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bf0a1c9fea2a067d6156985cce2fd2a2551f6111e7739206f7bd70a563a97fcf oid sha256:ed26457abf2a6d89d61e911944b4e9f62de248c1939f997d0f8764e4c1ae08e9
size 45333 size 43088

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:24d9f9b2a63773f5bc49b7f8f8b85d1dd13e8e62175b61bca534b04626d19b0f oid sha256:46b410cba77b24d8657031f5fde29d4f25a639300862641ce0f8ad0de8ff99cb
size 46336 size 44076

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ca5217bba4fe65af4ac2a0aaaefa941a5a5ed0735dbd9bd69a648a157ca0c51b oid sha256:950e90b7c2c67fc95c9d6698dc7436885ee80dcf9da7d8467b0027c04c03c8f5
size 29343 size 29243

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:41a3c9cbec371b6aacabcb954315a65939b579c5963db63fc11789326ad2fc1b oid sha256:37ac3a032e758c4abf9ac8a87e887c7e015f2d35160d59a7e263963300bd0941
size 29068 size 31558

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0249fb4df9e88abaa28cc3a10e2cadc23b3b84581d74893d525076683f0a5588 oid sha256:44b9c4608696c6a690e7f00f661b3cb55d03826b11569b6e8ce0a48f9754dd39
size 36386 size 38547

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e1c6c00bca10e0bf725ab5debe3cd9dbb14be89f29872ca70df8f3886d1cec19 oid sha256:f92065e3de67f39ce27ae3e321f59fa2d18b5927752dc760450ec275935b1959
size 40928 size 43405

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:96ecb81ebfc7545eca7931538a443b7190b7903d8ebf5bf70bb95eb3b42e6a38 oid sha256:919101bfd215af97a098bdfae4980445bd1d0560f96fe9ce13a870e2d8817fff
size 30092 size 34247

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:09e79f148754898125af4972bbfecfb1e3fc8f2de918ba9dd12d9766ef09e4f1 oid sha256:2f9707b754041c871c408e12613faa186fa4f4f760ca793caf20c62bae07246b
size 31417 size 32021

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6c87682ca77c0ee163e437dae9f177e52937c6099e1d3583f2138735fd5ecc8f oid sha256:5ad9105ca2659f51b16b41f9317812590f38adb8431a64a139bb47f54fdf9eff
size 26669 size 29754

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8319fdce8ae47fd6dc7aea8f6c98e9e4de7322f0b9adc4262695ff3990a63052 oid sha256:04180611d2a959d07bbbd051c4386f6428d841c5ea74e197a80149370db76e04
size 39520 size 41830

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0ae6db46d01c157fcb8bb436b43e38da13723c498a222e68f792ac170ea2e2ac oid sha256:7c4a828b9aa4e4963e3cb39e6f65f82ff5e595fdccb4dc5b48db532eeac1fb00
size 27841 size 31388

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:484657673c4ae676b330320ba213485a16c6f31ea025b16b4f2c3a53053eaf7a oid sha256:bdc24799de625f9826fcf87a624e76b530e7f0e44c294d49f842ed2de7a9c085
size 29860 size 32981

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a239f4f8003693097fee6e9dddc31ed1ce7a53f7c2b9dab9db55059ca51df64f oid sha256:247a1cec9f0729ef1b23dc5ad730521bb7bfc117b4e9cc901ec922ee946f942e
size 37645 size 39401

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e10c07c74c54920daac28945f2fcafb8956edcd6df5aaac1d7dc757bd119ee3d oid sha256:bcc00accf1fa84c48831378faa27558d5ba24961dd3ba0c5f0587329cdbaa9bf
size 26750 size 29110

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:657dfa3cd260803b3386ae6c3d44f6cc4277c2ef2572680145c419dfd9690e7a oid sha256:da07e3041ec5814feb0d14e8478d0c817d1496155db2daa612048b94530df20b
size 33276 size 38200

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:58776319728d3367a347d62cf120803582b05c564f9cd76b2c8f406e70673821 oid sha256:c91724171606aa0c36987908af0abce5c05b3311469a08e532532307d1224c60
size 9949 size 9842

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4ae699d09bc1287d12d9974a6801f6b1648f0d6bd63eb9ac10302be37cae5383 oid sha256:f5a6da57cc5659ba3f3705cba41fd23c7628ba2477fcfc427b8e26bde37ed812
size 38181 size 39960

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:511985d79fe13467eec62772dbe6cc69dec1f1c9f9a3e66428fe9b22ce279d1f oid sha256:5b9cf46f0039e0f91e8372cd5f9a15b7b54b75c80e0a38f9ebc2548315d57c9f
size 44937 size 42657

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a084ed7657a2f263283a477e700df9fb6219e8268380f764c2e5e4055465044d oid sha256:5a4f65342c0e84179aceadc5d75fc763ed45c23f4db44bc16441c50990b193ed
size 45898 size 43582

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:51680be3eede0c4a53683b298415a07f3e9ea688ac39fe29373e1e80b0f4540b oid sha256:e2ef8069be91c6e20263da790c0b8b777515f46bdc4769532b321ae86c9152b8
size 28778 size 28701

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7172438656b08d5c244fc570fd25dc074562b7599f15eda8241ddb7c9b275fcd oid sha256:5f47c18fda5c4839e1098955f57c4a3e5b9856203c6a84c31903aa5e0b2da46b
size 28765 size 30971

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9aaf1721d3b8c3a000402eb605b7ba742560fae7297514a15f74fd97c2f838a1 oid sha256:112286f6a18865db2333a68cb804e930eb873bb04596bada2f825e2223b4f537
size 35406 size 37362

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b7b113b174727ce08ed2b23f320d49b556e869249302c0b84c1b5916ae8864ac oid sha256:ff86f906357d0bf611bfc9cd235e66e0247dd7bb66903cb0e5730224d5dc897c
size 40563 size 43008

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:444f717f986b154297294df6ba731befc3e2049f89a3294e26fd6d90f0831996 oid sha256:cc849f3158de5cd3cdc64a57e613f3b3a5cd70794e7984d7cfa1f8f425184592
size 29353 size 33440

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f9bd6d13f3c4f7a715e9e8698db252bc33823fe180e6d279eabcc9de618dcaee oid sha256:ed211c9bc258185de6ea4cc414fd214f256ad1649c26a8a28f25fd1b29c34783
size 31182 size 31630

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:acdd2cd73f2a37190bbbeb829af68a28dae8d73f5462396db4c6238ee8319351 oid sha256:6a88ffdfd4259a2e85b3a8375591eab3b0f12e927eb83f9c6820995acf44d79e
size 26733 size 29607

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:731f81d6ca0d4677dc56b58316c7396a7ad1ee08c785aecbb9eea4a0dea6b77a oid sha256:7fbe20a3d4e0c3706ce81c1f52c65ff4f749b0afd6bdd3efb4100ee1019afc51
size 38428 size 40580

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9d3a672f30777ff5b487bca938e612de8e34de94dc5ac189659ced73ed49bffa oid sha256:cff1f0a8a34cfd4eb55487540a02c70e629d7f6123355c95380498817053eb78
size 26309 size 29575

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e0d60be4104d5f124e49e8a7434204bd0593b46fca838e787aefa40769ac1778 oid sha256:71e56b906decbea31fcf5581dc92dcbb6be79f6a6962595786e9533cc62254ad
size 29202 size 32045

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7129f65572d7b4f6b302fa803f878b269f2ee7b11a684c3e1ec051f9192fc8fa oid sha256:18e36242d664e1f2179bb3aef3f77a5bbbf5cd44aece2d2c9a3d7d70d2356b92
size 36927 size 38769

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:83b636552b1a28db8bb81e870486acd861ee131f51f8a8980fb3f1471e55ba3f oid sha256:37270eed539ed52a113375dbfa516f443838530ef7995bb6372764afbc25e75d
size 24709 size 26787

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8c862dd75be71248752d53cc605860c55fe4eecce75eba545ab4471a13585296 oid sha256:0b73e8a2429db6324f471908f46ccd2f6723d59caf4acc738c07d6e402a19285
size 32951 size 37752

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1b0c1728d598c5bf307a2b9a193c2746831c83de892fc53ccb7da1bb183b8297 oid sha256:6d4663e9c2c439b05d6d1abe7ae7098539f5263461e9051b0b97305a31b5aa33
size 8475 size 8395

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e10c07c74c54920daac28945f2fcafb8956edcd6df5aaac1d7dc757bd119ee3d oid sha256:5365eb3dd7e10fde8591e6d5ee6dfadef88a9fa25339309ce08445f89af88789
size 26750 size 29187

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:78826ac37783b67bab2ef7c93a5ad1fcc1fd167926ed3e6d8c92eb826c8b603f oid sha256:5c66ca10f770fa2f29716f7c737be0acef03e364e9a27a85b81e4ef63e768b6f
size 23462 size 24012

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a3f39d62131784e82104ee0033e4da9e2de45eacc1527b878fdaa9dd64d45aa2 oid sha256:c5b7cb8b7ef8b58a5fa77b7c1c6a911798940337ea9646383ee0cb082dc68f7e
size 8324 size 8263

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:83b636552b1a28db8bb81e870486acd861ee131f51f8a8980fb3f1471e55ba3f oid sha256:c02fa02a7f5787d375ffa6e9bb69cde0b23ca87512ee8120d318becd6de13bef
size 24709 size 26763

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7114651af7b2d0ae7caf7df0bb81bb117fc829bd9795a57e173729673c23d231 oid sha256:5852e500de1f27083f033726a89d2dda594497d2af7fbe843091cdbd06ddd975
size 21082 size 21628

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7e69f830b7f15de531fc46efc02964fb641a4e3754b5108fb8020e97bbfb4b56 oid sha256:4c53e7edfe8b9ea00bb97f9ab6e8289081b5c36aad54a58d4f535fc533338797
size 15698 size 15733

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b5fba00f44ab63c07978ec0143f2f1fefc8fd1d78aed22a367e831c105b806c7 oid sha256:0075dca9fd927dc7df4e987fe5cca8d2a2ca5b268bc46916dcd49724264a3859
size 17306 size 19834

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6c7b387db1b0637745dfc93559b9afa3c637e5a492bf19f6f1d0dbe802ed73bb oid sha256:9d784e5d4d4af937a33825f7098d0849fa044d96e4cf21c7244f51432d3f32c7
size 47156 size 46725

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9892ec0c06bfa6e0b3a95fd35efa97e44f2e1f6264a324d6fdd994847f1830d1 oid sha256:1a1c851207069720dd83cf32da6da80d25a4a4c40cba9884548f9dea09ca6654
size 45897 size 45383

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:91ad7e392e21afe93606956e8e7eab7f0b075574e782ff15e873a71fc3ab16ab oid sha256:ea671a40116dfe7a0e4ac97b58cbbc48b3f70fe08b5613c4b21ec13bc6850c3f
size 15637 size 15589

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9d50be9f20ed0e75e337d790edb329996abf9e626591882088817301338fadb6 oid sha256:8b69241047e1d5787620331adc33f2b78084f47cf02c531a4e41011816590a09
size 17213 size 19523

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d76f7b10d8c88f39ab36581be35dd2a5ca5e7c3a92c69b96d4f3ec87f2e93468 oid sha256:ae27a6a05d85587b066203ec8828db0ec1cec2171e1f3a63c6b0c26e0b71555e
size 46564 size 46151

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:057ceb522ff354d0d7e8b73d59eb53c559f9c22a2727d24744dc6a34b9daaa55 oid sha256:067d6b499ce3ec737792a773c2959aea44829cb27a8fecbe1925ec884bc7f53f
size 45034 size 44650

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ce1aba18ea8a6b45d9c40de9af961952e261c7c6200ab13cdfa67906d995208b oid sha256:8da97746633081690a09a79e3f9974257bbe0f292ad5b8ee2eed454c1273c01e
size 14888 size 19747

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7 oid sha256:1aaa84f80b3691ed83308966e53deb8dbc97c7943d2f68a8036a6ead603ea627
size 14337 size 18270

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150 oid sha256:ddaae721cac21a188dc3862e3e14e80eeb13c92826bccfb8364ecbd72948d9bd
size 16275 size 23653

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3f52fa29e031ab362baed77ac18228960d67eb5d6bdf909b6c1fb3777f5eacc5 oid sha256:8cb143e42d0bc07652d74c74ba641a5332ee9bebb071fa5efba59ecacad04be5
size 19629 size 21857

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4b6a0bdd2239b473f541e95588f2531a2761d0d9872cda61cdd87e2e195b14cf oid sha256:5eecc64fbbf67e53578ba6c974905c16d000a5dc8b2e0fa99c41e46dea36424b
size 17425 size 19651

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3898d22d9de94323eb04d5385af9a1084434c521f8b51f5af74ef1159dd2d1cc oid sha256:c425ffed237de0e71451baf51f674051c3ab6481ecceeff15c7f2f73193fd3ab
size 25138 size 26866

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ebb657ce2227ccefcf9055c8c90342d6581ac0d916cd9f7e6a4990fa2bdc4177 oid sha256:21acaf9ea606378f5f1e83795e41bec9edc2da05143efba04b70f67cce2c80b0
size 61352 size 61626

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:97c8ea5d2d83222ee16c0a27c783e04ec12dbd26aff427c28a21c9c581ad057d oid sha256:e20d4dc80e8f983516cde0361333f941818cade431fb38ae318cfa9a589ab0d8
size 60558 size 60846