Merge branch 'mantra' into claude/recursing-hermann-e58bbb
Material Design conformance / budgets (push) Has been cancelled
Material Design conformance / tests (push) Has been cancelled

This commit is contained in:
Kgothatso Ngako committed 2026-10-02 22:27:28 +02:00
commit 5875655a32
7 files changed
+138 -21

No files matched your search

@@ -192,8 +192,8 @@ class DatabaseChatRepository(
false
}
override suspend fun parentsOf(chatRoomId: String): List<HexKey> = try {
SubgroupManager.parentsOf(database, chatRoomId)
override suspend fun parentsOf(chatRoomId: String): List<SubgroupManager.Parent> = try {
SubgroupManager.parentGroupsOf(database, chatRoomId)
} catch (e: Throwable) {
logger.e("Error reading the parents of $chatRoomId", e)
emptyList()
@@ -842,6 +842,35 @@ object SubgroupManager {
database.groupKeyStateDao().getLocalByChatRoomId(chatRoomId)?.parentChatRoomIds()
.orEmpty()
/**
* A group this room is a child of, plus the room itself where this device
* has it.
*
* [room] is the only place a parent's *name* can come from: a certificate
* names the child, never the certifier, so a device that is not in the
* parent room knows the lineage and not what to call it. Null is therefore
* the ordinary case for a subgroup member who is not also a parent member,
* and the id is all such a screen can honestly show.
*/
data class Parent(
val chatRoomId: HexKey,
val room: LocalChatRoom?,
)
/**
* [parentsOf], with each parent's room where this device holds one.
*
* The same verified set in the same order -- this only looks each id up, and
* a parent whose room is missing stays in the list rather than being dropped.
*/
suspend fun parentGroupsOf(database: MantraDatabase, chatRoomId: String): List<Parent> =
parentsOf(database, chatRoomId).map { parentChatRoomId ->
Parent(
chatRoomId = parentChatRoomId,
room = database.chatRoomDao().findChatRoomById(parentChatRoomId)
)
}
/**
* What a certificate arriving in a room is, or null if it is nothing this
* room should believe.
@@ -78,8 +78,12 @@ interface ChatRepository {
* may be adopted by another later. A member welcomed in after the founding
* holds no state and gets an empty list even when the room really is a
* subgroup.
*
* Each carries the parent's room where this device has one, which is the
* only place its name can come from -- a certificate names the child and
* never the certifier.
*/
suspend fun parentsOf(chatRoomId: String): List<HexKey>
suspend fun parentsOf(chatRoomId: String): List<SubgroupManager.Parent>
/**
* One event the group signed, as the group signed it, or null when this
@@ -329,7 +333,9 @@ interface ChatRepository {
override suspend fun subgroupsOf(chatRoomId: String): List<SubgroupManager.Subgroup> =
emptyList()
override suspend fun parentsOf(chatRoomId: String): List<HexKey> = emptyList()
override suspend fun parentsOf(
chatRoomId: String
): List<SubgroupManager.Parent> = emptyList()
override suspend fun groupSignedEvent(
chatRoomId: String,
@@ -282,15 +282,40 @@ fun ChatRoomDetailScreen(
// later. They are a set rather than a path -- neither
// is "the" parent and the order says nothing about
// which came first -- so each gets an equal row.
chatRoomDetailUIState.parentChatRoomIds.forEach { parentChatRoomId ->
item(key = "parent-$parentChatRoomId") {
chatRoomDetailUIState.parents.forEach { parent ->
item(key = "parent-${parent.chatRoomId}") {
// The parent's own name, which only its room
// can give: a certificate names the child and
// never the certifier, so a member of the
// subgroup who is not also in the parent has
// the id and nothing else to call it by.
val name = parent.room
?.chatRoom
?.subject
?.takeIf { it.isNotBlank() }
// Shown either way -- as the headline where
// there is no name, and under it where there
// is, since it is the part that can be
// compared against what the lineage says.
val identifier: @Composable () -> Unit = {
Text(
text = parent.chatRoomId
.take(16)
.inComparableGroups(),
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis
)
}
Card(
modifier = Modifier.fillMaxWidth(),
onClick = {
onNavigateToRoute.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = parentChatRoomId,
chatRoomId = parent.chatRoomId,
relayHint = relayHint
)
)
@@ -309,19 +334,25 @@ fun ChatRoomDetailScreen(
contentDescription = Decorative
)
},
headlineContent = {
// The label keeps its place above the
// name rather than being replaced by
// it: a card carrying a group's name
// and nothing else says nothing about
// which way the lineage runs, and the
// signing key's card -- same icon
// column, same shape -- sits directly
// above this one.
overlineContent = {
Text(text = stringResource(Res.string.parent_group))
},
supportingContent = {
Text(
text = parentChatRoomId
.take(16)
.inComparableGroups(),
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis
)
}
headlineContent = {
if (name != null) {
Text(text = name)
} else {
identifier()
}
},
supportingContent = identifier.takeIf { name != null }
)
}
}
@@ -83,7 +83,7 @@ class ChatRoomDetailViewModel(
groupKeyState = chatRepository.groupKeyState(chatRoomId),
signedGroupKeyStateEvent = chatRepository.signedGroupKeyStateEvent(chatRoomId),
subgroups = chatRepository.subgroupsOf(chatRoomId),
parentChatRoomIds = chatRepository.parentsOf(chatRoomId),
parents = chatRepository.parentsOf(chatRoomId),
canAddSubgroup = localChatRoom.chatRoom.mlsGroupState != null &&
chatRepository.canSign(chatRoomId),
adoptionOffers = chatRepository.adoptionOffersFor(chatRoomId),
@@ -45,15 +45,18 @@ sealed interface ChatRoomDetailUIState {
*/
val subgroups: List<SubgroupManager.Subgroup> = emptyList(),
/**
* The group this room is a subgroup of, or null when it is nobody's
* The groups this room is a subgroup of, empty when it is nobody's
* child.
*
* Verified: it comes off the room's key state, which only carries a
* parent whose certificate was checked. A member welcomed in after the
* founding holds no state and so sees nothing here, even in a room that
* really is a subgroup.
*
* Each carries the parent's room where this device holds one, so the
* card can name the group rather than only identify it.
*/
val parentChatRoomIds: List<HexKey> = emptyList(),
val parents: List<SubgroupManager.Parent> = emptyList(),
/**
* Whether this device could open a subgroup here at all.
*