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,
)
}
}