Merge pull request #2752 from element-hq/feature/bma/blockOpeningSpace

Block opening space
This commit is contained in:
Benoit Marty
2024-04-25 14:28:28 +02:00
committed by GitHub
199 changed files with 465 additions and 179 deletions

View File

@@ -128,7 +128,15 @@ class RoomFlowNode @AssistedInject constructor(
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
val info = roomInfo.getOrNull()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom(roomId))
if (info.isSpace) {
// It should not happen, but probably due to an issue in the sliding sync,
// we can have a space here in case the space has just been joined.
// So navigate to the JoinRoom target for now, which will
// handle the space not supported screen
backstack.newRoot(NavTarget.JoinRoom(roomId))
} else {
backstack.newRoot(NavTarget.JoinedRoom(roomId))
}
} else {
backstack.newRoot(NavTarget.JoinRoom(roomId))
}

View File

@@ -37,12 +37,14 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.coroutines.CoroutineScope
@@ -56,6 +58,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private val matrixClient: MatrixClient,
private val knockRoom: KnockRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val buildMeta: BuildMeta,
) : Presenter<JoinRoomState> {
interface Factory {
fun create(
@@ -134,6 +137,7 @@ class JoinRoomPresenter @AssistedInject constructor(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
knockAction = knockAction.value,
applicationName = buildMeta.applicationName,
eventSink = ::handleEvents
)
}
@@ -153,6 +157,7 @@ private fun RoomPreview.toContentState(): ContentState {
alias = canonicalAlias,
numberOfMembers = numberOfJoinedMembers,
isDirect = false,
roomType = roomType,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when {
// Note when isInvited, roomInfo will be used, so if this happen, it will be temporary.
@@ -173,6 +178,7 @@ internal fun RoomDescription.toContentState(): ContentState {
alias = alias,
numberOfMembers = numberOfMembers,
isDirect = false,
roomType = RoomType.Room,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (joinRule) {
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
@@ -191,6 +197,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
alias = canonicalAlias,
numberOfMembers = activeMembersCount,
isDirect = isDirect,
roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when {
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
@@ -207,7 +214,8 @@ internal fun ContentState.toInviteData(): InviteData? {
return when (this) {
is ContentState.Loaded -> InviteData(
roomId = roomId,
roomName = computedTitle,
// Note: name should not be null at this point, but use Id just in case...
roomName = name ?: roomId.value,
isDirect = isDirect
)
else -> null

View File

@@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.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.ui.model.InviteSender
@Immutable
@@ -31,6 +32,7 @@ data class JoinRoomState(
val contentState: ContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val knockAction: AsyncAction<Unit>,
val applicationName: String,
val eventSink: (JoinRoomEvents) -> Unit
) {
val joinAuthorisationStatus = when (contentState) {
@@ -50,17 +52,10 @@ sealed interface ContentState {
val alias: RoomAlias?,
val numberOfMembers: Long?,
val isDirect: Boolean,
val roomType: RoomType,
val roomAvatarUrl: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus,
) : ContentState {
val computedTitle = name ?: roomId.value
val computedSubtitle = when {
alias != null -> alias.value
name == null -> ""
else -> roomId.value
}
val showMemberCount = numberOfMembers != null
fun avatarData(size: AvatarSize): AvatarData {

View File

@@ -27,6 +27,7 @@ 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.room.RoomType
import io.element.android.libraries.matrix.ui.model.InviteSender
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
@@ -38,6 +39,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
aJoinRoomState(
contentState = anUnknownContentState()
),
aJoinRoomState(
contentState = aLoadedContentState(
name = null,
alias = null,
topic = null,
)
),
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
),
@@ -67,6 +75,15 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
aJoinRoomState(
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
),
aJoinRoomState(
contentState = aLoadedContentState(
roomId = RoomId("!aSpaceId:domain"),
name = "A space",
alias = null,
topic = "This is the topic of a space",
roomType = RoomType.Space,
)
),
)
}
@@ -85,11 +102,12 @@ fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(room
fun aLoadedContentState(
roomId: RoomId = A_ROOM_ID,
name: String = "Element X android",
name: String? = "Element X android",
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null,
isDirect: Boolean = false,
roomType: RoomType = RoomType.Room,
roomAvatarUrl: String? = null,
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
) = ContentState.Loaded(
@@ -99,6 +117,7 @@ fun aLoadedContentState(
topic = topic,
numberOfMembers = numberOfMembers,
isDirect = isDirect,
roomType = roomType,
roomAvatarUrl = roomAvatarUrl,
joinAuthorisationStatus = joinAuthorisationStatus
)
@@ -112,6 +131,7 @@ fun aJoinRoomState(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
knockAction = knockAction,
applicationName = "AppName",
eventSink = eventSink
)

View File

@@ -20,8 +20,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -29,9 +31,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
@@ -54,6 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.ui.strings.CommonStrings
@@ -75,7 +80,10 @@ fun JoinRoomView(
JoinRoomTopBar(onBackClicked = onBackPressed)
},
content = {
JoinRoomContent(contentState = state.contentState)
JoinRoomContent(
contentState = state.contentState,
applicationName = state.applicationName,
)
},
footer = {
JoinRoomFooter(
@@ -94,7 +102,8 @@ fun JoinRoomView(
},
onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
},
onGoBack = onBackPressed,
)
}
)
@@ -115,6 +124,7 @@ private fun JoinRoomFooter(
onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit,
onRetry: () -> Unit,
onGoBack: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.contentState is ContentState.Failure) {
@@ -124,6 +134,13 @@ private fun JoinRoomFooter(
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
Button(
text = stringResource(CommonStrings.action_go_back),
onClick = onGoBack,
modifier = modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
} else {
val joinAuthorisationStatus = state.joinAuthorisationStatus
when (joinAuthorisationStatus) {
@@ -170,6 +187,7 @@ private fun JoinRoomFooter(
@Composable
private fun JoinRoomContent(
contentState: ContentState,
applicationName: String,
modifier: Modifier = Modifier,
) {
when (contentState) {
@@ -180,10 +198,21 @@ private fun JoinRoomContent(
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
},
title = {
RoomPreviewTitleAtom(contentState.computedTitle)
if (contentState.name != null) {
RoomPreviewTitleAtom(
title = contentState.name,
)
} else {
RoomPreviewTitleAtom(
title = stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic
)
}
},
subtitle = {
RoomPreviewSubtitleAtom(contentState.computedSubtitle)
if (contentState.alias != null) {
RoomPreviewSubtitleAtom(contentState.alias.value)
}
},
description = {
Column(
@@ -195,6 +224,21 @@ private fun JoinRoomContent(
InviteSenderView(inviteSender = inviteSender)
}
RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.roomType == RoomType.Space) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.screen_join_room_space_not_supported_title),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyLgMedium,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
)
}
}
},
memberCount = {
@@ -250,7 +294,7 @@ private fun JoinRoomContent(
},
subtitle = {
Text(
text = "Failed to get information about the room",
text = stringResource(id = CommonStrings.error_unknown),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error,
)

View File

@@ -23,6 +23,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.joinroom.impl.JoinRoomPresenter
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@@ -37,6 +38,7 @@ object JoinRoomModule {
client: MatrixClient,
knockRoom: KnockRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
buildMeta: BuildMeta,
): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory {
override fun create(
@@ -51,6 +53,7 @@ object JoinRoomModule {
matrixClient = client,
knockRoom = knockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
buildMeta = buildMeta,
)
}
}

View File

@@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_join_action">"Join room"</string>
<string name="screen_join_room_knock_action">"Knock to join"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. Youll be able to join the conversation once approved."</string>
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
<string name="screen_join_room_title_knock">"Want to join this room?"</string>

View File

@@ -24,17 +24,20 @@ import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.ui.model.toInviteSender
@@ -61,6 +64,7 @@ class JoinRoomPresenterTest {
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
assertThat(state.applicationName).isEqualTo("AppName")
cancelAndIgnoreRemainingEvents()
}
}
@@ -315,7 +319,7 @@ class JoinRoomPresenterTest {
topic = "Room topic",
avatarUrl = "avatarUrl",
numberOfJoinedMembers = 2,
roomType = null,
roomType = RoomType.Room,
isHistoryWorldReadable = false,
isJoined = false,
isInvited = false,
@@ -339,6 +343,7 @@ class JoinRoomPresenterTest {
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
isDirect = false,
roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
)
@@ -412,6 +417,7 @@ class JoinRoomPresenterTest {
roomDescription: Optional<RoomDescription> = Optional.empty(),
matrixClient: MatrixClient = FakeMatrixClient(),
knockRoom: KnockRoom = FakeKnockRoom(),
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
): JoinRoomPresenter {
return JoinRoomPresenter(
@@ -420,6 +426,7 @@ class JoinRoomPresenterTest {
roomDescription = roomDescription,
matrixClient = matrixClient,
knockRoom = knockRoom,
buildMeta = buildMeta,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
)
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@@ -128,6 +129,21 @@ class JoinRoomViewTest {
rule.clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
}
@Test
fun `clicking on Go back 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,
),
onBackPressed = it
)
rule.clickOn(CommonStrings.action_go_back)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(

View File

@@ -42,16 +42,21 @@ private fun anEditDefaultNotificationSettingsState(
) = EditDefaultNotificationSettingState(
isOneToOne = isOneToOne,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
roomsWithUserDefinedMode = persistentListOf(aRoomSummary()),
roomsWithUserDefinedMode = persistentListOf(
aRoomSummary("Room"),
aRoomSummary(null),
),
changeNotificationSettingAction = changeNotificationSettingAction,
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
eventSink = {}
)
private fun aRoomSummary() = RoomSummary.Filled(
private fun aRoomSummary(
name: String?,
) = RoomSummary.Filled(
aRoomSummaryDetails(
roomId = RoomId("!roomId:domain"),
name = "Room",
name = name,
avatarUrl = null,
isDirect = false,
lastMessage = null,

View File

@@ -21,6 +21,7 @@ import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
@@ -100,7 +101,11 @@ fun EditDefaultNotificationSettingView(
)
ListItem(
headlineContent = {
Text(text = summary.details.name)
val roomName = summary.details.name
Text(
text = roomName ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { roomName == null }
)
},
supportingContent = {
Text(text = subtitle)

View File

@@ -24,6 +24,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -87,8 +89,9 @@ private fun RoomListModalBottomSheetContent(
ListItem(
headlineContent = {
Text(
text = contextMenu.roomName,
text = contextMenu.roomName ?: stringResource(id = CommonStrings.common_no_room_name),
style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { contextMenu.roomName == null }
)
}
)
@@ -192,22 +195,11 @@ private fun RoomListModalBottomSheetContent(
// Remove this preview when the issue is fixed.
@PreviewsDayNight
@Composable
internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
internal fun RoomListModalBottomSheetContentPreview(
@PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
) = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = aContextMenuShown(hasNewContent = true),
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},
onLeaveRoomClicked = {},
onFavoriteChanged = {},
)
}
@PreviewsDayNight
@Composable
internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = aContextMenuShown(isDm = true),
contextMenu = contextMenu,
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},

View File

@@ -298,6 +298,7 @@ class RoomListPresenter @Inject constructor(
@VisibleForTesting
internal fun RoomListRoomSummary.toInviteData() = InviteData(
roomId = roomId,
roomName = name,
// Note: `name` should not be null at this point, but just in case, fallback to the roomId
roomName = name ?: roomId.value,
isDirect = isDirect,
)

View File

@@ -48,7 +48,7 @@ data class RoomListState(
data object Hidden : ContextMenu
data class Shown(
val roomId: RoomId,
val roomName: String,
val roomName: String?,
val isDm: Boolean,
val isFavorite: Boolean,
val markAsUnreadFeatureFlagEnabled: Boolean,

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
open class RoomListStateContextMenuShownProvider : PreviewParameterProvider<RoomListState.ContextMenu.Shown> {
override val values: Sequence<RoomListState.ContextMenu.Shown>
get() = sequenceOf(
aContextMenuShown(hasNewContent = true),
aContextMenuShown(isDm = true),
aContextMenuShown(roomName = null)
)
}
internal fun aContextMenuShown(
roomName: String? = "aRoom",
isDm: Boolean = false,
hasNewContent: Boolean = false,
isFavorite: Boolean = false,
) = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = roomName,
isDm = isDm,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
)

View File

@@ -32,7 +32,6 @@ import io.element.android.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@@ -45,6 +44,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(),
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aRoomListState(hasNetworkConnection = false),
aRoomListState(contextMenu = aContextMenuShown(roomName = null)),
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
@@ -109,7 +109,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
),
aRoomListRoomSummary(
id = "!roomId3:domain",
displayType = RoomSummaryDisplayType.PLACEHOLDER,
displayType = RoomSummaryDisplayType.PLACEHOLDER,
),
aRoomListRoomSummary(
id = "!roomId4:domain",
@@ -117,17 +117,3 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
),
)
}
internal fun aContextMenuShown(
roomName: String = "aRoom",
isDm: Boolean = false,
hasNewContent: Boolean = false,
isFavorite: Boolean = false,
) = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = roomName,
isDm = isDm,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
)

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -168,7 +169,7 @@ private fun RoomSummaryScaffoldRow(
@Composable
private fun NameAndTimestampRow(
name: String,
name: String?,
timestamp: String?,
isHighlighted: Boolean,
modifier: Modifier = Modifier
@@ -181,7 +182,8 @@ private fun NameAndTimestampRow(
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
text = name,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = MaterialTheme.roomListRoomName(),
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -272,7 +274,7 @@ private fun LastMessageAndIndicatorRow(
@Composable
private fun InviteNameAndIndicatorRow(
name: String,
name: String?,
modifier: Modifier = Modifier,
) {
Row(
@@ -283,7 +285,8 @@ private fun InviteNameAndIndicatorRow(
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
text = name,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = MaterialTheme.roomListRoomName(),
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@@ -28,7 +28,7 @@ data class RoomListRoomSummary(
val id: String,
val displayType: RoomSummaryDisplayType,
val roomId: RoomId,
val name: String,
val name: String?,
val canonicalAlias: RoomAlias?,
val numberOfUnreadMessages: Int,
val numberOfUnreadMentions: Int,

View File

@@ -31,6 +31,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
listOf(
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
aRoomListRoomSummary(),
aRoomListRoomSummary(name = null),
aRoomListRoomSummary(lastMessage = null),
aRoomListRoomSummary(
name = "A very long room name that should be truncated",
@@ -100,7 +101,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
displayName = "Bob",
),
isDirect = true,
)
),
aRoomListRoomSummary(
name = null,
displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender(
userId = UserId("@bob:matrix.org"),
displayName = "Bob",
),
),
),
).flatten()
}
@@ -117,7 +126,7 @@ internal fun anInviteSender(
internal fun aRoomListRoomSummary(
id: String = "!roomId:domain",
name: String = "Room name",
name: String? = "Room name",
numberOfUnreadMessages: Int = 0,
numberOfUnreadMentions: Int = 0,
numberOfUnreadNotifications: Int = 0,

View File

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

View File

@@ -18,17 +18,23 @@ package io.element.android.libraries.designsystem.atomic.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewTitleAtom(title: String, modifier: Modifier = Modifier) {
fun RoomPreviewTitleAtom(
title: String,
modifier: Modifier = Modifier,
fontStyle: FontStyle? = null,
) {
Text(
modifier = modifier,
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
fontStyle = fontStyle,
color = ElementTheme.colors.textPrimary,
)
}

View File

@@ -26,7 +26,8 @@ data class AvatarData(
val size: AvatarSize,
) {
val initial by lazy {
(name?.takeIf { it.isNotBlank() } ?: id)
// For roomIds, use "#" as initial
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.room
sealed interface RoomType {
data object Space : RoomType
data object Room : RoomType
data class Other(val type: String) : RoomType
}

View File

@@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.room.preview
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomType
data class RoomPreview(
/** The room id for this room. */
@@ -33,7 +34,7 @@ data class RoomPreview(
/** The number of joined members. */
val numberOfJoinedMembers: Long,
/** The room type (space, custom) or nothing, if it's a regular room. */
val roomType: String?,
val roomType: RoomType,
/** Is the history world-readable for this room? */
val isHistoryWorldReadable: Boolean,
/** Is the room joined by the current user? */

View File

@@ -37,7 +37,7 @@ sealed interface RoomSummary {
data class RoomSummaryDetails(
val roomId: RoomId,
val name: String,
val name: String?,
val canonicalAlias: RoomAlias?,
val isDirect: Boolean,
val avatarUrl: String?,

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.RoomType
fun String?.toRoomType(): RoomType {
return when (this) {
null -> RoomType.Room
"m.space" -> RoomType.Space
else -> RoomType.Other(this)
}
}

View File

@@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room.preview
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.impl.room.toRoomType
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
object RoomPreviewMapper {
@@ -30,7 +31,7 @@ object RoomPreviewMapper {
topic = roomPreview.topic,
avatarUrl = roomPreview.avatarUrl,
numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
roomType = roomPreview.roomType,
roomType = roomPreview.roomType.toRoomType(),
isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
isJoined = roomPreview.isJoined,
isInvited = roomPreview.isInvited,

View File

@@ -40,7 +40,7 @@ val RoomListFilter.predicate
(roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread)
}
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true)
roomSummary is RoomSummary.Filled && roomSummary.details.name.orEmpty().contains(pattern, ignoreCase = true)
}
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited()

View File

@@ -33,7 +33,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
}
return RoomSummaryDetails(
roomId = RoomId(roomInfo.id),
name = roomInfo.name ?: roomInfo.id,
name = roomInfo.name,
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl,

View File

@@ -63,7 +63,7 @@ fun aRoomSummaryFilled(
fun aRoomSummaryDetails(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
name: String? = A_ROOM_NAME,
isDirect: Boolean = false,
avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(),

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
open class RoomSummaryDetailsProvider : PreviewParameterProvider<RoomSummaryDetails> {
override val values: Sequence<RoomSummaryDetails>
get() = sequenceOf(
aRoomSummaryDetails(),
aRoomSummaryDetails(name = null),
)
}
fun aRoomSummaryDetails(
roomId: RoomId = RoomId("!room:domain"),
name: String? = "roomName",
canonicalAlias: RoomAlias? = null,
isDirect: Boolean = true,
avatarUrl: String? = null,
lastMessage: RoomMessage? = null,
inviter: RoomMember? = null,
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
isDm: Boolean = false,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
) = RoomSummaryDetails(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage,
inviter = inviter,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDm = isDm,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
)

View File

@@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
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.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -43,12 +44,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.ui.strings.CommonStrings
@@ -67,7 +62,8 @@ fun SelectedRoom(
) {
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarUrl, AvatarSize.SelectedRoom))
Text(
text = roomSummary.name,
// If name is null, we do not have space to render "No room name", so just use `#` here.
text = roomSummary.name ?: "#",
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.bodyLarge,
@@ -97,45 +93,11 @@ fun SelectedRoom(
@PreviewsDayNight
@Composable
internal fun SelectedRoomPreview() = ElementPreview {
internal fun SelectedRoomPreview(
@PreviewParameter(RoomSummaryDetailsProvider::class) roomSummaryDetails: RoomSummaryDetails
) = ElementPreview {
SelectedRoom(
roomSummary = aRoomSummaryDetails(),
roomSummary = roomSummaryDetails,
onRoomRemoved = {},
)
}
fun aRoomSummaryDetails(
roomId: RoomId = RoomId("!room:domain"),
name: String = "roomName",
canonicalAlias: RoomAlias? = null,
isDirect: Boolean = true,
avatarUrl: String? = null,
lastMessage: RoomMessage? = null,
inviter: RoomMember? = null,
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
isDm: Boolean = false,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
) = RoomSummaryDetails(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage,
inviter = inviter,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDm = isDm,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
)

View File

@@ -57,7 +57,7 @@ class RoomSelectPresenter @AssistedInject constructor(
LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
.map { it.details }
.filter { it.name.contains(query, ignoreCase = true) }
.filter { it.name.orEmpty().contains(query, ignoreCase = true) }
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
.toPersistentList()
results = if (filteredSummaries.isNotEmpty()) {

View File

@@ -68,4 +68,8 @@ private fun aForwardMessagesRoomList() = persistentListOf(
name = "Room with alias",
canonicalAlias = RoomAlias("#alias:example.org"),
),
aRoomSummaryDetails(
roomId = RoomId("!room3:domain"),
name = null,
),
)

View File

@@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -235,7 +236,8 @@ private fun RoomSummaryView(
// Name
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = summary.name,
text = summary.name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { summary.name == null },
color = ElementTheme.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@@ -158,6 +158,7 @@
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Mute"</string>
<string name="common_no_results">"No results"</string>
<string name="common_no_room_name">"No room name"</string>
<string name="common_offline">"Offline"</string>
<string name="common_or">"or"</string>
<string name="common_password">"Password"</string>

View File

@@ -101,7 +101,6 @@ class KonsistPreviewTest {
"PollContentViewEndedPreview",
"PollContentViewUndisclosedPreview",
"ReadReceiptBottomSheetPreview",
"RoomListModalBottomSheetContentForDmPreview",
"RoomMemberListViewBannedPreview",
"SasEmojisPreview",
"SecureBackupSetupViewChangePreview",

Some files were not shown because too many files have changed in this diff Show More