[Room Details] Implement room details screen (#256)

* Implement Room Details screen

* Add option to create permalink from room id and alias, add share room action
This commit is contained in:
Jorge Martin Espinosa 2023-03-29 07:16:27 +02:00 committed by GitHub
parent 4a121fbd0f
commit ecc73dd325
83 changed files with 1203 additions and 117 deletions

View file

@ -46,6 +46,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
implementation(projects.features.verifysession.api)
implementation(projects.features.roomdetails.api)
implementation(projects.tests.uitests)
implementation(libs.coil)

View file

@ -26,12 +26,15 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -44,6 +47,7 @@ class RoomFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint,
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
) : BackstackNode<RoomFlowNode.NavTarget>(
backstack = BackStack(
@ -84,7 +88,14 @@ class RoomFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Messages -> {
messagesEntryPoint.createNode(this, buildContext)
messagesEntryPoint.createNode(this, buildContext, object : MessagesEntryPoint.Callback {
override fun onRoomDetailsClicked() {
backstack.push(NavTarget.RoomDetails)
}
})
}
NavTarget.RoomDetails -> {
roomDetailsEntryPoint.createNode(this, buildContext)
}
}
}
@ -92,6 +103,9 @@ class RoomFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable {
@Parcelize
object Messages : NavTarget
@Parcelize
object RoomDetails : NavTarget
}
@Composable
@ -99,6 +113,7 @@ class RoomFlowNode @AssistedInject constructor(
Children(
navModel = backstack,
modifier = modifier,
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View file

@ -245,6 +245,7 @@ koverMerged {
includes += "*State"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
}
bound {
minValue = 90

1
changelog.d/251.feature Normal file
View file

@ -0,0 +1 @@
Implement Room Details screen

View file

@ -26,4 +26,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View file

@ -16,6 +16,19 @@
package io.element.android.features.messages.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
interface MessagesEntryPoint : SimpleFeatureEntryPoint
interface MessagesEntryPoint : FeatureEntryPoint {
fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: Callback,
): Node
interface Callback : Plugin {
fun onRoomDetailsClicked()
}
}

View file

@ -18,15 +18,21 @@ package io.element.android.features.messages.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<MessagesNode>(buildContext)
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: MessagesEntryPoint.Callback
): Node {
return parentNode.createNode<MessagesNode>(buildContext, listOf(callback))
}
}

View file

@ -21,10 +21,13 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(RoomScope::class)
class MessagesNode @AssistedInject constructor(
@ -33,12 +36,19 @@ class MessagesNode @AssistedInject constructor(
private val presenter: MessagesPresenter,
) : Node(buildContext, plugins = plugins) {
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
private fun onRoomDetailsClicked() {
callback?.onRoomDetailsClicked()
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MessagesView(
state = state,
onBackPressed = this::navigateUp,
onRoomDetailsClicked = this::onRoomDetailsClicked,
modifier = modifier
)
}

View file

@ -21,6 +21,7 @@
package io.element.android.features.messages.impl
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -69,6 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.launch
import timber.log.Timber
@ -77,6 +79,7 @@ fun MessagesView(
state: MessagesState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
onRoomDetailsClicked: () -> Unit = {},
) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
val itemActionsBottomSheetState = rememberModalBottomSheetState(
@ -112,7 +115,8 @@ fun MessagesView(
MessagesViewTopBar(
roomTitle = state.roomName,
roomAvatar = state.roomAvatar,
onBackPressed = onBackPressed
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
)
},
content = { padding ->
@ -174,6 +178,7 @@ fun MessagesViewTopBar(
roomTitle: String?,
roomAvatar: AvatarData?,
modifier: Modifier = Modifier,
onRoomDetailsClicked: () -> Unit = {},
onBackPressed: () -> Unit = {},
) {
TopAppBar(
@ -187,7 +192,10 @@ fun MessagesViewTopBar(
}
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.clickable { onRoomDetailsClicked() },
verticalAlignment = Alignment.CenterVertically
) {
if (roomAvatar != null) {
Avatar(roomAvatar)
Spacer(modifier = Modifier.width(8.dp))

View file

@ -2,6 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Attach screenshot"</string>
<string name="screen_bug_report_contact_me">"You may contact me if you have any follow up questions"</string>
<string name="screen_bug_report_edit_screenshot">"Edit screenshot"</string>
<string name="screen_bug_report_editor_description">"Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can."</string>
<string name="screen_bug_report_editor_placeholder">"Describe the bug…"</string>
<string name="screen_bug_report_editor_supporting">"If possible, please write the description in English."</string>

1
features/roomdetails/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.roomdetails.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,26 @@
/*
* 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.roomdetails.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.libraries.architecture.FeatureEntryPoint
interface RoomDetailsEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext): Node
}

1
features/roomdetails/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,57 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.roomdetails.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
api(projects.features.roomdetails.api)
implementation(libs.coil.compose)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
ksp(libs.showkase.processor)
}

View file

@ -0,0 +1,32 @@
/*
* 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.roomdetails.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<RoomDetailsFlowNode>(buildContext)
}
}

View file

@ -0,0 +1,19 @@
/*
* 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.roomdetails.impl
sealed interface RoomDetailsEvent

View file

@ -0,0 +1,68 @@
/*
* 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.roomdetails.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
class RoomDetailsFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.RoomDetails,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
object RoomDetails : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext)
}
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = backstack,
modifier = modifier,
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.roomdetails.impl
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.ui.strings.R as StringR
@ContributesNode(RoomScope::class)
class RoomDetailsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: RoomDetailsPresenter,
private val room: MatrixRoom,
) : Node(buildContext, plugins = plugins) {
private fun onShareRoom(context: Context) {
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
?: PermalinkBuilder.permalinkForRoomId(room.roomId)
permalinkResult.onSuccess { permalink ->
startSharePlainTextIntent(
context = context,
activityResultLauncher = null,
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
text = permalink,
noActivityFoundMessage = context.getString(StringR.string.error_no_compatible_app_found)
)
}
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
val state = presenter.present()
RoomDetailsView(
state = state,
modifier = modifier,
goBack = { navigateUp() },
onShareRoom = { onShareRoom(context) },
)
}
}

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.roomdetails.impl
import androidx.compose.runtime.Composable
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import javax.inject.Inject
class RoomDetailsPresenter @Inject constructor(
private val room: MatrixRoom,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
// fun handleEvents(event: RoomDetailsEvent) {}
return RoomDetailsState(
roomId = room.roomId.value,
roomName = room.name ?: room.displayName,
roomAlias = room.alias,
roomAvatarUrl = room.avatarUrl,
roomTopic = room.topic,
memberCount = room.members.size,
isEncrypted = room.isEncrypted,
// eventSink = ::handleEvents
)
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.roomdetails.impl
data class RoomDetailsState(
val roomId: String,
val roomName: String,
val roomAlias: String?,
val roomAvatarUrl: String?,
val roomTopic: String?,
val memberCount: Int,
val isEncrypted: Boolean,
// val eventSink: (RoomDetailsEvent) -> Unit
)

View file

@ -0,0 +1,45 @@
/*
* 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.roomdetails.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
override val values: Sequence<RoomDetailsState>
get() = sequenceOf(
aRoomDetailsState(),
aRoomDetailsState().copy(roomTopic = null),
aRoomDetailsState().copy(isEncrypted = false),
aRoomDetailsState().copy(roomAlias = null),
// Add other state here
)
}
fun aRoomDetailsState() = RoomDetailsState(
roomId = "a room id",
roomName = "Marketing",
roomAlias = "#marketing:domain.com",
roomAvatarUrl = null,
roomTopic = "Welcome to #marketing, home of the Marketing team " +
"|| WIKI PAGE: https://domain.org/wiki/Marketing " +
"|| MAIL iki/Marketing " +
"|| MAI iki/Marketing " +
"|| MAI iki/Marketing...",
memberCount = 32,
isEncrypted = true,
// eventSink = {}
)

View file

@ -0,0 +1,204 @@
/*
* 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.roomdetails.impl
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.PersonAddAlt
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomDetailsView(
state: RoomDetailsState,
goBack: () -> Unit,
onShareRoom: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) })
},
) { padding ->
Column(modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
) {
HeaderSection(
avatarUrl = state.roomAvatarUrl,
roomId = state.roomId,
roomName = state.roomName,
roomAlias = state.roomAlias
)
ShareSection(onShareRoom = onShareRoom)
if (state.roomTopic != null) {
TopicSection(roomTopic = state.roomTopic)
}
MembersSection(memberCount = state.memberCount)
if (state.isEncrypted) {
SecuritySection()
}
OtherActionsSection()
}
}
}
@Composable
internal fun ShareSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) {
PreferenceCategory(modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_share_room_title),
icon = Icons.Outlined.Share,
onClick = onShareRoom,
)
}
}
@Composable
internal fun HeaderSection(
avatarUrl: String?,
roomId: String,
roomName: String,
roomAlias: String?,
modifier: Modifier = Modifier
) {
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(70.dp)) {
Avatar(
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.HUGE),
modifier = Modifier.fillMaxSize()
)
}
Spacer(modifier = Modifier.height(30.dp))
Text(roomName, style = ElementTextStyles.Bold.title1)
if (roomAlias != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(roomAlias, style = ElementTextStyles.Regular.body, color = MaterialTheme.colorScheme.secondary)
}
Spacer(Modifier.height(32.dp))
}
}
@Composable
internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
PreferenceCategory(title = stringResource(R.string.screen_room_details_topic_title), modifier = modifier) {
Text(
roomTopic,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary
)
}
}
@Composable
internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) {
PreferenceCategory(modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_people_title),
icon = Icons.Outlined.Person,
currentValue = memberCount.toString(),
)
PreferenceText(
title = stringResource(R.string.screen_room_details_invite_people_title),
icon = Icons.Outlined.PersonAddAlt,
)
}
}
@Composable
internal fun SecuritySection(modifier: Modifier = Modifier) {
PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title), modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_encryption_enabled_title),
subtitle = stringResource(R.string.screen_room_details_encryption_enabled_subtitle),
icon = Icons.Outlined.Lock,
)
}
}
@Composable
internal fun OtherActionsSection(modifier: Modifier = Modifier) {
PreferenceCategory(showDivider = false, modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_leave_room_title),
icon = ImageVector.vectorResource(R.drawable.ic_door_open),
tintColor = LocalColors.current.textActionCritical,
)
}
}
@Preview
@Composable
fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: RoomDetailsState) {
RoomDetailsView(
state = state,
goBack = {},
onShareRoom = {},
)
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,520Q457,520 468.5,508.5Q480,497 480,480Q480,463 468.5,451.5Q457,440 440,440Q423,440 411.5,451.5Q400,463 400,480Q400,497 411.5,508.5Q423,520 440,520ZM280,840L280,760L520,720Q520,720 520,720Q520,720 520,720L520,275Q520,260 511,248Q502,236 488,234L280,200L280,120L500,156Q544,164 572,197Q600,230 600,274L600,718Q600,747 581,769.5Q562,792 533,797L280,840ZM280,760L680,760L680,200Q680,200 680,200Q680,200 680,200L280,200Q280,200 280,200Q280,200 280,200L280,760ZM160,840Q143,840 131.5,828.5Q120,817 120,800Q120,783 131.5,771.5Q143,760 160,760L200,760L200,200Q200,166 223.5,143Q247,120 280,120L680,120Q714,120 737,143Q760,166 760,200L760,760L800,760Q817,760 828.5,771.5Q840,783 840,800Q840,817 828.5,828.5Q817,840 800,840L160,840Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>
<string name="screen_room_details_share_room_title">"Share room"</string>
<string name="screen_room_details_topic_title">"Topic"</string>
</resources>

View file

@ -0,0 +1,79 @@
/*
* 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.roomdetails
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
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.room.FakeMatrixRoom
import kotlinx.coroutines.test.runTest
import org.junit.Test
class RoomDetailsPresenterTests {
@Test
fun `present - initial state is created from room info`() = runTest {
val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.roomId).isEqualTo(room.roomId.value)
Truth.assertThat(initialState.roomName).isEqualTo(room.name)
Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic)
Truth.assertThat(initialState.memberCount).isEqualTo(room.members.count())
Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
}
}
@Test
fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(name = null)
val presenter = RoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.roomName).isEqualTo(room.displayName)
}
}
}
fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
displayName: String = "A fallback display name",
topic: String? = "A topic",
avatarUrl: String? = "https://matrix.org/avatar.jpg",
members: List<RoomMember> = emptyList(),
isEncrypted: Boolean = true,
) = FakeMatrixRoom(
roomId = roomId,
name = name,
displayName = displayName,
topic = topic,
avatarUrl = avatarUrl,
members = members,
isEncrypted = isEncrypted,
)

View file

@ -69,3 +69,6 @@ val ElementOrange = Color(0xFFD9B072)
val Vermilion = Color(0xFFFF5B55)
val LinkColor = Color(0xFF0086E6)
val TextColorCriticalLight = Color(0xFFD51928)
val TextColorCriticalDark = Color(0xfffd3e3c)

View file

@ -28,3 +28,12 @@ fun Boolean.toEnabledColor(): Color {
MaterialTheme.colorScheme.primary.copy(alpha = 0.40f)
}
}
@Composable
fun Boolean.toSecondaryEnabledColor(): Color {
return if (this) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.secondary.copy(alpha = 0.40f)
}
}

View file

@ -18,5 +18,7 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.ui.unit.dp
internal val preferenceMinHeight = 80.dp
internal val preferencePaddingEnd = 16.dp
internal val preferenceMinHeightOnlyTitle = 48.dp
internal val preferenceMinHeight = 64.dp
internal val preferencePaddingHorizontal = 16.dp
internal val preferencePaddingVertical = 16.dp

View file

@ -35,29 +35,35 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun PreferenceCategory(
title: String,
modifier: Modifier = Modifier,
title: String? = null,
showDivider: Boolean = true,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = modifier
.fillMaxWidth()
) {
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.secondary,
thickness = 1.dp
)
Text(
modifier = Modifier.padding(top = 8.dp, start = 56.dp),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
text = title,
)
if (title != null) {
PreferenceCategoryTitle(title = title)
}
content()
if (showDivider) {
PreferenceDivider()
}
}
}
@Composable
fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
text = title,
)
}
@Preview
@Composable
internal fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() }

View file

@ -0,0 +1,27 @@
/*
* 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.libraries.designsystem.components.preferences
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.theme.components.Divider
@Composable
fun PreferenceDivider(modifier: Modifier = Modifier) {
Divider(modifier, thickness = 0.5.dp)
}

View file

@ -29,7 +29,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
@ -39,10 +38,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -92,12 +90,7 @@ fun PreferenceTopAppBar(
TopAppBar(
modifier = modifier,
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
BackButton(onClick = onBackPressed)
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
@ -132,13 +125,16 @@ private fun ContentToPreview() {
) {
PreferenceText(
title = "Title",
subtitle = "Some other text",
icon = Icons.Default.BugReport,
)
PreferenceDivider()
PreferenceSwitch(
title = "Switch",
icon = Icons.Default.Announcement,
isChecked = true
isChecked = true,
)
PreferenceDivider()
PreferenceSlide(
title = "Slide",
summary = "Summary",

View file

@ -23,9 +23,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
@ -51,15 +52,15 @@ fun PreferenceSlide(
Box(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight),
contentAlignment = Alignment.CenterStart
.defaultMinSize(minHeight = preferenceMinHeight)
.padding(top = preferencePaddingVertical),
) {
Row(modifier = Modifier.fillMaxWidth()) {
PreferenceIcon(icon = icon)
Column(
modifier = Modifier
.weight(1f)
.padding(end = preferencePaddingEnd),
.padding(end = preferencePaddingHorizontal),
) {
Text(
modifier = Modifier.fillMaxWidth(),
@ -97,6 +98,7 @@ internal fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPrevie
@Composable
private fun ContentToPreview() {
PreferenceSlide(
icon = Icons.Default.Person,
title = "Slide",
summary = "Summary",
value = 0.75F

View file

@ -53,22 +53,20 @@ fun PreferenceSwitch(
.clickable { onCheckedChange(!isChecked) },
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.fillMaxWidth()) {
PreferenceIcon(
modifier = Modifier.padding(vertical = preferencePaddingVertical),
icon = icon,
enabled = enabled
)
Text(
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f).padding(vertical = preferencePaddingVertical),
style = MaterialTheme.typography.bodyLarge,
color = enabled.toEnabledColor(),
text = title
)
Checkbox(
modifier = Modifier.padding(end = preferencePaddingEnd),
modifier = Modifier.padding(end = preferencePaddingHorizontal).align(Alignment.CenterVertically),
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange

View file

@ -18,18 +18,24 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -37,31 +43,52 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun PreferenceText(
title: String,
// TODO subtitle
title: String?,
modifier: Modifier = Modifier,
subtitle: String? = null,
currentValue: String? = null,
icon: ImageVector? = null,
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Box(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.defaultMinSize(minHeight = minHeight)
.padding(end = preferencePaddingHorizontal)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(vertical = preferencePaddingVertical)
) {
PreferenceIcon(icon = icon)
Text(
modifier = Modifier
.weight(1f)
.padding(end = preferencePaddingEnd),
style = MaterialTheme.typography.bodyLarge,
text = title,
color = MaterialTheme.colorScheme.primary,
)
PreferenceIcon(icon = icon, tintColor = tintColor)
Column(modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
if (title != null) {
Text(
style = MaterialTheme.typography.bodyLarge,
text = title,
color = tintColor ?: MaterialTheme.colorScheme.primary,
)
}
if (title != null && subtitle != null) {
Spacer(modifier = Modifier.height(8.dp))
}
if (subtitle != null) {
Text(
style = MaterialTheme.typography.bodySmall,
text = subtitle,
color = tintColor ?: MaterialTheme.colorScheme.tertiary,
)
}
}
if (currentValue != null) {
Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
Spacer(Modifier.width(16.dp))
}
}
}
}
@ -78,6 +105,7 @@ internal fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview
private fun ContentToPreview() {
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = Icons.Default.BugReport,
)
}

View file

@ -17,10 +17,12 @@
package io.element.android.libraries.designsystem.components.preferences.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -28,25 +30,30 @@ import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceIcon(
icon: ImageVector?,
modifier: Modifier = Modifier,
enabled: Boolean = true
tintColor: Color? = null,
enabled: Boolean = true,
isVisible: Boolean = true,
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = "",
tint = enabled.toEnabledColor(),
tint = tintColor ?: enabled.toSecondaryEnabledColor(),
modifier = modifier
.padding(start = 8.dp)
.width(48.dp),
.width(48.dp)
.heightIn(max = 48.dp),
)
} else {
} else if (isVisible) {
Spacer(modifier = modifier.width(56.dp))
} else {
Spacer(modifier = modifier.width(16.dp))
}
}

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.designsystem.Gray_400
import io.element.android.libraries.designsystem.Gray_450
import io.element.android.libraries.designsystem.SystemGrey5Dark
import io.element.android.libraries.designsystem.SystemGrey6Dark
import io.element.android.libraries.designsystem.TextColorCriticalDark
import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview
fun elementColorsDark() = ElementColors(
@ -37,6 +38,7 @@ fun elementColorsDark() = ElementColors(
messageHighlightedBackground = Azure,
quaternary = Gray_400,
quinary = Gray_450,
textActionCritical = TextColorCriticalDark,
isLight = false,
)
@ -69,7 +71,7 @@ val materialColorSchemeDark = darkColorScheme(
// TODO errorContainer = ColorDarkTokens.ErrorContainer,
// TODO onErrorContainer = ColorDarkTokens.OnErrorContainer,
// TODO outline = ColorDarkTokens.Outline,
// TODO outlineVariant = ColorDarkTokens.OutlineVariant,
outlineVariant = Gray_450,
// TODO scrim = ColorDarkTokens.Scrim,
)

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.designsystem.Gray_25
import io.element.android.libraries.designsystem.Gray_50
import io.element.android.libraries.designsystem.SystemGrey5Light
import io.element.android.libraries.designsystem.SystemGrey6Light
import io.element.android.libraries.designsystem.TextColorCriticalLight
import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview
fun elementColorsLight() = ElementColors(
@ -37,6 +38,7 @@ fun elementColorsLight() = ElementColors(
messageHighlightedBackground = Azure,
quaternary = Gray_100,
quinary = Gray_50,
textActionCritical = TextColorCriticalLight,
isLight = true,
)
@ -69,7 +71,7 @@ val materialColorSchemeLight = lightColorScheme(
// TODO errorContainer = ColorLightTokens.ErrorContainer,
// TODO onErrorContainer = ColorLightTokens.OnErrorContainer,
// TODO outline = ColorLightTokens.Outline,
// TODO outlineVariant = ColorLightTokens.OutlineVariant,
outlineVariant = Gray_50,
// TODO scrim = ColorLightTokens.Scrim,
)

View file

@ -29,7 +29,8 @@ class ElementColors(
messageHighlightedBackground: Color,
quaternary: Color,
quinary: Color,
isLight: Boolean,
textActionCritical: Color,
isLight: Boolean
) {
var messageFromMeBackground by mutableStateOf(messageFromMeBackground)
private set
@ -44,6 +45,9 @@ class ElementColors(
var quinary by mutableStateOf(quinary)
private set
var textActionCritical by mutableStateOf(textActionCritical)
private set
var isLight by mutableStateOf(isLight)
private set
@ -53,6 +57,7 @@ class ElementColors(
messageHighlightedBackground: Color = this.messageHighlightedBackground,
quaternary: Color = this.quaternary,
quinary: Color = this.quinary,
textActionCritical: Color = this.textActionCritical,
isLight: Boolean = this.isLight,
) = ElementColors(
messageFromMeBackground = messageFromMeBackground,
@ -60,6 +65,7 @@ class ElementColors(
messageHighlightedBackground = messageHighlightedBackground,
quaternary = quaternary,
quinary = quinary,
textActionCritical = textActionCritical,
isLight = isLight,
)
@ -69,6 +75,7 @@ class ElementColors(
messageHighlightedBackground = other.messageHighlightedBackground
quaternary = other.quaternary
quinary = other.quinary
textActionCritical = other.textActionCritical
isLight = other.isLight
}
}

View file

@ -38,7 +38,7 @@ fun TopAppBar(
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(),
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
androidx.compose.material3.TopAppBar(

View file

@ -0,0 +1,22 @@
/*
* 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.libraries.matrix.api.config
object MatrixConfiguration {
const val matrixToPermalinkBaseUrl: String = "https://matrix.to/#/"
val clientPermalinkBaseUrl: String? = null
}

View file

@ -17,14 +17,13 @@
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import io.element.android.libraries.matrix.api.config.MatrixConfiguration
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
object MatrixToConverter {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].
@ -35,14 +34,15 @@ object MatrixToConverter {
*/
fun convert(uri: Uri): Uri? {
val uriString = uri.toString()
val baseUrl = MatrixConfiguration.matrixToPermalinkBaseUrl
return when {
// URL is already a matrix.to
uriString.startsWith(MATRIX_TO_URL_BASE) -> uri
uriString.startsWith(baseUrl) -> uri
// Web or client url
SUPPORTED_PATHS.any { it in uriString } -> {
val path = SUPPORTED_PATHS.first { it in uriString }
Uri.parse(MATRIX_TO_URL_BASE + uriString.substringAfter(path))
Uri.parse(baseUrl + uriString.substringAfter(path))
}
// URL is not supported
else -> null

View file

@ -0,0 +1,61 @@
/*
* 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.libraries.matrix.api.permalink
import io.element.android.libraries.matrix.api.config.MatrixConfiguration
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomId
object PermalinkBuilder {
private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.matrixToPermalinkBaseUrl).also {
var baseUrl = it
if (!baseUrl.endsWith("/")) {
baseUrl += "/"
}
if (!baseUrl.endsWith("/#/")) {
baseUrl += "/#/"
}
}
fun permalinkForRoomAlias(roomAlias: String): Result<String> {
return if (MatrixPatterns.isRoomAlias(roomAlias)) {
Result.success(permalinkForRoomAliasOrId(roomAlias))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomAlias)
}
}
fun permalinkForRoomId(roomId: RoomId): Result<String> {
return if (MatrixPatterns.isRoomId(roomId.value)) {
Result.success(permalinkForRoomAliasOrId(roomId.value))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomId)
}
}
private fun permalinkForRoomAliasOrId(value: String): String {
val id = escapeId(value)
return permalinkBaseUrl + id
}
private fun escapeId(value: String) = value.replace("/", "%2F")
}
sealed class PermalinkBuilderError : Throwable() {
object InvalidRoomAlias : PermalinkBuilderError()
object InvalidRoomId : PermalinkBuilderError()
}

View file

@ -27,8 +27,12 @@ interface MatrixRoom: Closeable {
val name: String?
val bestName: String
val displayName: String
val alias: String?
val alternativeAliases: List<String>
val topic: String?
val avatarUrl: String?
val members: List<RoomMember>
val isEncrypted: Boolean
fun syncUpdateFlow(): Flow<Long>

View file

@ -0,0 +1,31 @@
/*
* 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.libraries.matrix.api.room
data class RoomMember(
val userId: String,
val displayName: String?,
val avatarUrl: String?,
val membership: RoomMembershipState,
val isNameAmbiguous: Boolean,
val powerLevel: Long,
val normalizedPowerLevel: Long
)
enum class RoomMembershipState {
BAN, INVITE, JOIN, KNOCK, LEAVE
}

View file

@ -0,0 +1,46 @@
/*
* 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.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
object RoomMemberMapper {
fun map(roomMember: RustRoomMember): RoomMember =
RoomMember(
roomMember.userId,
roomMember.displayName,
roomMember.avatarUrl,
mapMembership(roomMember.membership),
roomMember.isNameAmbiguous,
roomMember.powerLevel,
roomMember.normalizedPowerLevel,
)
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
when (membershipState) {
RustMembershipState.BAN -> RoomMembershipState.BAN
RustMembershipState.INVITE -> RoomMembershipState.INVITE
RustMembershipState.JOIN -> RoomMembershipState.JOIN
RustMembershipState.KNOCK -> RoomMembershipState.KNOCK
RustMembershipState.LEAVE -> RoomMembershipState.LEAVE
}
}

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import kotlinx.coroutines.CoroutineScope
@ -94,6 +95,18 @@ class RustMatrixRoom(
return innerRoom.avatarUrl()
}
override val members: List<RoomMember>
get() = innerRoom.members().map(RoomMemberMapper::map)
override val isEncrypted: Boolean
get() = innerRoom.isEncrypted()
override val alias: String?
get() = innerRoom.canonicalAlias()
override val alternativeAliases: List<String>
get() = innerRoom.alternativeAliases()
override suspend fun fetchMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.fetchMembers()

View file

@ -35,7 +35,9 @@ import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
@ -145,7 +147,8 @@ class RustMatrixTimeline(
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
runCatching {
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null)
val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null)
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
listenerTokens += result.taskHandle
result.items
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@ -33,6 +34,10 @@ class FakeMatrixRoom(
override val displayName: String = "",
override val topic: String? = null,
override val avatarUrl: String? = null,
override val members: List<RoomMember> = emptyList(),
override val isEncrypted: Boolean = false,
override val alias: String? = null,
override val alternativeAliases: List<String> = emptyList(),
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
) : MatrixRoom {

View file

@ -17,7 +17,6 @@
<string name="action_disable">"Disable"</string>
<string name="action_done">"Done"</string>
<string name="action_edit">"Edit"</string>
<string name="action_edit_screenshot">"Edit screenshot"</string>
<string name="action_enable">"Enable"</string>
<string name="action_invite">"Invite"</string>
<string name="action_invite_friends_to_app">"Invite friends to %1$s"</string>
@ -39,6 +38,7 @@
<string name="action_save">"Save"</string>
<string name="action_search">"Search"</string>
<string name="action_send">"Send"</string>
<string name="action_share_link">"Share link"</string>
<string name="action_skip">"Skip"</string>
<string name="action_start">"Start"</string>
<string name="action_start_chat">"Start chat"</string>
@ -63,6 +63,7 @@
<string name="common_message_layout">"Message layout"</string>
<string name="common_message_removed">"Message removed"</string>
<string name="common_modern">"Modern"</string>
<string name="common_no_results">"No results"</string>
<string name="common_offline">"Offline"</string>
<string name="common_password">"Password"</string>
<string name="common_people">"People"</string>
@ -103,6 +104,7 @@
<string name="emoji_picker_category_symbols">"Symbols"</string>
<string name="error_failed_creating_the_permalink">"Failed creating the permalink"</string>
<string name="error_failed_loading_messages">"Failed loading messages"</string>
<string name="error_no_compatible_app_found">"No compatible app was found to handle this action."</string>
<string name="error_some_messages_have_not_been_sent">"Some messages have not been sent"</string>
<string name="error_unknown">"Sorry, an error occurred"</string>
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
@ -125,13 +127,13 @@
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>
<string name="screen_room_details_topic_title">"Topic"</string>
<string name="screen_room_member_details_block_alert_action">"Block"</string>
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
<string name="screen_room_member_details_block_user">"Block user"</string>
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
<string name="screen_room_member_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
<string name="settings_title_general">"General"</string>

View file

@ -124,7 +124,7 @@ data class MobileScreen(
* The screen shown when tapping the name of a room from the Room
* screen.
*/
RoomDetails,
RoomDetailss,
/**
* The screen that lists public rooms for you to discover.

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47606ad889ab978c5ccb3bcac3ead43a85cc2a83ce15e5520ac2e271d6b5f473
size 44415
oid sha256:4da4fd3b7c17b00bd17639a8ec5c6315abdf36b491c7da874f865bc386e622d8
size 45198

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92439979ab7bdb1a2c6ff1e98137a358ed788c3977c0ba7a1100eff41e22de79
size 44282
oid sha256:3a191ed88e984ca94c890b644fcee602539bf72a2e402be64caeb189db12bb80
size 44940

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:453dc174a87e3f43fe3bf7915634c3351b395ee05e488e37e37fd30c643c98fb
size 43160
oid sha256:9083a3f1146c5aa30c39ced0f535eddd38cafb2bad6c509717c10b74356a844b
size 43877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f2da5aa58c2c32994c11ae03bb3aa3ff4f310ea306eec056cc521eccf305772
size 42980
oid sha256:e4b03c928ec2cd4dbcc7fce37c0374b1e2b794d473d6ecc11bc5c33cc6953cca
size 43699

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eedd92351d9ffb9ebd519b88e0875196a10d7b7bf7db82e6d044cb6ec200970e
size 5195
oid sha256:d59b2a6b17f548953ce8521acae9e9f89b3c56245c4722c8c099af18453018d4
size 5285

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ceab55b3080020e622f1c8e44270907716fc777bd25d9d0573a9ea8dab643490
size 5043
oid sha256:5ee3a6bcf3ce1f24c143de680dd8aa326166d5fbc95fd8fecc4c0b15dfb6d7ce
size 5008

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:854525a9d9ca96fc53329faa4f816587da18663bb66c89a09870a01926370699
size 16394
oid sha256:4a36dadd693d2bf6c9ca2bfea6ec4e783248f8d3e5ca88d5cfde468b4bda116a
size 17055

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b1ad6738aa4046b8c46e2b581489c6114ae669e94142d64198423e28797c3843
size 15659
oid sha256:e5403566ef1d4d8cb25904fc74353197ad83e7ec452517a9cb696264a1609e32
size 15999

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2dba6b0eaa74d3c9dd3b62de76506198ce7c3aa6e5c7fca3d52bea3b56fb2ae3
size 9103
oid sha256:ed6b297eeed4301c11caa9b59b8e7bdcebd63ca89219dee2efba11936c681abe
size 9617

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b944575976d4971f76188d21740fd5b7c3a2937ab22b6e60688142e56296a8e
size 8797
oid sha256:73c4ab7efd89ec8d9ca035346bbca9b073112d31065495f148b6510ff1ee23e6
size 9203

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44fd34661da1a384fbb0df08f3dc7226bd723a33138d1ac8ad3ef50930c84e0c
size 7587
oid sha256:4665f288b04b8407f2ba25c272a56dd1d96b65523d76e08e6283156bf91f20ed
size 7818

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46bf34a9b246650a019c6983b17b76b11d4af6a0525e4ca87ca170d11c209144
size 7303
oid sha256:8b45fd891f3e3b38301be79159ec1e6d04114de58b2170a0649656780f64f5ea
size 7312

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26f29ecbb43cc403577c79747d21ef821810fe741df781541f0b8426be85f4cd
size 6145
oid sha256:3781254877c86737c2e042483f4a93e4608ce49994ec70a8c3e9e73124ea1388
size 8764

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfbb4b967d7b5a2bf5cb9f9b8ccffa72d87a77442d31dcce44e5b9f68c0fb259
size 5896
oid sha256:7505951ac4acf5cc0887af7bde9b8e659ae76643f5b6e639e24ee1df85448ece
size 7970

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df1b8d096d1b93065c7afb24873296c2ff19c91b8bbab662c42e7aa83b6ad9e0
size 19801
oid sha256:f250b20941e26ca156589a49f8e10ba34f1d29e56af60ab39dca65e83bc617fb
size 22411

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:576a65721a13bdbbb4bb5ff76d0727886e5c89931d1258c9d946aed6e8477256
size 19299
oid sha256:1f13b118db744d879d693a6145d86152cc57182ff706597f7bfc83f2826d9ddc
size 21566

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:274521bb420e73dcce6bca9d4328909d896998e4fdf3cebe79dd7c6c4873db45
oid sha256:047fbf34978c9b8d9b800b91e97bf95a94cedb31ddaf347d5e0000571ea491a9
size 4469

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e3a82fb55f7a5be9ddbb5a8146d4fe53ad6111e21b40912428fd95dde85346c
size 4470
oid sha256:234e0efe349185340b3efcaa181b31670a9dd55b57378e0b9c0b543f9ded70c2
size 4468

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc8026b2bf6fbc4afaf2641d2deba1b3ae27d582dbc30c70841356232aa8cbbe
size 117237
oid sha256:62b824f55b718ea805b97ce75810cd5c5de5498831aa8e5c8fa449121c48ea0c
size 117342

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c0847975608f68e32e497a043c373cd471b18dcefb1cfd0a6abbfb2e3963265
size 115619
oid sha256:da75446e5bc981fd21e95ff1b23ccccbf11bc4f9461982ed46b72c0182061abb
size 115627

View file

@ -57,6 +57,12 @@
"screen_roomlist_.*",
"session_verification_banner_.*"
]
},
{
"name": ":features:roomdetails:impl",
"includeRegex": [
"screen_room_details_.*"
]
}
]
}