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 6cd0af9235
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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.isDm
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.parcelize.Parcelize
@Parcelize
@ -36,3 +37,11 @@ fun RoomInfo.toInviteData(): InviteData {
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.libraries.matrix.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.Inject
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.acceptdecline.AcceptDeclineInviteEvents
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.room.CurrentUserMembership
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.isDm
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.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.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@Inject
class JoinRoomPresenter(
@ -80,6 +85,8 @@ class JoinRoomPresenter(
): JoinRoomPresenter
}
private val spaceList = matrixClient.spaceService.spaceRoomList(roomId)
@Composable
override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope()
@ -87,6 +94,9 @@ class JoinRoomPresenter(
val roomInfo by remember {
matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty())
val spaceRoom by remember {
spaceList.currentSpaceFlow()
}.collectAsState()
val joinAction: 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) }
@ -96,55 +106,41 @@ class JoinRoomPresenter(
val hideInviteAvatars by matrixClient.rememberHideInvitesAvatar()
val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() }
val contentState by produceState<ContentState>(
initialValue = ContentState.Loading,
key1 = roomInfo,
key2 = retryCount,
key3 = isDismissingContent,
) {
var contentState by remember {
mutableStateOf<ContentState>(ContentState.Loading)
}
LaunchedEffect(roomInfo, retryCount, isDismissingContent, spaceRoom) {
when {
isDismissingContent -> value = ContentState.Dismissing
isDismissingContent -> contentState = ContentState.Dismissing
roomInfo.isPresent -> {
val notJoinedRoom = matrixClient.getRoomPreview(roomIdOrAlias, serverNames).getOrNull()
val (sender, reason) = when (roomInfo.get().currentUserMembership) {
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 membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull()
val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers
value = roomInfo.get().toContentState(
membershipSender = sender,
contentState = roomInfo.get().toContentState(
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 -> {
value = roomDescription.get().toContentState()
contentState = roomDescription.get().toContentState()
}
else -> {
value = ContentState.Loading
contentState = ContentState.Loading
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
value = result.fold(
contentState = result.fold(
onSuccess = { preview ->
val membershipInfo = when (preview.previewInfo.membership) {
CurrentUserMembership.INVITED,
CurrentUserMembership.BANNED,
CurrentUserMembership.KNOCKED -> {
preview.membershipDetails().getOrNull()
}
else -> null
}
preview.previewInfo.toContentState(
senderMember = membershipInfo?.senderMember,
reason = membershipInfo?.currentUserMember?.membershipChangeReason,
)
val membershipDetails = preview.membershipDetails().getOrNull()
preview.previewInfo.toContentState(membershipDetails)
},
onFailure = { throwable ->
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(
roomId = roomId,
name = name,
topic = topic,
alias = canonicalAlias,
numberOfMembers = numberOfJoinedMembers,
isDm = false,
roomType = roomType,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (membership) {
CurrentUserMembership.INVITED -> {
JoinAuthorisationStatus.IsInvited(
inviteData = toInviteData(),
inviteSender = senderMember?.toInviteSender()
)
}
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
else -> joinRule.toJoinAuthorisationStatus()
joinAuthorisationStatus = computeJoinAuthorisationStatus(
membership,
membershipDetails,
joinRule,
{ toInviteData() }
),
joinRule = joinRule,
details = when (roomType) {
is RoomType.Other,
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
internal fun RoomDescription.toContentState(): ContentState {
return ContentState.Loaded(
@ -284,22 +306,29 @@ internal fun RoomDescription.toContentState(): ContentState {
topic = topic,
alias = alias,
numberOfMembers = numberOfMembers,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (joinRule) {
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin
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
internal fun RoomInfo.toContentState(
membershipSender: RoomMember?,
joinedMembersCountOverride: Long?,
reason: String?,
membershipDetails: RoomMembershipDetails?,
childrenCount: Int?,
): ContentState {
return ContentState.Loaded(
roomId = id,
@ -307,24 +336,49 @@ internal fun RoomInfo.toContentState(
topic = topic,
alias = canonicalAlias,
numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount,
isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (currentUserMembership) {
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
inviteData = toInviteData(),
inviteSender = membershipSender?.toInviteSender(),
joinAuthorisationStatus = computeJoinAuthorisationStatus(
membership = currentUserMembership,
membershipDetails = membershipDetails,
joinRule = joinRule,
inviteData = { toInviteData() }
),
joinRule = joinRule,
details = if (isSpace) {
LoadedDetails.Space(
childrenCount = childrenCount ?: 0,
heroes = heroes,
)
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
banSender = membershipSender?.toInviteSender(),
reason = reason,
} else {
LoadedDetails.Room(
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 {
return when (this) {
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.RoomId
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.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@ -41,9 +43,6 @@ data class JoinRoomState(
val joinAuthorisationStatus = when (contentState) {
is ContentState.Loaded -> {
when {
contentState.roomType == RoomType.Space -> {
JoinAuthorisationStatus.IsSpace(applicationName)
}
isJoinActionUnauthorized -> {
JoinAuthorisationStatus.Unauthorized
}
@ -77,12 +76,13 @@ sealed interface ContentState {
val topic: String?,
val alias: RoomAlias?,
val numberOfMembers: Long?,
val isDm: Boolean,
val roomType: RoomType,
val roomAvatarUrl: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus,
val joinRule: JoinRule?,
val details: LoadedDetails,
) : ContentState {
val showMemberCount = numberOfMembers != null
val isSpace = details is LoadedDetails.Space
fun avatarData(size: AvatarSize): 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 {
data object None : JoinAuthorisationStatus
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus
data class IsBanned(val banSender: InviteSender?, val reason: String?) : 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.toRoomIdOrAlias
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.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toPersistentList
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState>
@ -77,13 +79,17 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
name = "A space",
alias = null,
topic = "This is the topic of a space",
roomType = RoomType.Space,
details = aLoadedDetailsSpace(
childrenCount = 42,
),
)
),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A DM",
isDm = true,
details = aLoadedDetailsRoom(
isDm = true,
),
)
),
aJoinRoomState(
@ -156,20 +162,34 @@ fun aLoadedContentState(
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null,
isDm: Boolean = false,
roomType: RoomType = RoomType.Room,
roomAvatarUrl: String? = null,
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
joinRule: JoinRule? = null,
details: LoadedDetails = aLoadedDetailsRoom(isDm = false),
) = ContentState.Loaded(
roomId = roomId,
name = name,
alias = alias,
topic = topic,
numberOfMembers = numberOfMembers,
isDm = isDm,
roomType = roomType,
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(

View file

@ -7,21 +7,20 @@
package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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.unit.dp
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.libraries.designsystem.atomic.atoms.PlaceholderAtom
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.ButtonSize
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.Text
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.TopAppBar
import io.element.android.libraries.designsystem.theme.placeholderBackground
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 kotlinx.collections.immutable.persistentListOf
@Composable
fun JoinRoomView(
@ -92,7 +99,7 @@ fun JoinRoomView(
containerColor = Color.Transparent,
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 32.dp
vertical = 24.dp
),
topBar = {
JoinRoomTopBar(
@ -220,12 +227,14 @@ private fun JoinRoomFooter(
onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
leadingIcon = IconSource.Vector(CompoundIcons.Close())
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
leadingIcon = IconSource.Vector(CompoundIcons.Check())
)
}
Spacer(modifier = Modifier.height(24.dp))
@ -278,7 +287,6 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack)
is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack)
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
private fun JoinRoomContent(
roomIdOrAlias: RoomIdOrAlias,
@ -397,19 +383,40 @@ private fun JoinRoomContent(
IsKnockedLoadedContent()
}
else -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages)
Spacer(modifier = Modifier.height(32.dp))
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.verticalScroll(rememberScrollState())
) {
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
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
private fun UnknownRoomContent(
modifier: Modifier = Modifier
@ -429,7 +475,21 @@ private fun UnknownRoomContent(
RoomPreviewOrganism(
modifier = modifier,
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 = {
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
@ -448,7 +508,7 @@ private fun IncompleteContent(
RoomPreviewOrganism(
modifier = modifier,
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
},
title = {
when (roomIdOrAlias) {
@ -471,43 +531,32 @@ private fun IncompleteContent(
@Composable
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints(
modifier = modifier
.fillMaxHeight()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
IconTitleSubtitleMolecule(
modifier = Modifier.sizeIn(minHeight = maxHeight * 0.7f),
iconStyle = BigIcon.Style.SuccessSolid,
title = stringResource(R.string.screen_join_room_knock_sent_title),
subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
)
}
IconTitleSubtitleMolecule(
modifier = modifier.padding(horizontal = 8.dp),
iconStyle = BigIcon.Style.SuccessSolid,
title = stringResource(R.string.screen_join_room_knock_sent_title),
subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
)
}
@Composable
private fun DefaultLoadedContent(
contentState: ContentState.Loaded,
knockMessage: String,
hideAvatarImage: Boolean,
onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
Avatar(
contentState.avatarData(AvatarSize.RoomHeader),
contentState.avatarData(AvatarSize.RoomPreviewHeader),
hideImage = hideAvatarImage,
avatarType = AvatarType.Room(),
avatarType = if (contentState.isSpace) AvatarType.Space() else AvatarType.Room(),
)
},
title = {
if (contentState.name != null) {
RoomPreviewTitleAtom(
title = contentState.name,
)
RoomPreviewTitleAtom(title = contentState.name)
} else {
RoomPreviewTitleAtom(
title = stringResource(id = CommonStrings.common_no_room_name),
@ -516,37 +565,32 @@ private fun DefaultLoadedContent(
}
},
subtitle = {
if (contentState.alias != null) {
RoomPreviewSubtitleAtom(contentState.alias.value)
}
},
description = {
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
when {
contentState.details is LoadedDetails.Space -> {
SpaceInfoRow(
joinRule = contentState.joinRule ?: JoinRule.Public,
numberOfRooms = contentState.details.childrenCount,
)
}
contentState.alias != null -> {
RoomPreviewSubtitleAtom(contentState.alias.value)
}
}
},
description = {
RoomPreviewDescriptionAtom(
contentState.topic ?: "",
maxLines = if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanJoin) Int.MAX_VALUE else 2
)
},
memberCount = {
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_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_invited_by">"Invited by"</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_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.RoomId
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.exception.ClientException
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.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.JoinRule
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.aRoomPreviewInfo
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.toInviteSender
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
@ -90,6 +92,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo()
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -107,7 +112,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
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)
}
}
@ -118,6 +123,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -142,7 +150,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
val inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED,
@ -151,7 +159,21 @@ class JoinRoomPresenterTest {
)
val inviteData = roomInfo.toInviteData()
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 {
getRoomInfoFlowLambda = { _ ->
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
fun `present - when room is invited read the number of member from the room preview`() = runTest {
val roomInfo = aRoomInfo(
@ -182,10 +335,16 @@ class JoinRoomPresenterTest {
aRoomPreview(
info = aRoomPreviewInfo(
numberOfJoinedMembers = 10,
)
),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
)
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -209,7 +368,11 @@ class JoinRoomPresenterTest {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply {
val matrixClient = FakeMatrixClient(
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
}
@ -244,6 +407,9 @@ class JoinRoomPresenterTest {
}
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@ -272,6 +438,9 @@ class JoinRoomPresenterTest {
fun `present - when room is joined with error, it is possible to clear the error`() = runTest {
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@ -334,16 +503,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.BANNED,
),
roomMembershipDetails = {
Result.success(
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
Result.success(aRoomMembershipDetails())
}
)
)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -371,6 +538,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -392,6 +562,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@ -423,7 +596,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomDescription.topic)
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
assertThat(contentState.isDm).isFalse()
assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = false))
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
}
}
@ -497,6 +670,9 @@ class JoinRoomPresenterTest {
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@ -542,6 +718,9 @@ class JoinRoomPresenterTest {
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@ -586,6 +765,9 @@ class JoinRoomPresenterTest {
val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@ -634,10 +816,16 @@ class JoinRoomPresenterTest {
isHistoryWorldReadable = false,
joinRule = JoinRule.Public,
currentUserMembership = null,
)
),
roomMembershipDetails = {
Result.success(aRoomMembershipDetails())
},
)
)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -652,10 +840,10 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin,
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
)
)
}
@ -681,16 +869,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.INVITED,
),
roomMembershipDetails = {
Result.success(
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
Result.success(aRoomMembershipDetails())
}
)
)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -705,8 +891,6 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
inviteData = InviteData(
@ -724,7 +908,9 @@ class JoinRoomPresenterTest {
),
membershipChangeReason = null,
),
)
),
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
)
)
}
@ -751,15 +937,15 @@ class JoinRoomPresenterTest {
),
roomMembershipDetails = {
Result.success(
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
aRoomMembershipDetails(),
)
}
)
)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -774,8 +960,6 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
banSender = InviteSender(
@ -789,7 +973,9 @@ class JoinRoomPresenterTest {
membershipChangeReason = null,
),
reason = null,
)
),
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
)
)
}
@ -815,16 +1001,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.KNOCKED,
),
roomMembershipDetails = {
Result.success(
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
Result.success(aRoomMembershipDetails())
}
)
)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -839,10 +1023,10 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDm = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked,
joinRule = JoinRule.Public,
details = aLoadedDetailsRoom(isDm = false),
)
)
}
@ -854,9 +1038,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
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(
matrixClient = client
@ -874,9 +1066,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
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(
matrixClient = client
@ -894,9 +1094,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
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(
matrixClient = client
@ -914,9 +1122,19 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
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(
matrixClient = client
@ -934,9 +1152,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
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(
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
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -1004,7 +1213,10 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION)
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -1029,7 +1241,10 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null))
}
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@ -1068,7 +1283,11 @@ internal fun createJoinRoomPresenter(
roomDescription: Optional<RoomDescription> = Optional.empty(),
serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
matrixClient: MatrixClient = FakeMatrixClient(),
matrixClient: MatrixClient = FakeMatrixClient(
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
),
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
@ -1095,3 +1314,8 @@ internal fun createJoinRoomPresenter(
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.test.anInviteData
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.test.room.aRoomMember
import io.element.android.libraries.matrix.ui.model.toInviteSender
@ -218,21 +217,6 @@ class JoinRoomViewTest {
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
fun `clicking on ok when user is unauthorized the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ enum class AvatarSize(val dp: Dp) {
CurrentUserTopBar(32.dp),
IncomingCall(140.dp),
RoomHeader(96.dp),
RoomDetailsHeader(96.dp),
RoomListItem(52.dp),
SpaceListItem(52.dp),
@ -69,5 +69,7 @@ enum class AvatarSize(val dp: Dp) {
OrganizationHeader(64.dp),
SpaceHeader(64.dp),
RoomPreviewHeader(64.dp),
RoomPreviewInviter(56.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.StateFlow
import java.util.Optional
interface SpaceRoomList {
sealed interface PaginationStatus {
@ -16,7 +17,7 @@ interface SpaceRoomList {
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
}
fun currentSpaceFlow(): Flow<SpaceRoom?>
fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>>
val spaceRoomsFlow: Flow<List<SpaceRoom>>
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 kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList(
@ -31,7 +32,7 @@ class RustSpaceRoomList(
) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>()
override fun currentSpaceFlow(): Flow<SpaceRoom?> {
override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> {
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.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.util.Optional
/**
* An in memory cache of space rooms.
@ -20,8 +21,8 @@ import kotlinx.coroutines.flow.update
*/
class SpaceRoomCache {
private val inMemoryCache = MutableStateFlow<Map<RoomId, SpaceRoom?>>(emptyMap())
fun getSpaceRoomFlow(roomId: RoomId): Flow<SpaceRoom?> {
return inMemoryCache.mapState { it[roomId] }
fun getSpaceRoomFlow(roomId: RoomId): StateFlow<Optional<SpaceRoom>> {
return inMemoryCache.mapState { Optional.ofNullable(it[roomId]) }
}
fun update(spaceRooms: List<SpaceRoom>) {

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import kotlin.jvm.optionals.getOrNull
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomListTest {
@ -93,10 +94,10 @@ class RustSpaceRoomListTest {
spaceRoomCache = spaceRoomCache,
)
sut.currentSpaceFlow().test {
assertThat(awaitItem()).isNull()
assertThat(awaitItem().getOrNull()).isNull()
val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID)
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 {
val sut = SpaceRoomCache()
sut.getSpaceRoomFlow(A_ROOM_ID).test {
assertThat(awaitItem()).isNull()
assertThat(awaitItem().isEmpty).isTrue()
val room = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
@ -34,7 +34,7 @@ class SpaceRoomCacheTest {
roomType = RoomType.Space,
)
sut.update(listOf(space))
assertThat(awaitItem()).isEqualTo(space)
assertThat(awaitItem().get()).isEqualTo(space)
val spaceOther = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Space,

View file

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

View file

@ -7,19 +7,16 @@
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
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.AvatarData
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.matrix.api.room.join.JoinRule
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.persistentListOf
@ -47,47 +45,45 @@ fun SpaceHeaderView(
modifier: Modifier = Modifier,
topicMaxLines: Int = Int.MAX_VALUE,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 32.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space(false),
)
name?.let {
Text(
text = name,
style = ElementTheme.typography.fontHeadingLgBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
RoomPreviewOrganism(
modifier = modifier.padding(24.dp),
avatar = {
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space(),
)
}
if (joinRule != null) {
SpaceInfoRow(
joinRule = joinRule,
numberOfRooms = numberOfRooms,
},
title = {
if (name != null) {
RoomPreviewTitleAtom(title = name)
} 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

View file

@ -69,6 +69,7 @@ fun SpaceRoomItemView(
onLongClick = onLongClick,
) {
NameAndIndicatorRow(
isSpace = spaceRoom.isSpace,
name = spaceRoom.name,
showIndicator = showUnreadIndicator
)
@ -130,6 +131,7 @@ private fun SubtitleRow(
@Composable
private fun NameAndIndicatorRow(
isSpace: Boolean,
name: String?,
showIndicator: Boolean,
modifier: Modifier = Modifier,
@ -142,7 +144,7 @@ private fun NameAndIndicatorRow(
Text(
modifier = Modifier.weight(1f),
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 },
color = ElementTheme.colors.textPrimary,
maxLines = 1,

View file

@ -105,7 +105,7 @@ class DefaultNotificationConversationService(
targetSize = defaultShortcutIconSize.toLong()
)?.let(IconCompat::createWithBitmap)
?: 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)
val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150
size 16275
oid sha256:ddaae721cac21a188dc3862e3e14e80eeb13c92826bccfb8364ecbd72948d9bd
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
oid sha256:3f52fa29e031ab362baed77ac18228960d67eb5d6bdf909b6c1fb3777f5eacc5
size 19629
oid sha256:8cb143e42d0bc07652d74c74ba641a5332ee9bebb071fa5efba59ecacad04be5
size 21857

View file

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

View file

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

View file

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

View file

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