feat(groups): the entries the group has accepted, under the schemas on its screen
A section for the kind 31890s the group signed, read off GroupSignedEvent the way the profile, the posts, the relay lists and the schemas are. The reading gate is theirs -- the room as author, the room's real signature -- plus two of its own: an entry is matched to the list it replies to by the coordinate in its a root, and checked against that list's fields exactly as the queue checks a copy off a relay, so what this section calls accepted is what the queue will mark curated once the entry is broadcast and read back. Newest per coordinate, since re-curating is a newer event under the same d. Each card names the list, gives the queue row's glance of what the entry is, credits the suggester, and dates the group's decision; under it, the same broadcast button every signed event in the block has, gated on nothing. There is no way to add one here, because an entry is accepted from the queue on the schema's card above, and the empty state says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Pulled-From: curated/curated@732a4527c6
This commit is contained in:
@@ -611,6 +611,11 @@
|
||||
<string name="must_match_the_pattern">Must match %1$s.</string>
|
||||
<string name="at_least_one_of_these_is_needed">At least one of these is needed: %1$s.</string>
|
||||
<string name="field_and_problem">%1$s: %2$s</string>
|
||||
<!-- The group's screen: the entries it has accepted into the lists it curates. -->
|
||||
<string name="curated_entries">Curated entries</string>
|
||||
<string name="this_group_has_not_accepted_anything_into_a_list_yet">This group hasn't accepted anything into a list yet. An entry a quorum signs is shown here, and reaches the list's readers once it is broadcast.</string>
|
||||
<string name="in_list">In %1$s</string>
|
||||
<string name="suggested_by">Suggested by %1$s</string>
|
||||
<!-- Broadcasting one event the group signed to relays a member picks. -->
|
||||
<string name="broadcast">Broadcast</string>
|
||||
<string name="sends_the_event_exactly_as_the_group_signed_it">Sends the event to each relay below, exactly as the group signed it. Any member holding it can send it: the signature is the group's, not yours.</string>
|
||||
|
||||
@@ -10,8 +10,10 @@ import press.mantra.compose.managers.GroupKeyStateManager
|
||||
import press.mantra.compose.managers.MarmotGroupCreation
|
||||
import press.mantra.compose.managers.SubgroupManager
|
||||
import press.mantra.compose.nostr.GroupNostrProfile
|
||||
import press.mantra.compose.nostr.GroupCuratedEntry
|
||||
import press.mantra.compose.nostr.GroupCuratedSchema
|
||||
import press.mantra.compose.nostr.GroupPost
|
||||
import press.mantra.compose.nostr.curated.CuratedEntryEvent
|
||||
import press.mantra.compose.nostr.curated.CuratedSchemaEvent
|
||||
import press.mantra.compose.nostr.GroupRelayList
|
||||
import press.mantra.compose.nostr.GroupRelaySet
|
||||
@@ -183,6 +185,20 @@ class DatabaseChatRepository(
|
||||
emptyList()
|
||||
}
|
||||
|
||||
override suspend fun groupCuratedEntries(chatRoomId: String): List<GroupCuratedEntry> = try {
|
||||
// The schemas first, because an entry is read against the list it
|
||||
// replies to and there is nothing to read one by without them.
|
||||
GroupCuratedEntry.newestPerEntryAmong(
|
||||
signedEvents = database.groupSignedEventDao()
|
||||
.getByChatRoomIdAndKind(chatRoomId, CuratedEntryEvent.CANONICAL_KIND),
|
||||
chatRoomId = chatRoomId,
|
||||
schemas = groupCuratedSchemas(chatRoomId),
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
logger.e("Error reading the curated entries of $chatRoomId", e)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
override suspend fun groupSignedEvent(chatRoomId: String, id: HexKey): GroupSignedEvent? = try {
|
||||
database.groupSignedEventDao().getById(id)?.takeIf { it.chatRoomId == chatRoomId }
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package press.mantra.compose.nostr
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.GroupSignedEvent
|
||||
import press.mantra.compose.nostr.curated.CuratedEntry
|
||||
import press.mantra.compose.nostr.curated.CuratedEntryEvent
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* A canonical entry the group signed: a suggestion it accepted into one of
|
||||
* the lists it curates, as the list's own word.
|
||||
*
|
||||
* Kind 31890 -- see [CuratedEntryEvent] -- authored by the room's own key,
|
||||
* which is the one key a list's readers accept a canonical entry from. Where
|
||||
* the schema is the group asking and the queue is what strangers answered,
|
||||
* this is what the group *decided*: the entries the list stands behind, each
|
||||
* one a quorum's signature over a suggestion, edited or not, with a pointer
|
||||
* back at who suggested it.
|
||||
*
|
||||
* ### Read against the list it replies to
|
||||
*
|
||||
* An entry replies to a schema by coordinate, so the reading needs the
|
||||
* group's schemas as well as its signed events: the one whose coordinate the
|
||||
* entry's `a` root names is the list it belongs to, and the entry is then
|
||||
* checked against that list's fields exactly as `CuratedSuggestion.queueOf`
|
||||
* checks a copy that came back off a relay. The two readings agree by
|
||||
* construction, so what this screen shows as accepted is what the queue will
|
||||
* mark as curated once the entry has been broadcast and read back.
|
||||
*
|
||||
* An entry replying to a list this device holds no schema for is not shown:
|
||||
* without the schema there are no fields to read it by, and no way to say
|
||||
* which list it is in.
|
||||
*
|
||||
* ### One per coordinate
|
||||
*
|
||||
* The kind is addressable, so re-curating an entry -- to take in a
|
||||
* suggester's edit, or to fix the group's own -- is a newer event under the
|
||||
* same `d`, and the newest is the entry the way the newest schema is the
|
||||
* list. The coordinate is `31890:<group>:<d>` whatever list the entry replies
|
||||
* to, so the same `d` accepted into two of the group's lists is one
|
||||
* coordinate and the newer event replaces the older, on relays and here
|
||||
* alike. An identifier is the entry's rather than the list's, which is the
|
||||
* reference client's reading too: its `d` is an external id that names the
|
||||
* same film in every list.
|
||||
*
|
||||
* ### Nothing here has reached a relay
|
||||
*
|
||||
* The same caveat as the schema, and here it is the one that matters: the
|
||||
* queue screen marks a suggestion curated only from a copy read back off the
|
||||
* list's relays, so until a member broadcasts an entry from this section the
|
||||
* queue still shows it waiting. The broadcast button under each card is how
|
||||
* it gets there.
|
||||
*/
|
||||
data class GroupCuratedEntry(
|
||||
/** The group's statement, whole, with the signature that makes it one. */
|
||||
val signedEvent: GroupSignedEvent,
|
||||
/** The list the entry is in, as the group last signed its schema. */
|
||||
val list: GroupCuratedSchema,
|
||||
/** What the statement says, keyed by the list's fields, and where it came from. */
|
||||
val entry: CuratedEntry,
|
||||
) {
|
||||
/** The group's nostr identity, which for a derived room is also its id. */
|
||||
val publicKey: HexKey get() = signedEvent.publicKey
|
||||
|
||||
/** When the group signed it, which is the order entries are read in. */
|
||||
val signedAt: Instant get() = signedEvent.createdAt
|
||||
|
||||
/** The entry's `d`, which with the group's key is its coordinate. */
|
||||
val identifier: String get() = entry.identifier
|
||||
|
||||
val title: String get() = entry.title
|
||||
|
||||
/** `31890:<group>:<d>`. Two events sharing one are versions of one entry. */
|
||||
fun coordinate(): String = entry.coordinate()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The reading of one signed event against the group's [schemas], or
|
||||
* null when it is not one of these.
|
||||
*
|
||||
* Four ways to be null and they are all the same refusal: the wrong
|
||||
* kind, a signature that does not check out as [chatRoomId]'s, a root
|
||||
* naming no list this device holds, or an entry the list's own fields
|
||||
* refuse. A caller gets an entry the group really signed into a list it
|
||||
* really curates, or gets nothing.
|
||||
*/
|
||||
fun of(
|
||||
signedEvent: GroupSignedEvent,
|
||||
chatRoomId: String,
|
||||
schemas: List<GroupCuratedSchema>,
|
||||
): GroupCuratedEntry? {
|
||||
if (signedEvent.kind != CuratedEntryEvent.CANONICAL_KIND) return null
|
||||
if (!signedEvent.verifies()) return null
|
||||
|
||||
val list = schemas.firstOrNull { it.isRepliedToBy(signedEvent.tags) } ?: return null
|
||||
|
||||
val entry = CuratedEntryEvent.canonical(
|
||||
event = signedEvent.toEvent(),
|
||||
schema = list.schema,
|
||||
coordinate = list.coordinate(),
|
||||
curator = list.publicKey,
|
||||
) ?: return null
|
||||
|
||||
return GroupCuratedEntry(signedEvent = signedEvent, list = list, entry = entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* The newest version of every entry [chatRoomId] signed among
|
||||
* [signedEvents], most recently signed first.
|
||||
*
|
||||
* Newest per coordinate because the kind is addressable and a
|
||||
* re-curation is a newer event under the same `d`; newest first so that
|
||||
* what the group last decided is at the top, and the id breaking every
|
||||
* tie so that two devices reading the same events in different orders
|
||||
* show the same thing.
|
||||
*/
|
||||
fun newestPerEntryAmong(
|
||||
signedEvents: List<GroupSignedEvent>,
|
||||
chatRoomId: String,
|
||||
schemas: List<GroupCuratedSchema>,
|
||||
): List<GroupCuratedEntry> =
|
||||
signedEvents
|
||||
.mapNotNull { of(it, chatRoomId, schemas) }
|
||||
.groupBy { it.coordinate() }
|
||||
.values
|
||||
.map { versions ->
|
||||
versions.maxWith(compareBy({ it.signedAt }, { it.signedEvent.id }))
|
||||
}
|
||||
.sortedWith(
|
||||
compareByDescending<GroupCuratedEntry> { it.signedAt }
|
||||
.thenByDescending { it.signedEvent.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import press.mantra.compose.managers.SharedKeyDerivation
|
||||
import press.mantra.compose.managers.MarmotGroupCreation
|
||||
import press.mantra.compose.managers.SubgroupManager
|
||||
import press.mantra.compose.nostr.GroupNostrProfile
|
||||
import press.mantra.compose.nostr.GroupCuratedEntry
|
||||
import press.mantra.compose.nostr.GroupCuratedSchema
|
||||
import press.mantra.compose.nostr.GroupPost
|
||||
import press.mantra.compose.nostr.GroupRelayList
|
||||
@@ -125,6 +126,19 @@ interface ChatRepository {
|
||||
*/
|
||||
suspend fun groupCuratedSchemas(chatRoomId: String): List<GroupCuratedSchema>
|
||||
|
||||
/**
|
||||
* The canonical entries this group signed -- the suggestions it accepted
|
||||
* into the lists it curates -- one per coordinate, most recently signed
|
||||
* first.
|
||||
*
|
||||
* Kind 31890 is addressable, so a re-curation is a newer event under the
|
||||
* same identifier and the newest wins, the way a schema's edit does. Each
|
||||
* is read against the schema it replies to, and an entry whose list this
|
||||
* device holds no schema for is left out. Empty in a group that has
|
||||
* accepted nothing, which is every group until a quorum signs an entry.
|
||||
*/
|
||||
suspend fun groupCuratedEntries(chatRoomId: String): List<GroupCuratedEntry>
|
||||
|
||||
/**
|
||||
* One event the group signed, as the group signed it, or null when this
|
||||
* device does not hold it or it was signed in some other room.
|
||||
@@ -328,6 +342,10 @@ interface ChatRepository {
|
||||
chatRoomId: String
|
||||
): List<GroupCuratedSchema> = emptyList()
|
||||
|
||||
override suspend fun groupCuratedEntries(
|
||||
chatRoomId: String
|
||||
): List<GroupCuratedEntry> = emptyList()
|
||||
|
||||
override suspend fun groupSignedEvent(
|
||||
chatRoomId: String,
|
||||
id: HexKey
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.Autorenew
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
@@ -91,6 +92,7 @@ import press.mantra.compose.ui.composable.navigation.routes.EditGroupNostrProfil
|
||||
import press.mantra.compose.ui.composable.navigation.routes.EditGroupRelaysRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ProposalListRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SearchMemberToAddToChatRoomRoute
|
||||
import press.mantra.compose.extensions.shortened
|
||||
import press.mantra.compose.extensions.toFormattedTimeAndDateString
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
@@ -113,7 +115,11 @@ import mantra.composeapp.generated.resources.relays
|
||||
import mantra.composeapp.generated.resources.posts
|
||||
import mantra.composeapp.generated.resources.add_post
|
||||
import mantra.composeapp.generated.resources.a_reply
|
||||
import mantra.composeapp.generated.resources.curated_entries
|
||||
import mantra.composeapp.generated.resources.curated_schemas
|
||||
import mantra.composeapp.generated.resources.in_list
|
||||
import mantra.composeapp.generated.resources.suggested_by
|
||||
import mantra.composeapp.generated.resources.this_group_has_not_accepted_anything_into_a_list_yet
|
||||
import mantra.composeapp.generated.resources.this_group_has_not_published_a_curated_schema_yet
|
||||
import mantra.composeapp.generated.resources.add_schema
|
||||
import mantra.composeapp.generated.resources.view_suggestions
|
||||
@@ -143,6 +149,7 @@ import press.mantra.compose.ui.composable.navigation.routes.SelectSubgroupAdmins
|
||||
import press.mantra.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
|
||||
import press.mantra.compose.managers.SubgroupManager
|
||||
import press.mantra.compose.nostr.GroupCuratedEntry
|
||||
import press.mantra.compose.nostr.GroupCuratedSchema
|
||||
import press.mantra.compose.nostr.GroupNostrProfile
|
||||
import press.mantra.compose.nostr.GroupPost
|
||||
@@ -588,10 +595,8 @@ fun ChatRoomDetailScreen(
|
||||
// thing a group publishes for strangers to answer:
|
||||
// a post is the group speaking, a schema is the
|
||||
// group asking -- for entries to a list it will
|
||||
// then curate, by quorum, under the same key. It
|
||||
// closes the identity block because everything in
|
||||
// that block is signed by that key and read by
|
||||
// people who were never in the room.
|
||||
// then curate, by quorum, under the same key. What
|
||||
// it curated follows, in the section below.
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.curated_schemas),
|
||||
@@ -727,6 +732,57 @@ fun ChatRoomDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Under the schemas, because these are what a
|
||||
// schema is *for*: the schema asks, strangers
|
||||
// answer on the queue, and these are the answers
|
||||
// the group put its key to. They close the
|
||||
// identity block because everything in it is
|
||||
// signed by that key and read by people who were
|
||||
// never in the room, and an accepted entry is the
|
||||
// last thing in that chain.
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.curated_entries),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
|
||||
if (chatRoomDetailUIState.groupCuratedEntries.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.this_group_has_not_accepted_anything_into_a_list_yet
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(
|
||||
items = chatRoomDetailUIState.groupCuratedEntries,
|
||||
key = { accepted -> accepted.signedEvent.id }
|
||||
) { accepted ->
|
||||
// The card and its broadcast as one item,
|
||||
// so the button stays under the entry it
|
||||
// sends. No gate on either: reading what
|
||||
// the group decided takes no share of the
|
||||
// key, and neither does repeating its
|
||||
// signature to a relay. There is no way
|
||||
// in to add one here, because an entry
|
||||
// is accepted from the queue it was
|
||||
// suggested to, on the schema's card
|
||||
// above.
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
GroupCuratedEntryCard(accepted = accepted)
|
||||
|
||||
BroadcastButton(
|
||||
onClick = { broadcast(accepted.signedEvent) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The untyped way into everything above it. Each
|
||||
// section has a form that builds one kind of event;
|
||||
// this takes any of those kinds as JSON, pasted from
|
||||
@@ -1835,6 +1891,87 @@ private fun GroupCuratedSchemaCard(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry the group accepted into a list it curates, as the list's own word.
|
||||
*
|
||||
* The title first, because every list has one; then which list, since a group
|
||||
* may curate several and an entry says nothing about its list on its face;
|
||||
* then the glance the queue row gives -- the fields that say what the entry
|
||||
* *is*, labelled as the schema labels them, and not the links -- and the body
|
||||
* where the list has one. The last line credits the suggester, when there was
|
||||
* one, and dates the group's decision, which is the fact a member reading the
|
||||
* block came for.
|
||||
*
|
||||
* Not a way in to anything: the entry is revised from the queue it was
|
||||
* suggested to, where the suggestion it came from and the "Curated" mark sit
|
||||
* side by side, and the card's one action is the broadcast under it.
|
||||
*/
|
||||
@Composable
|
||||
private fun GroupCuratedEntryCard(accepted: GroupCuratedEntry) {
|
||||
val schema = accepted.list.schema
|
||||
val entry = accepted.entry
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = Decorative,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = accepted.title,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.relatedGap)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.in_list, schema.displayName()),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
glanceOf(entry, schema)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
bodyOf(entry, schema)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = listOfNotNull(
|
||||
entry.source?.let { stringResource(Res.string.suggested_by, it.pubKey.shortened()) },
|
||||
stringResource(
|
||||
Res.string.signed_by_the_group_on,
|
||||
accepted.signedAt.toFormattedTimeAndDateString()
|
||||
)
|
||||
).joinToString(" · "),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One subgroup, as the parent's record and this device's rooms together have it.
|
||||
*
|
||||
|
||||
@@ -418,9 +418,10 @@ private fun CuratedMark() {
|
||||
* The fields a form prompts for, less the title (the headline), the body (its
|
||||
* own line) and anything that is a link (the sheet). Labelled because a bare
|
||||
* "2014 · en" says nothing to somebody who does not know the list's fields,
|
||||
* and this screen is driven by a schema it has never seen before.
|
||||
* and this screen is driven by a schema it has never seen before. Shared with
|
||||
* the group's screen, so an entry is described the same way on both.
|
||||
*/
|
||||
private fun glanceOf(entry: CuratedEntry, schema: CuratedSchema): String? =
|
||||
internal fun glanceOf(entry: CuratedEntry, schema: CuratedSchema): String? =
|
||||
schema.formFields()
|
||||
.filterNot { it.tag() == TITLE_TAG || it.tag() == CuratedFieldConfig.CONTENT_TAG }
|
||||
.filterNot { it.type == CuratedFieldType.Url || it.type == CuratedFieldType.Image }
|
||||
@@ -431,7 +432,7 @@ private fun glanceOf(entry: CuratedEntry, schema: CuratedSchema): String? =
|
||||
?.joinToString(" · ")
|
||||
|
||||
/** The value of the field that writes to the body, or null. */
|
||||
private fun bodyOf(entry: CuratedEntry, schema: CuratedSchema): String? =
|
||||
internal fun bodyOf(entry: CuratedEntry, schema: CuratedSchema): String? =
|
||||
schema.fields.firstOrNull { it.tag() == CuratedFieldConfig.CONTENT_TAG }?.let { entry.value(it.name) }
|
||||
|
||||
private const val TITLE_TAG = "title"
|
||||
|
||||
@@ -92,6 +92,7 @@ class ChatRoomDetailViewModel(
|
||||
groupRelayLists = chatRepository.groupRelayLists(chatRoomId),
|
||||
groupPosts = chatRepository.groupPosts(chatRoomId),
|
||||
groupCuratedSchemas = chatRepository.groupCuratedSchemas(chatRoomId),
|
||||
groupCuratedEntries = chatRepository.groupCuratedEntries(chatRoomId),
|
||||
canEditNostrProfile = canSign,
|
||||
canAddSubgroup = canSign,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import press.mantra.compose.database.model.GroupSignedEvent
|
||||
import press.mantra.compose.database.model.MantraArtifact
|
||||
import press.mantra.compose.database.model.MantraDialect
|
||||
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
|
||||
import press.mantra.compose.nostr.GroupCuratedEntry
|
||||
import press.mantra.compose.nostr.GroupCuratedSchema
|
||||
import press.mantra.compose.nostr.GroupNostrProfile
|
||||
import press.mantra.compose.nostr.GroupPost
|
||||
@@ -96,6 +97,16 @@ sealed interface ChatRoomDetailUIState {
|
||||
* proposes a schema.
|
||||
*/
|
||||
val groupCuratedSchemas: List<GroupCuratedSchema> = emptyList(),
|
||||
/**
|
||||
* The entries this group has accepted into the lists it curates, most
|
||||
* recently signed first.
|
||||
*
|
||||
* Under the schemas because they are what a schema is *for*: the schema
|
||||
* asks, strangers suggest, and these are what the group decided to
|
||||
* stand behind. Empty in a group that has accepted nothing, which is
|
||||
* every group until a quorum signs an entry.
|
||||
*/
|
||||
val groupCuratedEntries: List<GroupCuratedEntry> = emptyList(),
|
||||
/**
|
||||
* Whether this device could sign a profile for the group.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user