Merge branch 'mantra' into claude/recursing-hermann-e58bbb
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -777,6 +777,54 @@ class SubgroupManagerJvmTest {
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a parent this device is in is named by its room`() = runBlocking {
|
||||
parentWith(members = listOf(alice, bob))
|
||||
childWithKeyState(parents = listOf(parentRoomId))
|
||||
|
||||
val parent = SubgroupManager.parentGroupsOf(db, childRoomId).single()
|
||||
|
||||
assertEquals(parentRoomId, parent.chatRoomId)
|
||||
// The room, and nothing else, is where a parent's name comes from: a
|
||||
// certificate names the child and never the certifier.
|
||||
assertEquals("Ekklesia", parent.room?.chatRoom?.subject)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a parent this device is not in is still listed, with nothing to name it`() = runBlocking {
|
||||
// No parent room: the ordinary position of a subgroup member who is not
|
||||
// also in the group above. The lineage is known -- it was checked
|
||||
// against that group's own signature -- so the parent stays in the list
|
||||
// and only its name is missing.
|
||||
childWithKeyState(parents = listOf(parentRoomId))
|
||||
|
||||
val parent = SubgroupManager.parentGroupsOf(db, childRoomId).single()
|
||||
|
||||
assertEquals(parentRoomId, parent.chatRoomId)
|
||||
assertNull(parent.room)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two parents keep the lineage's own order, named or not`() = runBlocking {
|
||||
parentWith(members = listOf(alice, bob))
|
||||
childWithKeyState(parents = listOf(parentRoomId, siblingRoomId))
|
||||
|
||||
val parents = SubgroupManager.parentGroupsOf(db, childRoomId)
|
||||
|
||||
// The same set in the same order -- canonical, which is sorted by id and
|
||||
// so says nothing about which parent came first. Asserted against
|
||||
// `parentsOf` rather than against a written-out order, since the ids are
|
||||
// derived from keys and nobody should have to know how two of them sort.
|
||||
assertEquals(
|
||||
SubgroupManager.parentsOf(db, childRoomId),
|
||||
parents.map { it.chatRoomId }
|
||||
)
|
||||
assertEquals(
|
||||
mapOf(parentRoomId to "Ekklesia", siblingRoomId to null),
|
||||
parents.associate { it.chatRoomId to it.room?.chatRoom?.subject }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refuseAdoptionOf(adopting: LocalChatRoom): String? =
|
||||
SubgroupManager.refuseAdoption(
|
||||
database = db,
|
||||
|
||||
Reference in New Issue
Block a user