Space-filter fix for bridge spaces, drag reorder, collapsed Home, call profiles, rail labels
Sonar / Sonar Quality Checks (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Post-release / post-release (push) Has been cancelled
Sonar / Sonar Quality Checks (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Post-release / post-release (push) Has been cancelled
- Space selection now unions the space's real child list (spaceRoomList) into the SDK descendants filter: mautrix-telegram spaces (children are DM portals) showed an empty room list, and DMs vanished inside every space - Long-press-drag the avatar to manually reorder rooms within a section, persisted per space (desktop parity); long-press elsewhere keeps the context menu - Home starts with Text/Voice sections collapsed (unread rooms still poke through) so DMs stay in reach; inside a space sections start expanded - Call participant rows show resolved display names + real avatars - Space rail items get name labels (identical bridge logos were indistinguishable) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
+11
-5
@@ -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<UserId>,
|
||||
modifier: Modifier = Modifier,
|
||||
users: ImmutableMap<UserId, MatrixUser> = 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,
|
||||
)
|
||||
|
||||
+37
-2
@@ -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.
|
||||
|
||||
+54
-18
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -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) {
|
||||
|
||||
+6
-1
@@ -45,6 +45,7 @@ fun buildBlapSections(
|
||||
summaries: List<RoomListRoomSummary>,
|
||||
voiceRoomIds: Set<RoomId>,
|
||||
collapsedSections: Set<String>,
|
||||
manualOrder: List<RoomId> = emptyList(),
|
||||
): Pair<List<RoomListRoomSummary>, List<BlapSectionEntry>> {
|
||||
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,
|
||||
|
||||
+3
@@ -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<RoomId>) : RoomListEvent
|
||||
|
||||
sealed interface ContextMenuEvent : RoomListEvent
|
||||
data object HideContextMenu : ContextMenuEvent
|
||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvent
|
||||
|
||||
+56
-3
@@ -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<RoomId>()) }
|
||||
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<UserId, MatrixUser>()) }
|
||||
val blapProfileAttempts = remember { mutableSetOf<UserId>() }
|
||||
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<RoomId>,
|
||||
blapCollapsedSections: Set<String>,
|
||||
blapManualOrder: List<RoomId>,
|
||||
blapCallUsers: Map<UserId, MatrixUser>,
|
||||
): 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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<RoomId>,
|
||||
val blapCollapsedSections: ImmutableSet<String>,
|
||||
// Blap: manual drag-and-drop order (per space, device-local) + resolved
|
||||
// profiles for users in active calls
|
||||
val blapManualOrder: ImmutableList<RoomId> = persistentListOf(),
|
||||
val blapCallUsers: ImmutableMap<UserId, MatrixUser> = persistentMapOf(),
|
||||
) : RoomListContentState
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user