Merge pull request #5503 from element-hq/feature/bma/immutableCleanup

Do some cleanup on our immutable annotation usage
This commit is contained in:
Benoit Marty
2025-10-10 17:42:26 +02:00
committed by GitHub
73 changed files with 178 additions and 107 deletions

View File

@@ -103,6 +103,39 @@ jobs:
path: | path: |
**/build/reports/**/*.* **/build/reports/**/*.*
compose:
name: Compose tests
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
run: ./tools/compose/check_stability.sh
lint: lint:
name: Android lint check name: Android lint check
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -7,12 +7,10 @@
package io.element.android.appnav.root package io.element.android.appnav.root
import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
@Immutable
data class RootState( data class RootState(
val rageshakeDetectionState: RageshakeDetectionState, val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState, val crashDetectionState: CrashDetectionState,

View File

@@ -8,10 +8,13 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class CreateRoomConfig( data class CreateRoomConfig(
val roomName: String? = null, val roomName: String? = null,
val topic: String? = null, val topic: String? = null,

View File

@@ -7,7 +7,6 @@
package io.element.android.features.home.impl package io.element.android.features.home.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
@@ -15,7 +14,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class HomeState( data class HomeState(
/** /**
* The current user of this session, in case of multiple accounts, will contains 3 items, with the * The current user of this session, in case of multiple accounts, will contains 3 items, with the

View File

@@ -19,7 +19,6 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@Immutable
data class RoomListState( data class RoomListState(
val contextMenu: ContextMenu, val contextMenu: ContextMenu,
val declineInviteMenu: DeclineInviteMenu, val declineInviteMenu: DeclineInviteMenu,

View File

@@ -16,7 +16,9 @@ import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -28,7 +30,10 @@ class HomeSpacesPresenter(
@Composable @Composable
override fun present(): HomeSpacesState { override fun present(): HomeSpacesState {
val hideInvitesAvatar by client.rememberHideInvitesAvatar() val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList()) val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
}.collectAsState(persistentListOf())
val seenSpaceInvites by remember { val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() } seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf()) }.collectAsState(persistentSetOf())

View File

@@ -9,11 +9,12 @@ package io.element.android.features.home.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState( data class HomeSpacesState(
val space: CurrentSpace, val space: CurrentSpace,
val spaceRooms: List<SpaceRoom>, val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>, val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean, val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit, val eventSink: (HomeSpacesEvents) -> Unit,

View File

@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> { open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
@@ -39,7 +40,7 @@ internal fun aHomeSpacesState(
eventSink: (HomeSpacesEvents) -> Unit = {}, eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState( ) = HomeSpacesState(
space = space, space = space,
spaceRooms = spaceRooms, spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
eventSink = eventSink, eventSink = eventSink,

View File

@@ -24,7 +24,6 @@ import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500 internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@Immutable
data class JoinRoomState( data class JoinRoomState(
val roomIdOrAlias: RoomIdOrAlias, val roomIdOrAlias: RoomIdOrAlias,
val contentState: ContentState, val contentState: ContentState,

View File

@@ -7,6 +7,9 @@
package io.element.android.features.leaveroom.api package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Immutable
@Immutable
interface LeaveRoomState { interface LeaveRoomState {
val eventSink: (LeaveRoomEvent) -> Unit val eventSink: (LeaveRoomEvent) -> Unit
} }

View File

@@ -17,6 +17,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject @Inject
class ChangeAccountProviderPresenter( class ChangeAccountProviderPresenter(
@@ -39,6 +40,7 @@ class ChangeAccountProviderPresenter(
isValid = true, isValid = true,
) )
} }
.toImmutableList()
} }
val canSearchForAccountProviders = remember { val canSearchForAccountProviders = remember {

View File

@@ -9,10 +9,11 @@ package io.element.android.features.login.impl.screens.changeaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState( data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: ImmutableList<AccountProvider>,
val canSearchForAccountProviders: Boolean, val canSearchForAccountProviders: Boolean,
val changeServerState: ChangeServerState, val changeServerState: ChangeServerState,
) )

View File

@@ -12,6 +12,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import kotlinx.collections.immutable.toImmutableList
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> { open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState> override val values: Sequence<ChangeAccountProviderState>
@@ -29,7 +30,7 @@ fun aChangeAccountProviderState(
canSearchForAccountProviders: Boolean = true, canSearchForAccountProviders: Boolean = true,
changeServerState: ChangeServerState = aChangeServerState(), changeServerState: ChangeServerState = aChangeServerState(),
) = ChangeAccountProviderState( ) = ChangeAccountProviderState(
accountProviders = accountProviders, accountProviders = accountProviders.toImmutableList(),
canSearchForAccountProviders = canSearchForAccountProviders, canSearchForAccountProviders = canSearchForAccountProviders,
changeServerState = changeServerState, changeServerState = changeServerState,
) )

View File

@@ -21,6 +21,7 @@ import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList
@Inject @Inject
class ChooseAccountProviderPresenter( class ChooseAccountProviderPresenter(
@@ -69,6 +70,7 @@ class ChooseAccountProviderPresenter(
isValid = true, isValid = true,
) )
} }
.toImmutableList()
} }
return ChooseAccountProviderState( return ChooseAccountProviderState(

View File

@@ -10,10 +10,11 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChooseAccountProviderState( data class ChooseAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: ImmutableList<AccountProvider>,
val selectedAccountProvider: AccountProvider?, val selectedAccountProvider: AccountProvider?,
val loginMode: AsyncData<LoginMode>, val loginMode: AsyncData<LoginMode>,
val eventSink: (ChooseAccountProviderEvents) -> Unit, val eventSink: (ChooseAccountProviderEvents) -> Unit,

View File

@@ -12,6 +12,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableList
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> { open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
private val server1 = anAccountProvider( private val server1 = anAccountProvider(
@@ -70,7 +71,7 @@ fun aChooseAccountProviderState(
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (ChooseAccountProviderEvents) -> Unit = {}, eventSink: (ChooseAccountProviderEvents) -> Unit = {},
) = ChooseAccountProviderState( ) = ChooseAccountProviderState(
accountProviders = accountProviders, accountProviders = accountProviders.toImmutableList(),
selectedAccountProvider = selectedAccountProvider, selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode, loginMode = loginMode,
eventSink = eventSink, eventSink = eventSink,

View File

@@ -7,7 +7,6 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
@@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class MessagesState( data class MessagesState(
val roomId: RoomId, val roomId: RoomId,
val roomName: String?, val roomName: String?,

View File

@@ -13,7 +13,6 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class ActionListState( data class ActionListState(
val target: Target, val target: Target,
val eventSink: (ActionListEvents) -> Unit, val eventSink: (ActionListEvents) -> Unit,

View File

@@ -9,11 +9,9 @@ package io.element.android.features.messages.impl.actionlist.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Immutable
enum class TimelineItemAction( enum class TimelineItemAction(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,

View File

@@ -7,9 +7,6 @@
package io.element.android.features.messages.impl.attachments.preview package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.runtime.Immutable
@Immutable
sealed interface AttachmentsPreviewEvents { sealed interface AttachmentsPreviewEvents {
data object SendAttachment : AttachmentsPreviewEvents data object SendAttachment : AttachmentsPreviewEvents
data object CancelAndDismiss : AttachmentsPreviewEvents data object CancelAndDismiss : AttachmentsPreviewEvents

View File

@@ -8,12 +8,10 @@
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.Suggestion
@Immutable
sealed interface MessageComposerEvents { sealed interface MessageComposerEvents {
data object ToggleFullScreenState : MessageComposerEvents data object ToggleFullScreenState : MessageComposerEvents
data object SendMessage : MessageComposerEvents data object SendMessage : MessageComposerEvents
@@ -30,6 +28,7 @@ sealed interface MessageComposerEvents {
data object Location : PickAttachmentSource data object Location : PickAttachmentSource
data object Poll : PickAttachmentSource data object Poll : PickAttachmentSource
} }
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
data class Error(val error: Throwable) : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents

View File

@@ -105,7 +105,7 @@ class PinnedMessagesListPresenter(
// We do not care about the call state here. // We do not care about the call state here.
roomCallState = aStandByCallState(), roomCallState = aStandByCallState(),
// don't compute this value or the pin icon will be shown // don't compute this value or the pin icon will be shown
pinnedEventIds = emptyList(), pinnedEventIds = persistentListOf(),
typingNotificationState = TypingNotificationState( typingNotificationState = TypingNotificationState(
renderTypingNotifications = false, renderTypingNotifications = false,
typingMembers = persistentListOf(), typingMembers = persistentListOf(),

View File

@@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration import kotlin.time.Duration
@Immutable
data class TimelineState( data class TimelineState(
val timelineItems: ImmutableList<TimelineItem>, val timelineItems: ImmutableList<TimelineItem>,
val timelineRoomInfo: TimelineRoomInfo, val timelineRoomInfo: TimelineRoomInfo,
@@ -72,14 +71,13 @@ sealed interface FocusRequestState {
} }
} }
@Immutable
data class TimelineRoomInfo( data class TimelineRoomInfo(
val isDm: Boolean, val isDm: Boolean,
val name: String?, val name: String?,
val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean, val userHasPermissionToSendReaction: Boolean,
val roomCallState: RoomCallState, val roomCallState: RoomCallState,
val pinnedEventIds: List<EventId>, val pinnedEventIds: ImmutableList<EventId>,
val typingNotificationState: TypingNotificationState, val typingNotificationState: TypingNotificationState,
val predecessorRoom: PredecessorRoom?, val predecessorRoom: PredecessorRoom?,
) )

View File

@@ -259,7 +259,7 @@ internal fun aTimelineRoomInfo(
userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true, userHasPermissionToSendReaction = true,
roomCallState = aStandByCallState(), roomCallState = aStandByCallState(),
pinnedEventIds = pinnedEventIds, pinnedEventIds = pinnedEventIds.toImmutableList(),
typingNotificationState = typingNotificationState, typingNotificationState = typingNotificationState,
predecessorRoom = predecessorRoom, predecessorRoom = predecessorRoom,
) )

View File

@@ -8,11 +8,14 @@
package io.element.android.features.messages.impl.timeline.components.customreaction.picker package io.element.android.features.messages.impl.timeline.components.customreaction.picker
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// Emoji is unstable (because from an external library?), so we annotate with @Immutable
@Immutable
data class EmojiPickerState( data class EmojiPickerState(
val categories: ImmutableList<EmojiCategory>, val categories: ImmutableList<EmojiCategory>,
val allEmojis: ImmutableList<Emoji>, val allEmojis: ImmutableList<Emoji>,

View File

@@ -7,10 +7,8 @@
package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
@Immutable
data class ReadReceiptBottomSheetState( data class ReadReceiptBottomSheetState(
val selectedEvent: TimelineItem.Event?, val selectedEvent: TimelineItem.Event?,
val eventSink: (ReadReceiptBottomSheetEvents) -> Unit, val eventSink: (ReadReceiptBottomSheetEvents) -> Unit,

View File

@@ -21,7 +21,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
import io.element.android.libraries.architecture.map
import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.dateformatter.api.DateFormatterMode
@@ -37,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.util.Date
@AssistedInject @AssistedInject
class TimelineItemEventFactory( class TimelineItemEventFactory(
@@ -146,10 +144,9 @@ class TimelineItemEventFactory(
senders = reaction.senders senders = reaction.senders
.sortedByDescending { it.timestamp } .sortedByDescending { it.timestamp }
.map { .map {
val date = Date(it.timestamp)
AggregatedReactionSender( AggregatedReactionSender(
senderId = it.senderId, senderId = it.senderId,
timestamp = date, timestamp = it.timestamp,
sentTime = dateFormatter.format( sentTime = dateFormatter.format(
it.timestamp, it.timestamp,
DateFormatterMode.TimeOrDate, DateFormatterMode.TimeOrDate,

View File

@@ -33,13 +33,14 @@ fun anAggregatedReaction(
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US).apply { val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
} }
val date = Date(1_689_061_264L) val timestamp = 1_689_061_264L
val date = Date(timestamp)
val senders = buildList { val senders = buildList {
repeat(count) { index -> repeat(count) { index ->
add( add(
AggregatedReactionSender( AggregatedReactionSender(
senderId = if (isHighlighted && index == 0) userId else UserId("@user$index:server.org"), senderId = if (isHighlighted && index == 0) userId else UserId("@user$index:server.org"),
timestamp = date, timestamp = timestamp,
sentTime = timeFormatter.format(date), sentTime = timeFormatter.format(date),
) )
) )

View File

@@ -7,15 +7,12 @@
package io.element.android.features.messages.impl.timeline.model package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import java.util.Date
@Immutable
data class AggregatedReactionSender( data class AggregatedReactionSender(
val senderId: UserId, val senderId: UserId,
val timestamp: Date, val timestamp: Long,
val sentTime: String, val sentTime: String,
val user: MatrixUser? = null val user: MatrixUser? = null
) )

View File

@@ -57,13 +57,11 @@ sealed interface TimelineItem {
is GroupedEvents -> "groupedEvent" is GroupedEvents -> "groupedEvent"
} }
@Immutable
data class Virtual( data class Virtual(
val id: UniqueId, val id: UniqueId,
val model: TimelineItemVirtualModel val model: TimelineItemVirtualModel
) : TimelineItem ) : TimelineItem
@Immutable
data class Event( data class Event(
val id: UniqueId, val id: UniqueId,
// Note: eventId can be null when the event is a local echo // Note: eventId can be null when the event is a local echo
@@ -124,7 +122,6 @@ sealed interface TimelineItem {
val sendhandle: SendHandle? get() = sendHandleProvider() val sendhandle: SendHandle? get() = sendHandleProvider()
} }
@Immutable
data class GroupedEvents( data class GroupedEvents(
val id: UniqueId, val id: UniqueId,
val events: ImmutableList<Event>, val events: ImmutableList<Event>,

View File

@@ -7,6 +7,8 @@
package io.element.android.features.preferences.impl.about package io.element.android.features.preferences.impl.about
import kotlinx.collections.immutable.ImmutableList
data class AboutState( data class AboutState(
val elementLegals: List<ElementLegal>, val elementLegals: ImmutableList<ElementLegal>,
) )

View File

@@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.about package io.element.android.features.preferences.impl.about
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import kotlinx.collections.immutable.toImmutableList
open class AboutStateProvider : PreviewParameterProvider<AboutState> { open class AboutStateProvider : PreviewParameterProvider<AboutState> {
override val values: Sequence<AboutState> override val values: Sequence<AboutState>
@@ -19,5 +20,5 @@ open class AboutStateProvider : PreviewParameterProvider<AboutState> {
fun anAboutState( fun anAboutState(
elementLegals: List<ElementLegal> = getAllLegals(), elementLegals: List<ElementLegal> = getAllLegals(),
) = AboutState( ) = AboutState(
elementLegals = elementLegals, elementLegals = elementLegals.toImmutableList(),
) )

View File

@@ -10,6 +10,8 @@ package io.element.android.features.preferences.impl.about
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.element.android.features.preferences.impl.BuildConfig import io.element.android.features.preferences.impl.BuildConfig
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT
private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE
@@ -24,8 +26,8 @@ sealed class ElementLegal(
data object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PRIVACY_URL) data object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PRIVACY_URL)
} }
fun getAllLegals(): List<ElementLegal> { fun getAllLegals(): ImmutableList<ElementLegal> {
return listOf( return persistentListOf(
ElementLegal.Copyright, ElementLegal.Copyright,
ElementLegal.AcceptableUsePolicy, ElementLegal.AcceptableUsePolicy,
ElementLegal.PrivacyPolicy, ElementLegal.PrivacyPolicy,

View File

@@ -7,7 +7,6 @@
package io.element.android.features.preferences.impl.notifications package io.element.android.features.preferences.impl.notifications
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
@@ -15,7 +14,6 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class NotificationSettingsState( data class NotificationSettingsState(
val matrixSettings: MatrixSettings, val matrixSettings: MatrixSettings,
val appSettings: AppSettings, val appSettings: AppSettings,

View File

@@ -8,12 +8,15 @@
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class EditUserProfileState( data class EditUserProfileState(
val userId: UserId, val userId: UserId,
val displayName: String, val displayName: String,

View File

@@ -7,9 +7,6 @@
package io.element.android.features.rageshake.api.crash package io.element.android.features.rageshake.api.crash
import androidx.compose.runtime.Immutable
@Immutable
data class CrashDetectionState( data class CrashDetectionState(
val appName: String, val appName: String,
val crashDetected: Boolean, val crashDetected: Boolean,

View File

@@ -7,10 +7,8 @@
package io.element.android.features.rageshake.api.detection package io.element.android.features.rageshake.api.detection
import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
@Immutable
data class RageshakeDetectionState( data class RageshakeDetectionState(
val takeScreenshot: Boolean, val takeScreenshot: Boolean,
val showDialog: Boolean, val showDialog: Boolean,

View File

@@ -7,12 +7,10 @@
package io.element.android.features.roomaliasresolver.impl package io.element.android.features.roomaliasresolver.impl
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@Immutable
data class RoomAliasResolverState( data class RoomAliasResolverState(
val roomAlias: RoomAlias, val roomAlias: RoomAlias,
val resolveState: AsyncData<ResolvedRoomAlias>, val resolveState: AsyncData<ResolvedRoomAlias>,

View File

@@ -8,12 +8,15 @@
package io.element.android.features.roomdetails.impl.edit package io.element.android.features.roomdetails.impl.edit
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class RoomDetailsEditState( data class RoomDetailsEditState(
val roomId: RoomId, val roomId: RoomId,
/** The raw room name (i.e. the room name from the state event `m.room.name`), not the display name. */ /** The raw room name (i.e. the room name from the state event `m.room.name`), not the display name. */

View File

@@ -6,7 +6,7 @@
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
id("kotlin-parcelize") id("kotlin-parcelize")
} }

View File

@@ -8,7 +8,6 @@
package io.element.android.features.roomdirectory.api package io.element.android.features.roomdirectory.api
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@@ -17,7 +16,6 @@ import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@Immutable
data class RoomDescription( data class RoomDescription(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,

View File

@@ -42,14 +42,14 @@ class RoomDirectoryPresenterTest {
@Test @Test
fun `present - room directory list emits empty state`() = runTest { fun `present - room directory list emits empty state`() = runTest {
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.State>(replay = 1) val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.SearchResult>(replay = 1)
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow)
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
presenter.test { presenter.test {
skipItems(1) skipItems(1)
directoryListStateFlow.emit( directoryListStateFlow.emit(
RoomDirectoryList.State(false, emptyList()) RoomDirectoryList.SearchResult(false, emptyList())
) )
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.displayEmptyState).isTrue() assertThat(state.displayEmptyState).isTrue()
@@ -60,14 +60,14 @@ class RoomDirectoryPresenterTest {
@Test @Test
fun `present - room directory list emits non-empty state`() = runTest { fun `present - room directory list emits non-empty state`() = runTest {
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.State>(replay = 1) val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.SearchResult>(replay = 1)
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow)
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
presenter.test { presenter.test {
skipItems(1) skipItems(1)
directoryListStateFlow.emit( directoryListStateFlow.emit(
RoomDirectoryList.State( RoomDirectoryList.SearchResult(
hasMoreToLoad = true, hasMoreToLoad = true,
items = listOf(aRoomDescription()) items = listOf(aRoomDescription())
) )

View File

@@ -7,6 +7,9 @@
package io.element.android.features.roommembermoderation.api package io.element.android.features.roommembermoderation.api
import androidx.compose.runtime.Immutable
@Immutable
interface RoomMemberModerationState { interface RoomMemberModerationState {
val canKick: Boolean val canKick: Boolean
val canBan: Boolean val canBan: Boolean

View File

@@ -10,6 +10,7 @@ package io.element.android.features.space.impl.leave
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class LeaveSpaceState( data class LeaveSpaceState(
val spaceName: String?, val spaceName: String?,
@@ -18,10 +19,15 @@ data class LeaveSpaceState(
val leaveSpaceAction: AsyncAction<Unit>, val leaveSpaceAction: AsyncAction<Unit>,
val eventSink: (LeaveSpaceEvents) -> Unit, val eventSink: (LeaveSpaceEvents) -> Unit,
) { ) {
private val rooms = selectableSpaceRooms.dataOrNull().orEmpty() private val rooms = selectableSpaceRooms.dataOrNull().orEmpty().toImmutableList()
private val partition = rooms.partition { it.isLastAdmin } private val lastAdminRooms: ImmutableList<SelectableSpaceRoom>
private val lastAdminRooms = partition.first private val selectableRooms: ImmutableList<SelectableSpaceRoom>
private val selectableRooms = partition.second
init {
val partition = rooms.partition { it.isLastAdmin }
lastAdminRooms = partition.first.toImmutableList()
selectableRooms = partition.second.toImmutableList()
}
/** /**
* True if we should show the quick action to select/deselect all rooms. * True if we should show the quick action to select/deselect all rooms.

View File

@@ -6,7 +6,7 @@
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
id("kotlin-parcelize") id("kotlin-parcelize")
} }

View File

@@ -7,13 +7,11 @@
package io.element.android.features.verifysession.impl.incoming package io.element.android.features.verifysession.impl.incoming
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
@Immutable
data class IncomingVerificationState( data class IncomingVerificationState(
val step: Step, val step: Step,
val request: VerificationRequest.Incoming, val request: VerificationRequest.Incoming,

View File

@@ -7,13 +7,11 @@
package io.element.android.features.verifysession.impl.outgoing package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
@Immutable
data class OutgoingVerificationState( data class OutgoingVerificationState(
val step: Step, val step: Step,
val request: VerificationRequest.Outgoing, val request: VerificationRequest.Outgoing,

View File

@@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.Color
/** /**
* This class holds all the semantic tokens of the Compound theme. * This class holds all the semantic tokens of the Compound theme.
*/ */
@Immutable
data class SemanticColors( data class SemanticColors(
/** Background colour for accent or brand actions. State: Hover */ /** Background colour for accent or brand actions. State: Hover */
val bgAccentHovered: Color, val bgAccentHovered: Color,

View File

@@ -7,11 +7,9 @@
package io.element.android.libraries.designsystem.components.avatar package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.runtime.Immutable
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import java.text.BreakIterator import java.text.BreakIterator
@Immutable
data class AvatarData( data class AvatarData(
val id: String, val id: String,
val name: String?, val name: String?,

View File

@@ -6,7 +6,7 @@
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View File

@@ -156,8 +156,8 @@ public class CameraPositionState(
/** /**
* The default saver implementation for [CameraPositionState]. * The default saver implementation for [CameraPositionState].
*/ */
public val Saver: Saver<CameraPositionState, SaveableCameraPositionState> = Saver( public val Saver: Saver<CameraPositionState, SaveableCameraPositionData> = Saver(
save = { SaveableCameraPositionState(it.position, it.cameraMode.toInternal()) }, save = { SaveableCameraPositionData(it.position, it.cameraMode.toInternal()) },
restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) } restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) }
) )
} }
@@ -172,7 +172,7 @@ public val currentCameraPositionState: CameraPositionState
get() = LocalCameraPositionState.current get() = LocalCameraPositionState.current
@Parcelize @Parcelize
public data class SaveableCameraPositionState( public data class SaveableCameraPositionData(
val position: CameraPosition, val position: CameraPosition,
val cameraMode: Int val cameraMode: Int
) : Parcelable ) : Parcelable

View File

@@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -19,7 +18,6 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class RoomInfo( data class RoomInfo(
val id: RoomId, val id: RoomId,
/** The room's name from the room state event if received from sync, or one that's been computed otherwise. */ /** The room's name from the room state event if received from sync, or one that's been computed otherwise. */

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -24,6 +25,7 @@ data class RoomMember(
/** /**
* Role of the RoomMember, based on its [powerLevel]. * Role of the RoomMember, based on its [powerLevel].
*/ */
@Immutable
sealed interface Role { sealed interface Role {
data class Owner(val isCreator: Boolean) : Role data class Owner(val isCreator: Boolean) : Role
data object Admin : Role data object Admin : Role

View File

@@ -26,9 +26,9 @@ interface RoomDirectoryList {
/** /**
* The current search results as a state flow. * The current search results as a state flow.
*/ */
val state: Flow<State> val state: Flow<SearchResult>
data class State( data class SearchResult(
val hasMoreToLoad: Boolean, val hasMoreToLoad: Boolean,
val items: List<RoomDescription>, val items: List<RoomDescription>,
) )

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.timeline package io.element.android.libraries.matrix.api.timeline
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId 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.core.RoomId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
@@ -42,6 +43,7 @@ interface Timeline : AutoCloseable {
} }
@Parcelize @Parcelize
@Immutable
sealed interface Mode : Parcelable { sealed interface Mode : Parcelable {
data object Live : Mode data object Live : Mode
data class FocusedOnEvent(val eventId: EventId) : Mode data class FocusedOnEvent(val eventId: EventId) : Mode

View File

@@ -7,10 +7,8 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable
data class EventReaction( data class EventReaction(
val key: String, val key: String,
val senders: ImmutableList<ReactionSender> val senders: ImmutableList<ReactionSender>

View File

@@ -8,10 +8,13 @@
package io.element.android.libraries.matrix.api.verification package io.element.android.libraries.matrix.api.verification
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Immutable
sealed interface VerificationRequest : Parcelable { sealed interface VerificationRequest : Parcelable {
@Immutable
sealed interface Outgoing : VerificationRequest { sealed interface Outgoing : VerificationRequest {
@Parcelize @Parcelize
data object CurrentSession : Outgoing data object CurrentSession : Outgoing

View File

@@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.NotJoinedRoom import io.element.android.libraries.matrix.api.room.NotJoinedRoom
@@ -15,7 +14,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
@Immutable
class NotJoinedRustRoom( class NotJoinedRustRoom(
private val sessionId: SessionId, private val sessionId: SessionId,
override val localRoom: RustBaseRoom?, override val localRoom: RustBaseRoom?,

View File

@@ -73,9 +73,9 @@ class RustRoomDirectoryList(
return !inner.isAtLastPage() return !inner.isAtLastPage()
} }
override val state: Flow<RoomDirectoryList.State> = override val state: Flow<RoomDirectoryList.SearchResult> =
combine(hasMoreToLoad, processor.roomDescriptionsFlow) { hasMoreToLoad, items -> combine(hasMoreToLoad, processor.roomDescriptionsFlow) { hasMoreToLoad, items ->
RoomDirectoryList.State( RoomDirectoryList.SearchResult(
hasMoreToLoad = hasMoreToLoad, hasMoreToLoad = hasMoreToLoad,
items = items items = items
) )

View File

@@ -42,7 +42,7 @@ class RustBaseRoomDirectoryListTest {
) )
val initialItem = awaitItem() val initialItem = awaitItem()
assertThat(initialItem).isEqualTo( assertThat(initialItem).isEqualTo(
RoomDirectoryList.State( RoomDirectoryList.SearchResult(
hasMoreToLoad = true, hasMoreToLoad = true,
items = listOf(mapper.map(aRustRoomDescription())) items = listOf(mapper.map(aRustRoomDescription()))
) )
@@ -57,7 +57,7 @@ class RustBaseRoomDirectoryListTest {
) )
val nextItem = awaitItem() val nextItem = awaitItem()
assertThat(nextItem).isEqualTo( assertThat(nextItem).isEqualTo(
RoomDirectoryList.State( RoomDirectoryList.SearchResult(
hasMoreToLoad = false, hasMoreToLoad = false,
items = listOf( items = listOf(
mapper.map(aRustRoomDescription()), mapper.map(aRustRoomDescription()),
@@ -66,7 +66,7 @@ class RustBaseRoomDirectoryListTest {
) )
val finalItem = awaitItem() val finalItem = awaitItem()
assertThat(finalItem).isEqualTo( assertThat(finalItem).isEqualTo(
RoomDirectoryList.State( RoomDirectoryList.SearchResult(
hasMoreToLoad = false, hasMoreToLoad = false,
items = listOf( items = listOf(
mapper.map(aRustRoomDescription()), mapper.map(aRustRoomDescription()),

View File

@@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.test.room package io.element.android.libraries.matrix.test.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
@@ -15,7 +14,6 @@ import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
@Immutable
class FakeNotJoinedRoom( class FakeNotJoinedRoom(
override val localRoom: BaseRoom? = null, override val localRoom: BaseRoom? = null,
override val previewInfo: RoomPreviewInfo = aRoomPreviewInfo(), override val previewInfo: RoomPreviewInfo = aRoomPreviewInfo(),

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
class FakeRoomDirectoryList( class FakeRoomDirectoryList(
override val state: Flow<RoomDirectoryList.State> = emptyFlow(), override val state: Flow<RoomDirectoryList.SearchResult> = emptyFlow(),
val filterLambda: (String?, Int, String?) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }, val filterLambda: (String?, Int, String?) -> Result<Unit> = { _, _, _ -> Result.success(Unit) },
val loadMoreLambda: () -> Result<Unit> = { Result.success(Unit) } val loadMoreLambda: () -> Result<Unit> = { Result.success(Unit) }
) : RoomDirectoryList { ) : RoomDirectoryList {

View File

@@ -8,7 +8,6 @@
package io.element.android.libraries.matrix.ui.model package io.element.android.libraries.matrix.ui.model
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@@ -21,7 +20,6 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.ui.R import io.element.android.libraries.matrix.ui.R
@Immutable
data class InviteSender( data class InviteSender(
val userId: UserId, val userId: UserId,
val displayName: String, val displayName: String,

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View File

@@ -7,6 +7,9 @@
package io.element.android.libraries.sessionstorage.api package io.element.android.libraries.sessionstorage.api
import androidx.compose.runtime.Immutable
@Immutable
sealed interface LoggedInState { sealed interface LoggedInState {
data object NotLoggedIn : LoggedInState data object NotLoggedIn : LoggedInState
data class LoggedIn( data class LoggedIn(

View File

@@ -128,15 +128,15 @@ class MarkdownTextEditorState(
} }
@Parcelize @Parcelize
data class SavedState( data class SavedValue(
val text: CharSequence, val text: CharSequence,
val selectionStart: Int, val selectionStart: Int,
val selectionEnd: Int, val selectionEnd: Int,
) : Parcelable ) : Parcelable
} }
object MarkdownTextEditorStateSaver : Saver<MarkdownTextEditorState, MarkdownTextEditorState.SavedState> { object MarkdownTextEditorStateSaver : Saver<MarkdownTextEditorState, MarkdownTextEditorState.SavedValue> {
override fun restore(value: MarkdownTextEditorState.SavedState): MarkdownTextEditorState { override fun restore(value: MarkdownTextEditorState.SavedValue): MarkdownTextEditorState {
return MarkdownTextEditorState( return MarkdownTextEditorState(
initialText = "", initialText = "",
initialFocus = false, initialFocus = false,
@@ -146,8 +146,8 @@ object MarkdownTextEditorStateSaver : Saver<MarkdownTextEditorState, MarkdownTex
} }
} }
override fun SaverScope.save(value: MarkdownTextEditorState): MarkdownTextEditorState.SavedState { override fun SaverScope.save(value: MarkdownTextEditorState): MarkdownTextEditorState.SavedValue {
return MarkdownTextEditorState.SavedState( return MarkdownTextEditorState.SavedValue(
text = value.text.value(), text = value.text.value(),
selectionStart = value.selection.first, selectionStart = value.selection.first,
selectionEnd = value.selection.last, selectionEnd = value.selection.last,

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {

View File

@@ -7,11 +7,14 @@
package io.element.android.libraries.troubleshoot.api.test package io.element.android.libraries.troubleshoot.api.test
import androidx.compose.runtime.Immutable
data class NotificationTroubleshootTestState( data class NotificationTroubleshootTestState(
val name: String, val name: String,
val description: String, val description: String,
val status: Status, val status: Status,
) { ) {
@Immutable
sealed interface Status { sealed interface Status {
data class Idle(val visible: Boolean) : Status data class Idle(val visible: Boolean) : Status
data object InProgress : Status data object InProgress : Status

View File

@@ -158,7 +158,6 @@ fun Project.setupKover() {
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState", "io.element.android.libraries.designsystem.swipe.SwipeableActionsState",
"io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState", "io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
"io.element.android.libraries.maplibre.compose.CameraPositionState", "io.element.android.libraries.maplibre.compose.CameraPositionState",
"io.element.android.libraries.maplibre.compose.SaveableCameraPositionState",
"io.element.android.libraries.maplibre.compose.SymbolState", "io.element.android.libraries.maplibre.compose.SymbolState",
"io.element.android.libraries.matrix.api.room.RoomMembershipState", "io.element.android.libraries.matrix.api.room.RoomMembershipState",
"io.element.android.libraries.matrix.api.room.RoomMembersState", "io.element.android.libraries.matrix.api.room.RoomMembersState",

View File

@@ -7,8 +7,12 @@
package io.element.android.tests.konsist package io.element.android.tests.konsist
import androidx.compose.runtime.Immutable
import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.ext.list.withoutName
import com.lemonappdev.konsist.api.verify.assertEmpty
import com.lemonappdev.konsist.api.verify.assertFalse import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test import org.junit.Test
@@ -60,4 +64,14 @@ class KonsistImmutableTest {
it.text.contains(".toPersistentMap()") it.text.contains(".toPersistentMap()")
} }
} }
@Test
fun `Immutable annotation is not used on sealed interface for Presenter Events`() {
Konsist
.scopeFromProduction()
.interfaces()
.withNameEndingWith("Events")
.withAnnotationOf(Immutable::class)
.assertEmpty()
}
} }

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
# Please see LICENSE files in the repository root for full details.
set -e
# Build the project with compose report
echo "Building the project with compose report..."
./gradlew assembleGplayDebug -PcomposeCompilerReports=true -PcomposeCompilerMetrics=true --stacktrace
echo "Checking stability of State classes..."
# Using the find command, list all the files ending with -classes.txt
find . -type f -name "*-classes.txt" | while read -r file; do
# echo "Processing $file"
# Check that there is no line containing "unstable class .*State {"
if grep -E 'unstable class .*State \{' "$file"; then
echo "❌ ERROR: Found unstable State class in $file"
exit 1
fi
done