feat(subgroups): ask for a room from the thing that needs one
Four buttons, one per entity, each opening the existing picker with the name already in it. This is the cheap version of the feature complete: a member looking at a translation can give it a room without backing out to the group, finding "Add subgroup" and retyping a name the app already knew. `SelectSubgroupAdminsRoute` grows three nullable fields. The name is a suggestion and stays editable -- a group renaming its own room is not a route's business to prevent -- and it is seeded in the view model's constructor rather than in `initiate`, because a field somebody may already be typing into must not be overwritten by a load completing underneath them. The origin is two flat fields rather than one type, because a route is `@Serializable` and is rebuilt from a bundle. The nav host reassembles them and requires both or neither: half a reference is a reference with the resolvable part removed. The view model then only holds it. Riding it onto the ceremony needs a `DkgSession` column and a tag on the proposal, which is Phase 5, and the comment at the `proposeRitual` call says so rather than leaving a parameter that looks forgotten. The gate is `canAddSubgroup` on all four states and not `canSign`: a share of the key *and* `mlsGroupState != null`, because a NIP-17 room has no admins to be a parent's and these entities can live in one. Three states had to learn it. `ProjectDetailUIState` and `TranslationArtifactVersionDetailUIState` already carried `canSign`; `ArtifactDetailUIState` carried no capability flag at all, so the artifact screen and its view model gained a `ChatRepository` as well -- the one place this phase is more than a button. Each flag is computed off `SubgroupOrigin.of(entity).parentChatRoomId` rather than the route's `chatRoomId`. They are the same room in every path that exists, and reading it through the function is what keeps the parent of a subgroup the room the entity actually lives in if a route is ever built by hand. Hidden rather than disabled where it cannot work, which is what the room's own subgroup entry does and for the same reason: a device with no share cannot open the certificate session at all, and `proposeSigningBatch` throws rather than refusing. Phase 3 of docs/subgroups-for-entities.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
<string name="add_chapter">Add chapter</string>
|
||||
<string name="add_dialect">Add dialect</string>
|
||||
<string name="add_to_group">Add to group</string>
|
||||
<string name="give_this_a_room_of_its_own">Give this a room of its own</string>
|
||||
<string name="add_translation">Add translation</string>
|
||||
<string name="after_this_there_is_no_turning_back">After this there is no turning back.</string>
|
||||
<string name="all_broadcasts_are_queued_so_that_we_can">All broadcasts are queued so that we can manage data usage on metered connections.</string>
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.Article
|
||||
import androidx.compose.material.icons.filled.PostAdd
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
@@ -35,7 +36,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.MantraArtifact
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.DetailRow
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import mantra.composeapp.generated.resources.upload_chapters
|
||||
@@ -44,6 +48,7 @@ import press.mantra.compose.ui.composable.navigation.routes.UploadChaptersRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.AddTranslationRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ChapterDetailRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SelectSubgroupAdminsRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationArtifactVersionDetailRoute
|
||||
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import press.mantra.compose.ui.theme.MantraTheme
|
||||
@@ -57,6 +62,7 @@ import mantra.composeapp.generated.resources.add_translation
|
||||
import mantra.composeapp.generated.resources.artifact_detail
|
||||
import mantra.composeapp.generated.resources.chapters
|
||||
import mantra.composeapp.generated.resources.details
|
||||
import mantra.composeapp.generated.resources.give_this_a_room_of_its_own
|
||||
import mantra.composeapp.generated.resources.no_chapters_yet
|
||||
import mantra.composeapp.generated.resources.no_translations_yet
|
||||
import mantra.composeapp.generated.resources.no_versions
|
||||
@@ -81,6 +87,7 @@ fun ArtifactDetailScreen(
|
||||
relayHint: String?,
|
||||
initialArtifactDetailUIState: ArtifactDetailUIState = ArtifactDetailUIState.Loading,
|
||||
mantraRepository: MantraRepository,
|
||||
chatRepository: ChatRepository,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
@@ -92,6 +99,7 @@ fun ArtifactDetailScreen(
|
||||
relayHint = relayHint,
|
||||
initialArtifactDetailUIState = initialArtifactDetailUIState,
|
||||
mantraRepository = mantraRepository,
|
||||
chatRepository = chatRepository,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -291,6 +299,48 @@ fun ArtifactDetailScreen(
|
||||
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
// Hidden rather than disabled where it cannot work,
|
||||
// the way the room's own subgroup entry is: a device
|
||||
// with no share of the key cannot open the
|
||||
// certificate session at all, and a NIP-17 room has
|
||||
// no admins to be a parent's.
|
||||
if (artifactDetailUIState.canAddSubgroup) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val candidate = SubgroupOrigin.of(artifact)
|
||||
|
||||
onNavigateToRoute.invoke(
|
||||
SelectSubgroupAdminsRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = candidate.parentChatRoomId,
|
||||
relayHint = relayHint,
|
||||
name = candidate.name,
|
||||
originKind = candidate.origin.kind,
|
||||
originId = candidate.origin.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AccountTree,
|
||||
contentDescription = Decorative
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.width(
|
||||
MaterialTheme.spacing.itemGap
|
||||
)
|
||||
)
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.give_this_a_room_of_its_own
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
// Details
|
||||
item {
|
||||
Text(
|
||||
@@ -386,6 +436,7 @@ private fun ArtifactDetailScreenPreview() {
|
||||
)
|
||||
),
|
||||
mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY,
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
onNavigateToRoute = {},
|
||||
onNavigateBack = {}
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material3.Card
|
||||
@@ -21,6 +23,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -33,6 +36,7 @@ import mantra.composeapp.generated.resources.Res
|
||||
import mantra.composeapp.generated.resources.country
|
||||
import mantra.composeapp.generated.resources.details
|
||||
import mantra.composeapp.generated.resources.dialect_detail
|
||||
import mantra.composeapp.generated.resources.give_this_a_room_of_its_own
|
||||
import mantra.composeapp.generated.resources.language
|
||||
import mantra.composeapp.generated.resources.library
|
||||
import mantra.composeapp.generated.resources.no_artifacts_are_written_in_this_dialect
|
||||
@@ -40,10 +44,12 @@ import mantra.composeapp.generated.resources.no_translations_are_into_this_diale
|
||||
import mantra.composeapp.generated.resources.translations
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import press.mantra.compose.database.model.MantraDialect
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ArtifactDetailRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SelectSubgroupAdminsRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationArtifactVersionDetailRoute
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.DetailRow
|
||||
@@ -228,6 +234,48 @@ fun DialectDetailScreen(
|
||||
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
// Hidden rather than disabled where it cannot work,
|
||||
// the way the room's own subgroup entry is: a device
|
||||
// with no share of the key cannot open the
|
||||
// certificate session at all, and a NIP-17 room has
|
||||
// no admins to be a parent's.
|
||||
if (dialectDetailUIState.canAddSubgroup) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val candidate = SubgroupOrigin.of(dialect)
|
||||
|
||||
onNavigateToRoute.invoke(
|
||||
SelectSubgroupAdminsRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = candidate.parentChatRoomId,
|
||||
relayHint = relayHint,
|
||||
name = candidate.name,
|
||||
originKind = candidate.origin.kind,
|
||||
originId = candidate.origin.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AccountTree,
|
||||
contentDescription = Decorative
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.width(
|
||||
MaterialTheme.spacing.itemGap
|
||||
)
|
||||
)
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.give_this_a_room_of_its_own
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.details),
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.Article
|
||||
import androidx.compose.material.icons.filled.PostAdd
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
@@ -38,6 +39,7 @@ import mantra.composeapp.generated.resources.add_source_version_to_project
|
||||
import mantra.composeapp.generated.resources.add_translation_to_project
|
||||
import mantra.composeapp.generated.resources.created_by
|
||||
import mantra.composeapp.generated.resources.details
|
||||
import mantra.composeapp.generated.resources.give_this_a_room_of_its_own
|
||||
import mantra.composeapp.generated.resources.no_source_versions_in_this_project
|
||||
import mantra.composeapp.generated.resources.no_translations_in_this_project
|
||||
import mantra.composeapp.generated.resources.owned_by_this_group
|
||||
@@ -49,11 +51,14 @@ import mantra.composeapp.generated.resources.version
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import press.mantra.compose.database.model.Profile
|
||||
import press.mantra.compose.extensions.shortened
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.FrostSigningRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.repository.NostrRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.AddToProjectRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SelectSubgroupAdminsRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationArtifactVersionDetailRoute
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.DetailRow
|
||||
@@ -87,6 +92,7 @@ fun ProjectDetailScreen(
|
||||
mantraRepository: MantraRepository,
|
||||
nostrRepository: NostrRepository,
|
||||
frostSigningRepository: FrostSigningRepository,
|
||||
chatRepository: ChatRepository,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
@@ -100,6 +106,7 @@ fun ProjectDetailScreen(
|
||||
mantraRepository = mantraRepository,
|
||||
nostrRepository = nostrRepository,
|
||||
frostSigningRepository = frostSigningRepository,
|
||||
chatRepository = chatRepository,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -258,6 +265,48 @@ fun ProjectDetailScreen(
|
||||
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
// Hidden rather than disabled where it cannot work,
|
||||
// the way the room's own subgroup entry is: a device
|
||||
// with no share of the key cannot open the
|
||||
// certificate session at all, and a NIP-17 room has
|
||||
// no admins to be a parent's.
|
||||
if (projectDetailUIState.canAddSubgroup) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val candidate = SubgroupOrigin.of(project)
|
||||
|
||||
onNavigateToRoute.invoke(
|
||||
SelectSubgroupAdminsRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = candidate.parentChatRoomId,
|
||||
relayHint = relayHint,
|
||||
name = candidate.name,
|
||||
originKind = candidate.origin.kind,
|
||||
originId = candidate.origin.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AccountTree,
|
||||
contentDescription = Decorative
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.width(
|
||||
MaterialTheme.spacing.itemGap
|
||||
)
|
||||
)
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.give_this_a_room_of_its_own
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
// Who it belongs to
|
||||
item {
|
||||
Text(
|
||||
@@ -359,6 +408,7 @@ private fun ProjectDetailScreenPreview() {
|
||||
mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY,
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||
frostSigningRepository = FrostSigningRepository.NO_OP_FROST_SIGNING_REPOSITORY,
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
onNavigateToRoute = {},
|
||||
onNavigateBack = {},
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import press.mantra.compose.managers.MarmotGroupName
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.DkgRepository
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.ErrorState
|
||||
@@ -85,6 +86,10 @@ fun SelectSubgroupAdminsScreen(
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
/** A name to open the field with, where the caller knows one. */
|
||||
suggestedName: String? = null,
|
||||
/** What the subgroup is for, where it was asked for from an entity. */
|
||||
origin: SubgroupOrigin.Ref? = null,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
@@ -94,6 +99,8 @@ fun SelectSubgroupAdminsScreen(
|
||||
parentChatRoomId = parentChatRoomId,
|
||||
initialUIState = initialUIState,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
suggestedName = suggestedName,
|
||||
origin = origin,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
|
||||
@@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.Article
|
||||
import androidx.compose.material.icons.filled.PlaylistAdd
|
||||
import androidx.compose.material3.Card
|
||||
@@ -32,12 +34,15 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.MantraTranslationArtifactVersion
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.FrostSigningRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.FrostSigningRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SelectSubgroupAdminsRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationChapterRoute
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.DetailRow
|
||||
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import press.mantra.compose.ui.theme.MantraTheme
|
||||
@@ -48,6 +53,7 @@ import mantra.composeapp.generated.resources.Res
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import mantra.composeapp.generated.resources.chapters
|
||||
import mantra.composeapp.generated.resources.details
|
||||
import mantra.composeapp.generated.resources.give_this_a_room_of_its_own
|
||||
import mantra.composeapp.generated.resources.no_chapters
|
||||
import mantra.composeapp.generated.resources.propose
|
||||
import mantra.composeapp.generated.resources.this_group_has_no_shared_key_so_it_cannot
|
||||
@@ -228,6 +234,48 @@ fun TranslationArtifactVersionDetailScreen(
|
||||
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
// Hidden rather than disabled where it cannot work,
|
||||
// the way the room's own subgroup entry is: a device
|
||||
// with no share of the key cannot open the
|
||||
// certificate session at all, and a NIP-17 room has
|
||||
// no admins to be a parent's.
|
||||
if (translationDetailUIState.canAddSubgroup) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val candidate = SubgroupOrigin.of(translation)
|
||||
|
||||
onNavigateToRoute.invoke(
|
||||
SelectSubgroupAdminsRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = candidate.parentChatRoomId,
|
||||
relayHint = relayHint,
|
||||
name = candidate.name,
|
||||
originKind = candidate.origin.kind,
|
||||
originId = candidate.origin.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AccountTree,
|
||||
contentDescription = Decorative
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.width(
|
||||
MaterialTheme.spacing.itemGap
|
||||
)
|
||||
)
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.give_this_a_room_of_its_own
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
// Details
|
||||
item {
|
||||
Text(
|
||||
|
||||
@@ -31,6 +31,7 @@ import press.mantra.compose.database.repository.DatabaseMarmotRepository
|
||||
import press.mantra.compose.database.repository.DatabaseNostrRepository
|
||||
import press.mantra.compose.database.repository.DatabaseSearchRepository
|
||||
import press.mantra.compose.managers.AuxDatabaseManager
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.ui.composable.ActiveProfileScreen
|
||||
import press.mantra.compose.ui.composable.AddMemberToChatRoomConfirmationScreen
|
||||
import press.mantra.compose.ui.composable.ChatRoomCreationScreen
|
||||
@@ -605,6 +606,13 @@ fun MantraNavHost(
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
suggestedName = route.name,
|
||||
// Both or neither. A route carrying one half of a reference
|
||||
// is a reference with the resolvable part missing, and
|
||||
// reassembling it here is the one place that can say so.
|
||||
origin = route.originKind?.let { kind ->
|
||||
route.originId?.let { id -> SubgroupOrigin.Ref(kind, id) }
|
||||
},
|
||||
onNavigateToRoute = { nextRoute ->
|
||||
navController.navigate(route = nextRoute)
|
||||
},
|
||||
@@ -1318,6 +1326,7 @@ fun MantraNavHost(
|
||||
chatRoomId = route.chatRoomId,
|
||||
relayHint = route.relayHint,
|
||||
mantraRepository = databaseMantraRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
onNavigateToRoute = { actionRoute ->
|
||||
navController.navigate(
|
||||
route = actionRoute
|
||||
@@ -1359,6 +1368,7 @@ fun MantraNavHost(
|
||||
mantraRepository = databaseMantraRepository,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
frostSigningRepository = databaseFrostSigningRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
onNavigateToRoute = { actionRoute ->
|
||||
navController.navigate(
|
||||
route = actionRoute
|
||||
|
||||
@@ -16,5 +16,29 @@ import kotlinx.serialization.Serializable
|
||||
data class SelectSubgroupAdminsRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val parentChatRoomId: String,
|
||||
val relayHint: String? = null
|
||||
val relayHint: String? = null,
|
||||
|
||||
/**
|
||||
* A name to open the field with, where the caller knows one.
|
||||
*
|
||||
* Set when the subgroup was asked for from a dialect, an artifact, a
|
||||
* translation or a project -- the entity's own name, bare, with no `#`; see
|
||||
* `SubgroupOrigin`. It is a suggestion and stays editable: a group naming its
|
||||
* own room something else is not this route's business to prevent.
|
||||
*
|
||||
* Null from the room's own "Add subgroup", which knows nothing to suggest.
|
||||
*/
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* The entity the subgroup is for, flattened: a kind and an event id.
|
||||
*
|
||||
* Two nullable fields rather than one type, because a route carries no
|
||||
* domain types -- it is `@Serializable` and is reconstructed from a bundle.
|
||||
* They are reassembled into a `SubgroupOrigin.Ref` by the view model, which
|
||||
* requires both or neither: half a reference is a reference with the
|
||||
* resolvable part removed.
|
||||
*/
|
||||
val originKind: Int? = null,
|
||||
val originId: String? = null,
|
||||
): Route()
|
||||
|
||||
@@ -13,6 +13,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.view.state.ArtifactDetailUIState
|
||||
|
||||
@@ -23,6 +26,7 @@ class ArtifactDetailViewModel(
|
||||
val relayHint: String?,
|
||||
initialArtifactDetailUIState: ArtifactDetailUIState,
|
||||
val mantraRepository: MantraRepository,
|
||||
val chatRepository: ChatRepository,
|
||||
): ViewModel() {
|
||||
|
||||
var artifactDetailUIState: ArtifactDetailUIState by mutableStateOf(initialArtifactDetailUIState)
|
||||
@@ -43,11 +47,29 @@ class ArtifactDetailViewModel(
|
||||
versions = mantraRepository.getArtifactVersions(artifactId),
|
||||
chapters = mantraRepository.getChaptersForArtifact(artifactId),
|
||||
translations = mantraRepository.getTranslationsForArtifact(artifactId),
|
||||
canAddSubgroup = canAddSubgroupFor(artifact),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether a subgroup could be opened for [entity] at all.
|
||||
*
|
||||
* The two conditions `ChatRoomDetailUIState.canAddSubgroup` states, read off
|
||||
* the entity's *own* room rather than the route's: the parent of a subgroup
|
||||
* made here is the room the entity actually lives in, and
|
||||
* `SubgroupOrigin.of` is the one place that says which room that is.
|
||||
*/
|
||||
private suspend fun canAddSubgroupFor(entity: SubgroupOriginEntity): Boolean {
|
||||
val parentChatRoomId = SubgroupOrigin.of(entity).parentChatRoomId
|
||||
val room = chatRepository.getChatRoomByIdentifier(parentChatRoomId)
|
||||
|
||||
return room?.chatRoom?.mlsGroupState != null &&
|
||||
chatRepository.canSign(parentChatRoomId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ArtifactDetailViewModel"
|
||||
|
||||
@@ -58,6 +80,7 @@ class ArtifactDetailViewModel(
|
||||
relayHint: String?,
|
||||
initialArtifactDetailUIState: ArtifactDetailUIState = ArtifactDetailUIState.Loading,
|
||||
mantraRepository: MantraRepository,
|
||||
chatRepository: ChatRepository,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
ArtifactDetailViewModel(
|
||||
@@ -67,6 +90,7 @@ class ArtifactDetailViewModel(
|
||||
relayHint = relayHint,
|
||||
initialArtifactDetailUIState = initialArtifactDetailUIState,
|
||||
mantraRepository = mantraRepository,
|
||||
chatRepository = chatRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.database.model.Profile
|
||||
import press.mantra.compose.repository.FrostSigningRepository
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.repository.NostrRepository
|
||||
import press.mantra.compose.ui.view.state.ProjectDetailUIState
|
||||
@@ -28,6 +31,7 @@ class ProjectDetailViewModel(
|
||||
val mantraRepository: MantraRepository,
|
||||
val nostrRepository: NostrRepository,
|
||||
val frostSigningRepository: FrostSigningRepository,
|
||||
val chatRepository: ChatRepository,
|
||||
): ViewModel() {
|
||||
|
||||
var projectDetailUIState: ProjectDetailUIState by mutableStateOf(initialProjectDetailUIState)
|
||||
@@ -54,11 +58,29 @@ class ProjectDetailViewModel(
|
||||
project.ownerPublicKeySet() + project.creatorPublicKey
|
||||
),
|
||||
canSign = frostSigningRepository.canSign(chatRoomId),
|
||||
canAddSubgroup = canAddSubgroupFor(project),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether a subgroup could be opened for [entity] at all.
|
||||
*
|
||||
* The two conditions `ChatRoomDetailUIState.canAddSubgroup` states, read off
|
||||
* the entity's *own* room rather than the route's: the parent of a subgroup
|
||||
* made here is the room the entity actually lives in, and
|
||||
* `SubgroupOrigin.of` is the one place that says which room that is.
|
||||
*/
|
||||
private suspend fun canAddSubgroupFor(entity: SubgroupOriginEntity): Boolean {
|
||||
val parentChatRoomId = SubgroupOrigin.of(entity).parentChatRoomId
|
||||
val room = chatRepository.getChatRoomByIdentifier(parentChatRoomId)
|
||||
|
||||
return room?.chatRoom?.mlsGroupState != null &&
|
||||
chatRepository.canSign(parentChatRoomId)
|
||||
}
|
||||
|
||||
private suspend fun peopleBehind(publicKeys: Set<HexKey>): Map<HexKey, Profile> =
|
||||
publicKeys.mapNotNull { publicKey ->
|
||||
nostrRepository.getProfileWithPublicKey(publicKey)?.let { publicKey to it }
|
||||
@@ -76,6 +98,7 @@ class ProjectDetailViewModel(
|
||||
mantraRepository: MantraRepository,
|
||||
nostrRepository: NostrRepository,
|
||||
frostSigningRepository: FrostSigningRepository,
|
||||
chatRepository: ChatRepository,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
ProjectDetailViewModel(
|
||||
@@ -87,6 +110,7 @@ class ProjectDetailViewModel(
|
||||
mantraRepository = mantraRepository,
|
||||
nostrRepository = nostrRepository,
|
||||
frostSigningRepository = frostSigningRepository,
|
||||
chatRepository = chatRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import press.mantra.compose.database.model.types.ChatRoomType
|
||||
import press.mantra.compose.managers.MarmotGroupCreation
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.DkgRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ChatRoomMessagingRoute
|
||||
@@ -53,6 +54,23 @@ class SelectSubgroupAdminsViewModel(
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val chatRepository: ChatRepository,
|
||||
val dkgRepository: DkgRepository,
|
||||
/**
|
||||
* A name to open the field with, or null when the caller has nothing to
|
||||
* suggest.
|
||||
*
|
||||
* Bare, with no `#` -- see [name] for where the convention is applied.
|
||||
*/
|
||||
val suggestedName: String? = null,
|
||||
/**
|
||||
* What this subgroup is being made for, when it was asked for from a dialect,
|
||||
* an artifact, a translation or a project.
|
||||
*
|
||||
* Held from here until [confirm] rides it onto the ceremony, which is the
|
||||
* only thing this class does with it: the picker neither reads the entity nor
|
||||
* checks that it exists. A room's own "Add subgroup" passes null and the
|
||||
* subgroup is for nothing in particular, which is a normal subgroup.
|
||||
*/
|
||||
val origin: SubgroupOrigin.Ref? = null,
|
||||
): ViewModel() {
|
||||
|
||||
var uiState: SelectSubgroupAdminsUIState by mutableStateOf(initialUIState)
|
||||
@@ -73,8 +91,13 @@ class SelectSubgroupAdminsViewModel(
|
||||
* halfway through a word. It is added by `MarmotGroupName.of` where the
|
||||
* subgroup's name is actually used -- in the certificate the parent signs and
|
||||
* on the room itself -- which is idempotent, so the two agree.
|
||||
*
|
||||
* Seeded from [suggestedName] at construction rather than in [initiate],
|
||||
* because a field the user may already be typing into must not be
|
||||
* overwritten by a load completing underneath them. It stays editable after
|
||||
* that: a pre-filled name is a suggestion, not a decision.
|
||||
*/
|
||||
val name: MutableState<String> = mutableStateOf("")
|
||||
val name: MutableState<String> = mutableStateOf(suggestedName?.trim().orEmpty())
|
||||
|
||||
val threshold: MutableState<Int> = mutableStateOf(ChatRoomType.MINIMUM_QUORUM)
|
||||
|
||||
@@ -246,6 +269,15 @@ class SelectSubgroupAdminsViewModel(
|
||||
// being in it, and is not theirs to tick.
|
||||
participantPublicKeys = selectedPublicKeys.toSet(),
|
||||
subject = name.value.trim()
|
||||
// [origin] is not on the ceremony yet. It will be, and for the
|
||||
// same reason the name is: the certificate is proposed after the
|
||||
// ceremony, possibly by an admin who was never on this screen, so
|
||||
// anything it needs has to be on the stored session rather than
|
||||
// in this lambda. That needs a `DkgSession` column and a tag on
|
||||
// the proposal, which is Phase 5 of
|
||||
// docs/subgroups-for-entities.md. Until then a subgroup made from
|
||||
// an entity is named after it and nothing more, which is exactly
|
||||
// the cheap version of this feature.
|
||||
)
|
||||
|
||||
isActionPending.value = false
|
||||
@@ -293,7 +325,9 @@ class SelectSubgroupAdminsViewModel(
|
||||
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository
|
||||
dkgRepository: DkgRepository,
|
||||
suggestedName: String? = null,
|
||||
origin: SubgroupOrigin.Ref? = null
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
SelectSubgroupAdminsViewModel(
|
||||
@@ -302,7 +336,9 @@ class SelectSubgroupAdminsViewModel(
|
||||
initialUIState = initialUIState,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
dkgRepository = dkgRepository,
|
||||
suggestedName = suggestedName,
|
||||
origin = origin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.database.model.MantraChapter
|
||||
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.managers.FrostSigningManager
|
||||
import press.mantra.compose.managers.SubgroupOrigin
|
||||
import press.mantra.compose.managers.TranslationScaffold
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.FrostSigningRepository
|
||||
@@ -71,11 +73,29 @@ class TranslationArtifactVersionDetailViewModel(
|
||||
.getChaptersForArtifactVersion(translation.artifactVersionId),
|
||||
),
|
||||
canSign = frostSigningRepository.canSign(chatRoomId),
|
||||
canAddSubgroup = canAddSubgroupFor(translation),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether a subgroup could be opened for [entity] at all.
|
||||
*
|
||||
* The two conditions `ChatRoomDetailUIState.canAddSubgroup` states, read off
|
||||
* the entity's *own* room rather than the route's: the parent of a subgroup
|
||||
* made here is the room the entity actually lives in, and
|
||||
* `SubgroupOrigin.of` is the one place that says which room that is.
|
||||
*/
|
||||
private suspend fun canAddSubgroupFor(entity: SubgroupOriginEntity): Boolean {
|
||||
val parentChatRoomId = SubgroupOrigin.of(entity).parentChatRoomId
|
||||
val room = chatRepository.getChatRoomByIdentifier(parentChatRoomId)
|
||||
|
||||
return room?.chatRoom?.mlsGroupState != null &&
|
||||
chatRepository.canSign(parentChatRoomId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the group to put the chapters this translation is missing into it.
|
||||
*
|
||||
|
||||
@@ -11,6 +11,22 @@ sealed interface ArtifactDetailUIState {
|
||||
val versions: List<MantraArtifactVersion> = emptyList(),
|
||||
val chapters: List<MantraChapter> = emptyList(),
|
||||
val translations: List<MantraTranslationArtifactVersion> = emptyList(),
|
||||
|
||||
/**
|
||||
* Whether this device could open a subgroup for this artifact at all.
|
||||
*
|
||||
* The same two conditions `ChatRoomDetailUIState.canAddSubgroup` states,
|
||||
* and both are about capability rather than permission: the room has to
|
||||
* be a Marmot one, since a NIP-17 room has no admins to be a parent's,
|
||||
* and this device has to hold a share of the group's key, because
|
||||
* certifying a child is a signature by the parent and
|
||||
* `FrostSigningManager.proposeSigningBatch` throws for a device that
|
||||
* cannot make one.
|
||||
*
|
||||
* The first flag on this state, which is why it arrives with the
|
||||
* subgroup button rather than before it.
|
||||
*/
|
||||
val canAddSubgroup: Boolean = false,
|
||||
): ArtifactDetailUIState
|
||||
|
||||
data class Error(
|
||||
|
||||
@@ -31,6 +31,15 @@ sealed interface ProjectDetailUIState {
|
||||
* pressed.
|
||||
*/
|
||||
val canSign: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether this device could open a subgroup for this project at all.
|
||||
*
|
||||
* [canSign] and a Marmot room, which is the same pair
|
||||
* `ChatRoomDetailUIState.canAddSubgroup` states. Held separately rather
|
||||
* than derived at the call site so there is one spelling of one rule.
|
||||
*/
|
||||
val canAddSubgroup: Boolean = false,
|
||||
): ProjectDetailUIState
|
||||
|
||||
data class Error(
|
||||
|
||||
@@ -38,6 +38,14 @@ sealed interface TranslationArtifactVersionDetailUIState {
|
||||
|
||||
/** Whether the group holds a shared key, without which nothing can be proposed. */
|
||||
val canSign: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether this device could open a subgroup for this translation at all.
|
||||
*
|
||||
* [canSign] and a Marmot room, the same pair
|
||||
* `ChatRoomDetailUIState.canAddSubgroup` states.
|
||||
*/
|
||||
val canAddSubgroup: Boolean = false,
|
||||
): TranslationArtifactVersionDetailUIState
|
||||
|
||||
data class Error(
|
||||
|
||||
Reference in New Issue
Block a user