From bf1462f9b1f17583ed4ef1849a99568705c8b755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 15 May 2026 14:57:39 +0200 Subject: [PATCH] WIP --- build.gradle.kts | 5 +++-- .../home/impl/components/HomeTopBar.kt | 3 ++- .../impl/spacefilters/SpaceFiltersView.kt | 9 +++++---- .../impl/screens/error/ErrorView.kt | 2 +- .../impl/common/ui/LocationPinMarkers.kt | 3 ++- .../location/impl/share/ShareLocationView.kt | 4 +++- .../location/impl/show/ShowLocationView.kt | 3 ++- .../screens/qrcode/error/QrCodeErrorView.kt | 2 +- .../features/messages/impl/MessagesView.kt | 3 ++- .../impl/threads/list/ThreadsListView.kt | 4 +++- .../event/TimelineItemLocationContent.kt | 7 +++++-- .../impl/advanced/AdvancedSettingsView.kt | 4 +++- gradle/libs.versions.toml | 1 + .../appyx/SafeChildrenTransitionScope.kt | 1 + .../theme/components/ModalBottomSheet.kt | 7 +++++-- .../matrix/ui/components/AvatarPickerView.kt | 1 + .../libraries/qrcode/QrCodeCameraView.kt | 2 +- tools/detekt/detekt.yml | 19 ++++++++++++++++++- 18 files changed, 59 insertions(+), 21 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 474b868eda..1aaccbe2f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,8 @@ tasks.register("clean").configure { delete(rootProject.layout.buildDirectory) } -private val ktLintVersion = the().versions.ktlint.get() +private val catalog = the() +private val ktLintVersion = catalog.versions.ktlint.get() allprojects { // Detekt @@ -46,7 +47,7 @@ allprojects { config.from(files("$rootDir/tools/detekt/detekt.yml")) } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.5.8") + detektPlugins(catalog.detekt.compose.rules) detektPlugins(project(":tests:detekt-rules")) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index 5f55beaf60..a563520e09 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -10,6 +10,7 @@ package io.element.android.features.home.impl.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -160,7 +161,7 @@ fun HomeTopBar( } @Composable -private fun RoomListMenuItems( +private fun RowScope.RoomListMenuItems( onToggleSearch: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, canReportBug: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 1f0d6ff7d9..8a270f1b85 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -93,7 +94,7 @@ fun SpaceFiltersView( SpaceFiltersBottomSheetContent( filters = state.visibleFilters, searchQuery = state.searchQuery, - onFilterSelected = { filter -> + onSelectFilter = { filter -> state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } ) @@ -105,9 +106,9 @@ fun SpaceFiltersView( @Composable private fun SpaceFiltersBottomSheetContent( - filters: List, + filters: ImmutableList, searchQuery: TextFieldState, - onFilterSelected: (SpaceServiceFilter) -> Unit, + onSelectFilter: (SpaceServiceFilter) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -133,7 +134,7 @@ private fun SpaceFiltersBottomSheetContent( items(filters) { filter -> SpaceFilterItem( filter = filter, - onClick = { onFilterSelected(filter) } + onClick = { onSelectFilter(filter) } ) } } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt index 4db2aa9ad5..cc94951dbd 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt @@ -147,7 +147,7 @@ private fun DoneButton( } @Composable -private fun Buttons( +private fun ColumnScope.Buttons( onRetry: () -> Unit, onCancel: () -> Unit, ) { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt index 762a719cf4..e30dfd53de 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt @@ -14,6 +14,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.features.location.api.Location import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.components.rememberLocationPinBitmap +import kotlinx.collections.immutable.ImmutableList import kotlinx.serialization.json.JsonPrimitive import org.maplibre.compose.expressions.dsl.and import org.maplibre.compose.expressions.dsl.asString @@ -66,7 +67,7 @@ data class LocationMarkerData( */ @Composable fun LocationPinMarkers( - markers: List, + markers: ImmutableList, onMarkerClick: ((LocationMarkerData) -> Unit)? = null, onClusterClick: ((Position) -> Unit)? = null, ) { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 985c6c8bdc..52400cd49d 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -180,6 +181,7 @@ private fun StartLiveLocationActionView( onActionSuccess: () -> Unit, modifier: Modifier = Modifier, ) { + val updatedOnActionSuccess by rememberUpdatedState(onActionSuccess) Box(modifier = modifier) { val asyncIndicatorState = rememberAsyncIndicatorState() AsyncIndicatorHost(state = asyncIndicatorState) @@ -202,7 +204,7 @@ private fun StartLiveLocationActionView( } } is AsyncAction.Success -> { - LaunchedEffect(action) { onActionSuccess() } + LaunchedEffect(action) { updatedOnActionSuccess() } } else -> Unit } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 80ffaabedd..231bc01b12 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraPosition @@ -172,7 +173,7 @@ fun ShowLocationView( trackUserLocation = state.isTrackMyLocation ) val markers = remember(state.locationShares) { - state.locationShares.map { it.toMarkerData() } + state.locationShares.map { it.toMarkerData() }.toImmutableList() } LocationPinMarkers(markers) }, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt index a6d9da3de2..6b5da8f300 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt @@ -125,7 +125,7 @@ private fun Content(errorScreenType: QrCodeErrorScreenType) { } @Composable -private fun Buttons( +private fun ColumnScope.Buttons( onRetry: () -> Unit, onCancel: () -> Unit, ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 3e6f14e805..b299ea42a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets @@ -412,7 +413,7 @@ fun MessagesView( } @Composable -internal fun MessagesMenuActions( +internal fun RowScope.MessagesMenuActions( displayThreads: Boolean, roomCallState: RoomCallState, onJoinCallClick: (isAudioCall: Boolean) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt index 5e26d849a5..7d741eb3ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -157,6 +158,7 @@ private fun ScrollHelper( listState: LazyListState, onPaginate: () -> Unit, ) { + val updatedOnPaginate by rememberUpdatedState(onPaginate) val lastVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex + listState.layoutInfo.visibleItemsInfo.size - 1 } } @@ -168,7 +170,7 @@ private fun ScrollHelper( } LaunchedEffect(needsPagination, lastVisibleItemIndex) { if (needsPagination) { - onPaginate() + updatedOnPaginate() delay(400L) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContent.kt index 1003d03b4b..12368530a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContent.kt @@ -9,7 +9,9 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberUpdatedState import io.element.android.features.location.api.Location import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -103,8 +105,9 @@ private fun rememberIsLiveLocationActive( mode: TimelineItemLocationContent.Mode.Live, currentTimeMillis: () -> Long, ): Boolean { + val updatedCurrentTimeMillis by rememberUpdatedState(currentTimeMillis) fun TimelineItemLocationContent.Mode.Live.isActive(): Boolean { - return isActive && endTimestamp > currentTimeMillis() + return isActive && endTimestamp > updatedCurrentTimeMillis() } return produceState( initialValue = mode.isActive(), @@ -112,7 +115,7 @@ private fun rememberIsLiveLocationActive( key2 = mode.isActive, ) { if (mode.isActive) { - val remainingMillis = mode.endTimestamp - currentTimeMillis() + val remainingMillis = mode.endTimestamp - updatedCurrentTimeMillis() delay(remainingMillis) } value = false diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index af28e3443f..7b5d0c5a9a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -343,6 +344,7 @@ private fun LiveLocationUpdatesSection( onOpenAppPermissionsClick: () -> Unit, modifier: Modifier = Modifier, ) { + val updatedOnValueSaved by rememberUpdatedState(onValueSaved) PreferenceCategory( modifier = modifier, showTopDivider = true, @@ -382,7 +384,7 @@ private fun LiveLocationUpdatesSection( value = sliderValue.toFloat(), onValueChange = { sliderValue = it.roundToInt() }, onValueChangeFinish = { - onValueSaved(sliderValue) + updatedOnValueSaved(sliderValue) }, valueRange = valueRange, colors = SliderDefaults.colors( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b526c0ab69..524ec54c73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref compose_compiler_plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } # https://developer.android.com/studio/write/java8-support#library-desugaring-versions android_desugar = "com.android.tools:desugar_jdk_libs:2.1.5" +detekt_compose_rules = "io.nlopez.compose.rules:detekt:0.4.28" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } metro_gradle_plugin = { module = "dev.zacsweers.metro:gradle-plugin", version.ref = "metro" } kotlin_compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin" } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt index 00e49d7bee..b90e95beca 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt @@ -186,6 +186,7 @@ class SafeChildrenTransitionScope( } @OptIn(ExperimentalSharedTransitionApi::class) +@Suppress("ContentSlotReused") @Composable inline fun ParentNode.SafeChildren( navModel: NavModel, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index 689cba727f..ec5f7ce456 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -24,6 +24,8 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.contentColorFor import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -88,14 +90,15 @@ fun ModalBottomSheet( dragHandle = dragHandle, contentWindowInsets = contentWindowInsets, ) { + val movableContent = remember { movableContentOf { content() } } if (scrollable) { Column( modifier = Modifier.verticalScroll(rememberScrollState()), ) { - content() + movableContent() } } else { - content() + movableContent() } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt index 4d3ba4dc6b..81822a04cc 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt @@ -184,6 +184,7 @@ private fun PickButton( } } +@Suppress("ModifierClickableOrder") @Composable private fun BoxScope.OverlayEditButton( onClick: () -> Unit, diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index 18bd1cff10..77648c0f0b 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -114,7 +114,7 @@ fun QrCodeCameraView( Box(modifier.clipToBounds()) { if (LocalInspectionMode.current) { Box( - modifier = modifier + modifier = Modifier .background(color = ElementTheme.colors.bgSubtlePrimary), contentAlignment = Alignment.Center, ) { diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 552b46755e..cc99ecab5a 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -150,7 +150,7 @@ exceptions: TooGenericExceptionCaught: active: false SwallowedException: - active: true + active: false ThrowingExceptionsWithoutMessageOrCause: active: true TooGenericExceptionThrown: @@ -224,12 +224,27 @@ comments: Compose: CompositionLocalAllowlist: active: true + # You can optionally define a list of CompositionLocals that are allowed here + allowedCompositionLocals: + - LocalCompoundColors + - LocalSnackbarDispatcher + - LocalCameraPositionState + - LocalMediaItemPresenterFactories + - LocalTimelineItemPresenterFactories + - LocalRoomMemberProfilesCache + - LocalMentionSpanUpdater + - LocalAnalyticsService + - LocalBuildMeta + - LocalUiTestMode + - LocalSdkIntVersionProvider CompositionLocalNaming: active: true ContentEmitterReturningValues: active: true # You can optionally add your own composables here # contentEmitters: MyComposable,MyOtherComposable + ModifierComposable: + active: true ModifierMissing: active: true ModifierReused: @@ -252,6 +267,8 @@ Compose: active: true PreviewPublic: active: true + # You can optionally disable that only previews with @PreviewParameter are flagged + previewPublicOnlyIfParams: false RememberMissing: active: true UnstableCollections: