From 0f29e8e8753514a4b0cd87be79103aa294c168f8 Mon Sep 17 00:00:00 2001 From: Jenna Vassar Date: Wed, 30 Sep 2026 07:18:13 -0700 Subject: [PATCH] Add an extension for room details sections (#7781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add an extension for room details sections Provide a no-op rendering hook for build-specific room details sections and shared room audio labels. Keep the default room details behavior unchanged. * Cover the default room details extension Verify that the no-op extension renders no content and leaves surrounding UI unchanged through recomposition. Cover its constructor and both generated render paths. * Refine the room details extension and link the enterprise picker Expose ColumnScope to contributed room details sections and use room-details string keys. Pin the enterprise submodule to the reviewed picker implementation so integrated builds include it behind the default-off flag. * Make the room details extension a functional interface Declare the single composable rendering contract as a fun interface to satisfy the Sonar check while preserving its named dependency injection binding. * Update localazy strings for the audio section in room details * Bump enterprise commit --------- Co-authored-by: Jorge Martín --- enterprise | 2 +- features/roomdetails/api/build.gradle.kts | 2 +- .../roomdetails/api/RoomDetailsExtension.kt | 18 +++++++ .../impl/NoopRoomDetailsExtension.kt | 21 ++++++++ .../roomdetails/impl/RoomDetailsNode.kt | 3 ++ .../roomdetails/impl/RoomDetailsView.kt | 4 ++ .../impl/src/main/res/values/localazy.xml | 3 ++ .../impl/NoopRoomDetailsExtensionTest.kt | 50 +++++++++++++++++++ .../roomdetails/impl/RoomDetailsViewTest.kt | 12 +++++ 9 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsExtension.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtension.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtensionTest.kt diff --git a/enterprise b/enterprise index 6b422f4c8e..bcffe42d5c 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 6b422f4c8e9e9e5557056e8a861fe3bfd9338f24 +Subproject commit bcffe42d5c2ca97b438f2ad157116067b3819995 diff --git a/features/roomdetails/api/build.gradle.kts b/features/roomdetails/api/build.gradle.kts index ce393840f8..e6f6dd4f5e 100644 --- a/features/roomdetails/api/build.gradle.kts +++ b/features/roomdetails/api/build.gradle.kts @@ -7,7 +7,7 @@ */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") id("kotlin-parcelize") } diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsExtension.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsExtension.kt new file mode 100644 index 0000000000..25f8f9e1f7 --- /dev/null +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsExtension.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomdetails.api + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** Optional room-details sections contributed by a build-specific implementation. */ +fun interface RoomDetailsExtension { + @Composable + fun ColumnScope.Render(modifier: Modifier) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtension.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtension.kt new file mode 100644 index 0000000000..59178e0d09 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtension.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomdetails.impl + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.roomdetails.api.RoomDetailsExtension +import io.element.android.libraries.di.RoomScope + +@ContributesBinding(RoomScope::class) +class NoopRoomDetailsExtension : RoomDetailsExtension { + @Composable + override fun ColumnScope.Render(modifier: Modifier) = Unit +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 7f22c7e111..dd1c10ae82 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -24,6 +24,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode import io.element.android.features.leaveroom.api.LeaveRoomRenderer +import io.element.android.features.roomdetails.api.RoomDetailsExtension import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback @@ -46,6 +47,7 @@ class RoomDetailsNode( private val room: BaseRoom, private val analyticsService: AnalyticsService, private val leaveRoomRenderer: LeaveRoomRenderer, + private val roomDetailsExtension: RoomDetailsExtension, ) : Node(buildContext, plugins = plugins), RoomDetailsNavigator { interface Callback : Plugin { fun navigateBack() @@ -120,6 +122,7 @@ class RoomDetailsNode( RoomDetailsView( state = state, + additionalSections = { with(roomDetailsExtension) { Render(Modifier) } }, modifier = modifier, goBack = ::navigateUp, onActionClick = ::onActionClick, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 99aab2f957..2d76ca5e78 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -123,6 +124,7 @@ fun RoomDetailsView( onProfileClick: (UserId) -> Unit, onReportRoomClick: () -> Unit, modifier: Modifier = Modifier, + additionalSections: @Composable ColumnScope.() -> Unit = {}, leaveRoomView: @Composable () -> Unit, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -198,6 +200,8 @@ fun RoomDetailsView( ) } + additionalSections() + PreferenceCategory { if (state.hasNewContent) { ListItem( diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 25473238fd..4ee26bb578 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -42,6 +42,9 @@ "You have unsaved changes." "Save changes?" "Add topic" + "Push to talk" + "Room Audio" + "Video calls" "Encrypted" "Not encrypted" "Public room" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtensionTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtensionTest.kt new file mode 100644 index 0000000000..9d25a6e78e --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/NoopRoomDetailsExtensionTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalTestApi::class) + +package io.element.android.features.roomdetails.impl + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class NoopRoomDetailsExtensionTest : RobolectricTest() { + @Test + fun `default extension renders no additional content`() = runAndroidComposeUiTest { + val extension = NoopRoomDetailsExtension() + setContent { Column { with(extension) { Render(Modifier) } } } + onRoot().onChildren().assertCountEquals(0) + } + + @Test + fun `default extension leaves surrounding content unchanged during updates`() = runAndroidComposeUiTest { + val extension = NoopRoomDetailsExtension() + val text = mutableStateOf("Before") + setContent { + Column { + BasicText(text.value) + with(extension) { Render(Modifier) } + } + } + onNodeWithText("Before").assertExists() + onRoot().onChildren().assertCountEquals(1) + runOnIdle { text.value = "After" } + onNodeWithText("After").assertExists() + onRoot().onChildren().assertCountEquals(1) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index 767ddd5dd9..53cb66da15 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -11,16 +11,20 @@ package io.element.android.features.roomdetails.impl import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.shared.aUserProfileState +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -41,6 +45,12 @@ import org.junit.Test import org.robolectric.annotation.Config class RoomDetailsViewTest : RobolectricTest() { + @Test + fun `extension section is rendered`() = runAndroidComposeUiTest { + setRoomDetailView(additionalSections = { Text("Extra section") }) + onNodeWithText("Extra section").assertExists() + } + @Test fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> @@ -381,6 +391,7 @@ private fun AndroidComposeUiTest.setRoomDetailView( onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onReportRoomClick: () -> Unit = EnsureNeverCalled(), + additionalSections: @Composable ColumnScope.() -> Unit = {}, ) { setContent { RoomDetailsView( @@ -402,6 +413,7 @@ private fun AndroidComposeUiTest.setRoomDetailView( onProfileClick = onProfileClick, onReportRoomClick = onReportRoomClick, leaveRoomView = {}, + additionalSections = additionalSections, ) } }