diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index efdc545e61..0113efd324 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) implementation(libs.haze) + implementation(libs.reorderable) implementation(libs.haze.materials) implementation(projects.features.preferences.impl) implementation(projects.features.reportroom.api) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapRoomListSections.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapRoomListSections.kt index fc9408343a..fc52e4aabc 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapRoomListSections.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapRoomListSections.kt @@ -32,8 +32,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.theme.components.Icon 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.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf private const val MAX_VOICE_MEMBERS_SHOWN = 8 @@ -79,10 +82,13 @@ internal fun BlapSectionHeader( internal fun BlapVoiceMembersRow( participants: ImmutableList, modifier: Modifier = Modifier, + users: ImmutableMap = persistentMapOf(), ) { Column(modifier = modifier.padding(start = 64.dp, bottom = 8.dp)) { participants.take(MAX_VOICE_MEMBERS_SHOWN).forEach { userId -> - val localpart = userId.value.removePrefix("@").substringBefore(":") + val user = users[userId] + val name = user?.displayName?.takeIf { it.isNotBlank() } + ?: userId.value.removePrefix("@").substringBefore(":") Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp), @@ -90,15 +96,15 @@ internal fun BlapVoiceMembersRow( Avatar( avatarData = AvatarData( id = userId.value, - name = localpart, - url = null, - size = AvatarSize.TimelineReadReceipt, + name = name, + url = user?.avatarUrl, + size = AvatarSize.TimelineThreadLatestEventSender, ), avatarType = AvatarType.User, ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = localpart, + text = name, style = ElementTheme.typography.fontBodySmRegular, color = ElementTheme.colors.textSecondary, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt index d73da94307..7d8d87e812 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -28,6 +29,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -38,6 +41,7 @@ import io.element.android.features.home.impl.spacefilters.selectedFilter import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.model.getAvatarData /** @@ -65,6 +69,7 @@ fun BlapSpaceRail( item(key = "blap_rail_home") { RailItem( isSelected = selectedRoomId == null, + label = "Home", onClick = { onSelect(null) }, ) { Icon( @@ -79,6 +84,9 @@ fun BlapSpaceRail( val isSelected = filter.spaceRoom.roomId == selectedRoomId RailItem( isSelected = isSelected, + // Bridge spaces often share one logo (two Discord servers look identical) — + // the name label is what tells them apart. + label = filter.spaceRoom.displayName, onClick = { onSelect(filter) }, ) { Avatar( @@ -92,14 +100,41 @@ fun BlapSpaceRail( @Composable private fun RailItem( + isSelected: Boolean, + label: String, + onClick: () -> Unit, + content: @Composable () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RailItemAvatar(isSelected = isSelected, onClick = onClick, content = content) + Text( + text = label, + style = ElementTheme.typography.fontBodyXsRegular, + color = if (isSelected) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 2.dp), + ) + } +} + +@Composable +private fun RailItemAvatar( isSelected: Boolean, onClick: () -> Unit, content: @Composable () -> Unit, ) { Box( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), + .fillMaxWidth(), contentAlignment = Alignment.Center, ) { // Discord-style selection pill hugging the left edge of the rail. diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index 69edfd1113..c57a28e074 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -11,6 +11,7 @@ package io.element.android.features.home.impl.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -32,6 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.home.impl.R import io.element.android.features.home.impl.contentType @@ -58,6 +60,8 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable fun RoomListContentView( @@ -228,8 +232,25 @@ private fun RoomsViewList( // Blap: Discord-style sections. Invites/knocks render on top, ungrouped; then // Text / Voice / DM sections (desktop order, Home included) with collapsible // headers and inline voice members. - val (blapSpecial, blapSections) = remember(state.summaries, state.blapVoiceRoomIds, state.blapCollapsedSections) { - buildBlapSections(state.summaries, state.blapVoiceRoomIds, state.blapCollapsedSections) + val (blapSpecial, blapSections) = remember(state.summaries, state.blapVoiceRoomIds, state.blapCollapsedSections, state.blapManualOrder) { + buildBlapSections(state.summaries, state.blapVoiceRoomIds, state.blapCollapsedSections, state.blapManualOrder) + } + // Blap: long-press-drag manual reordering, constrained to within a section. + val blapSectionByRoom = remember(blapSections) { + blapSections.flatMap { entry -> entry.rooms.map { it.roomId.value to entry.section } }.toMap() + } + val blapFlatRoomIds = remember(blapSections) { blapSections.flatMap { entry -> entry.rooms.map { it.roomId.value } } } + val blapReorderableState = rememberReorderableLazyListState(lazyListState) { from, to -> + val fromKey = from.key as? String ?: return@rememberReorderableLazyListState + val toKey = to.key as? String ?: return@rememberReorderableLazyListState + val fromSection = blapSectionByRoom[fromKey] ?: return@rememberReorderableLazyListState + if (fromSection != blapSectionByRoom[toKey]) return@rememberReorderableLazyListState + val ids = blapFlatRoomIds.toMutableList() + val fromIndex = ids.indexOf(fromKey) + val toIndex = ids.indexOf(toKey) + if (fromIndex < 0 || toIndex < 0) return@rememberReorderableLazyListState + ids.add(toIndex, ids.removeAt(fromIndex)) + eventSink(RoomListEvent.BlapSetRoomOrder(ids.map { RoomId(it) })) } LazyColumn( state = lazyListState, @@ -296,7 +317,7 @@ private fun RoomsViewList( } } blapSections.forEach { entry -> - item(contentType = "blap_section_header") { + item(key = "blap_header_" + entry.section.key, contentType = "blap_section_header") { BlapSectionHeader( section = entry.section, isCollapsed = entry.isCollapsed, @@ -305,23 +326,38 @@ private fun RoomsViewList( } itemsIndexed( items = entry.rooms, + key = { _, room -> room.roomId.value }, contentType = { _, room -> room.contentType() }, ) { index, room -> - RoomSummaryRow( - room = room, - hideInviteAvatars = hideInvitesAvatars, - isInviteSeen = false, - showUnreadCount = state.showUnreadCount, - onClick = onRoomClick, - eventSink = eventSink, - ) - // Any room with an active call shows who's in it — not just classified - // voice rooms (classification lags until the space was opened once). - if (room.activeRoomCallParticipants.isNotEmpty()) { - BlapVoiceMembersRow(participants = room.activeRoomCallParticipants) - } - if (index != entry.rooms.lastIndex) { - HorizontalDivider() + ReorderableItem(blapReorderableState, key = room.roomId.value) { isDragging -> + Column( + modifier = if (isDragging) { + Modifier.background(ElementTheme.colors.bgSubtleSecondary) + } else { + Modifier + }, + ) { + RoomSummaryRow( + room = room, + hideInviteAvatars = hideInvitesAvatars, + isInviteSeen = false, + showUnreadCount = state.showUnreadCount, + onClick = onRoomClick, + blapAvatarModifier = Modifier.longPressDraggableHandle(), + eventSink = eventSink, + ) + // Any room with an active call shows who's in it — not just classified + // voice rooms (classification lags until the space was opened once). + if (room.activeRoomCallParticipants.isNotEmpty()) { + BlapVoiceMembersRow( + participants = room.activeRoomCallParticipants, + users = state.blapCallUsers, + ) + } + if (index != entry.rooms.lastIndex) { + HorizontalDivider() + } + } } } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 77d1ae3254..da401df8ad 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -78,6 +78,9 @@ internal fun RoomSummaryRow( onClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, showUnreadCount: Boolean = false, + // Blap: drag handle for manual reordering, applied to the avatar so the row's + // long-press context menu keeps working + blapAvatarModifier: Modifier = Modifier, eventSink: (RoomListEvent) -> Unit, ) { Box(modifier = modifier) { @@ -122,6 +125,7 @@ internal fun RoomSummaryRow( onLongClick = { eventSink(RoomListEvent.ShowContextMenu(room)) }, + blapAvatarModifier = blapAvatarModifier, ) { NameAndTimestampRow( name = room.name, @@ -174,6 +178,7 @@ private fun RoomSummaryScaffoldRow( onLongClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, hideAvatarImage: Boolean = false, + blapAvatarModifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit ) { val clickModifier = Modifier @@ -193,7 +198,7 @@ private fun RoomSummaryScaffoldRow( .padding(horizontal = 16.dp, vertical = 11.dp) .height(IntrinsicSize.Min), ) { - Box { + Box(modifier = blapAvatarModifier) { Avatar( avatarData = room.avatarData, avatarType = if (room.isSpace) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/BlapSections.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/BlapSections.kt index bf172778fd..08c7394e17 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/BlapSections.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/BlapSections.kt @@ -45,6 +45,7 @@ fun buildBlapSections( summaries: List, voiceRoomIds: Set, collapsedSections: Set, + manualOrder: List = emptyList(), ): Pair, List> { val (rooms, special) = summaries.partition { it.displayType == io.element.android.features.home.impl.model.RoomSummaryDisplayType.ROOM @@ -56,9 +57,13 @@ fun buildBlapSections( else -> BlapSection.Text } } + // Manual drag-and-drop placements first, unplaced rooms keep their (stable) + // default order after them — same semantics as desktop's YapRoomList. + val manualIndex = manualOrder.withIndex().associate { (index, id) -> id to index } val sectionOrder = listOf(BlapSection.Text, BlapSection.Voice, BlapSection.Dm) val entries = sectionOrder.mapNotNull { section -> - val sectionRooms = bySection[section] ?: return@mapNotNull null + val sectionRooms = (bySection[section] ?: return@mapNotNull null) + .sortedBy { manualIndex[it.roomId] ?: Int.MAX_VALUE } val isCollapsed = section.key in collapsedSections BlapSectionEntry( section = section, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt index 429b7aac5b..0523449dba 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt @@ -27,6 +27,9 @@ sealed interface RoomListEvent { // Blap: collapse/expand a room list section data class BlapToggleSection(val section: BlapSection) : RoomListEvent + // Blap: persist the new manual room order after a drag (full displayed order) + data class BlapSetRoomOrder(val order: List) : RoomListEvent + sealed interface ContextMenuEvent : RoomListEvent data object HideContextMenu : ContextMenuEvent data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvent diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index e05a0ed328..e432e27a96 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -51,16 +51,19 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.push.api.battery.BatteryOptimizationState import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -116,12 +119,18 @@ class RoomListPresenter( sharedPreferences.getStringSet(BLAP_VOICE_ROOMS_KEY, null).orEmpty().map { RoomId(it) }.toSet() ) } + // Blap: the selected space's true child list — used for voice classification AND + // unioned into the room filter (the SDK's descendants list omits DM portals, which + // left bridge spaces like mautrix-telegram's looking empty). + var blapSpaceChildIds by remember { mutableStateOf(emptySet()) } LaunchedEffect(selectedSpaceId) { + blapSpaceChildIds = emptySet() if (selectedSpaceId == null) return@LaunchedEffect val spaceRoomList = client.spaceService.spaceRoomList(selectedSpaceId) try { spaceRoomList.loadAllIncrementally(this) spaceRoomList.spaceRoomsFlow.collect { spaceRooms -> + blapSpaceChildIds = spaceRooms.map { it.roomId }.toSet() val voiceIds = spaceRooms.filter { it.roomType.isBlapVoiceRoom() }.map { it.roomId } if (voiceIds.isNotEmpty() && !blapVoiceRoomIds.containsAll(voiceIds)) { blapVoiceRoomIds = blapVoiceRoomIds + voiceIds @@ -135,8 +144,34 @@ class RoomListPresenter( } } // Blap: per-space section collapse state, persisted like desktop's localStorage key. + // At Home the channel sections start collapsed (DMs stay in reach on a phone); + // inside a space they start expanded. User toggles win once persisted. var blapCollapsedSections by remember(selectedSpaceId) { - mutableStateOf(sharedPreferences.getStringSet(blapCollapseKey(selectedSpaceId), null).orEmpty().toSet()) + mutableStateOf( + sharedPreferences.getStringSet(blapCollapseKey(selectedSpaceId), null)?.toSet() + ?: if (selectedSpaceId == null) setOf(BlapSection.Text.key, BlapSection.Voice.key) else emptySet() + ) + } + // Blap: manual drag-and-drop room order, per space, device-local (desktop parity). + var blapManualOrder by remember(selectedSpaceId) { + mutableStateOf( + sharedPreferences.getString(blapOrderKey(selectedSpaceId), null) + ?.split(",")?.filter { it.isNotBlank() }?.map { RoomId(it) }.orEmpty() + ) + } + // Blap: resolve profiles (display name + avatar) for users in active calls. + var blapCallUsers by remember { mutableStateOf(emptyMap()) } + val blapProfileAttempts = remember { mutableSetOf() } + LaunchedEffect(Unit) { + roomListDataSource.roomSummariesFlow.collect { summaries -> + val wanted = summaries.flatMap { it.activeRoomCallParticipants }.toSet() + for (userId in wanted) { + if (!blapProfileAttempts.add(userId)) continue + client.getProfile(userId).getOrNull()?.let { user -> + blapCallUsers = blapCallUsers + (userId to user) + } + } + } } var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } @@ -196,12 +231,22 @@ class RoomListPresenter( } sharedPreferences.edit { putStringSet(blapCollapseKey(selectedSpaceId), blapCollapsedSections) } } + is RoomListEvent.BlapSetRoomOrder -> { + blapManualOrder = event.order + sharedPreferences.edit { + putString(blapOrderKey(selectedSpaceId), event.order.joinToString(",") { it.value }) + } + } } } - LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { + LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter(), blapSpaceChildIds) { val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() } - val selectedSpaceFilter = spaceFiltersState.selectedFilter().into() + // Blap: union the space's real child list into the SDK's descendants — bridge + // spaces whose children are DM portals (mautrix-telegram) otherwise come up empty. + val selectedSpaceFilter = spaceFiltersState.selectedFilter()?.let { filter -> + RoomListFilter.Identifiers((filter.descendants.toSet() + blapSpaceChildIds).toList()) + } val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter)) roomListDataSource.updateFilter(allFilters) } @@ -217,6 +262,8 @@ class RoomListPresenter( showUnreadCount, blapVoiceRoomIds, blapCollapsedSections, + blapManualOrder, + blapCallUsers, ) return RoomListState( @@ -271,6 +318,8 @@ class RoomListPresenter( private fun blapCollapseKey(spaceId: RoomId?) = "blap_sections_collapsed_" + (spaceId?.value ?: "all") + private fun blapOrderKey(spaceId: RoomId?) = "blap_room_order_" + (spaceId?.value ?: "all") + private companion object { const val BLAP_VOICE_ROOMS_KEY = "blap_voice_room_ids" } @@ -282,6 +331,8 @@ class RoomListPresenter( showUnreadCount: Boolean, blapVoiceRoomIds: Set, blapCollapsedSections: Set, + blapManualOrder: List, + blapCallUsers: Map, ): RoomListContentState { val roomSummaries by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) } @@ -317,6 +368,8 @@ class RoomListPresenter( seenRoomInvites = seenRoomInvites.toImmutableSet(), blapVoiceRoomIds = blapVoiceRoomIds.toImmutableSet(), blapCollapsedSections = blapCollapsedSections.toImmutableSet(), + blapManualOrder = blapManualOrder.toImmutableList(), + blapCallUsers = blapCallUsers.toImmutableMap(), ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index 640419f15c..0f52c357f8 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -17,9 +17,14 @@ import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteS import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.battery.BatteryOptimizationState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf data class RoomListState( val contextMenu: ContextMenu, @@ -77,5 +82,9 @@ sealed interface RoomListContentState { // Blap: Discord-style sections val blapVoiceRoomIds: ImmutableSet, val blapCollapsedSections: ImmutableSet, + // Blap: manual drag-and-drop order (per space, device-local) + resolved + // profiles for users in active calls + val blapManualOrder: ImmutableList = persistentListOf(), + val blapCallUsers: ImmutableMap = persistentMapOf(), ) : RoomListContentState } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cdc77980a6..523c0b11b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ serialization_json = "1.11.0" coil = "3.5.0" # Rollback to 1.0.4, 1.0.5 has this issue: https://github.com/airbnb/Showkase/issues/420 showkase = "1.0.5" +reorderable = "2.5.1" # There is some custom logic in `RootFlowNode` that may break because it reuses some Appyx internal APIs. # When upgrading this version, check state restoration still works fine. appyx = "1.7.1" @@ -189,6 +190,7 @@ matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.07.15" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }