Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,3 +37,11 @@ fun RoomInfo.toInviteData(): InviteData {
isDm = isDm,
)
}

fun SpaceRoom.toInviteData(): InviteData {
return InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = false,
)
}
1 change: 1 addition & 0 deletions features/joinroom/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ dependencies {
testImplementation(projects.features.invite.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.previewutils)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -80,13 +85,18 @@ class JoinRoomPresenter(
): JoinRoomPresenter
}

private val spaceList = matrixClient.spaceService.spaceRoomList(roomId)

@Composable
override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) }
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) }
Expand All @@ -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)) {
Expand Down Expand Up @@ -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(
Expand All @@ -284,47 +306,79 @@ 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,
name = name,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading
Loading