refactor(groups): take out the group's nostr identity and the curated lists, and keep broadcast unreachable
Some checks failed
Material Design conformance / budgets (push) Has been cancelled
Material Design conformance / tests (push) Has been cancelled

Lines B, D and H of docs/curated-to-mantra.md, pulled from the Curated fork this
morning, go again this afternoon: the group's nostr profile (kind 0), its four relay
lists (NIP-65, NIP-17, NIP-50, NIP-51), its posts (kind 1), the curated schemas it
publishes (31889), the suggestions it reads (31888) and the entries it accepts
(31890). Eighty-two files, twelve screens, the `nostr/curated/` package, the readings
(`GroupNostrProfile`, `GroupRelayList`, `GroupPost`, `GroupCuratedSchema`,
`GroupCuratedEntry`, `CuratedSuggestion`), the `applyInnerEvent` arms for all eight
kinds, the `ChatRepository` and `NostrRepository` reads that fed them, 226 strings,
and every test that came with them. A translation collective has no list to curate
and no reason to describe itself under a key no member controls, and five rows
saying so on every group's screen were five rows about somebody else's product.

**The pasted-event proposal goes with them, because it was theirs.**
`GroupEventProposal.ACCEPTED_KINDS` was exactly kinds 0, 1, the four relay-list
kinds and 31889 -- "the kinds the group's screen has a place for", in its own words --
so with the five rows gone it would have refused every paste. Keeping it as a
generic "sign any event" was considered and rejected: the screen's whole argument
was that a member goes to the row to see the event landed, and there is no row. The
`ProposedEvent` summaries for the same kinds go too; the one test of its pre-existing
fallback ("Event of kind N") is kept, as the only line of `ProposedEventTest` that
was about code this repository had before the pull.

**Broadcast stays, and nothing opens it.** `BroadcastGroupSignedEventScreen`, its
route, view model, state and `BroadcastButton` are kept, on the decision that a way
to send a group-signed event to relays is worth having against the day something
wires a button in -- the button's only call sites were the five removed screens.
Two things had to change for it to compile against a tree with no relay lists:
`defaultRelaysFor(kind)` is now the app's own publish set for every kind, since the
General list it preferred, the Blocked list it filtered by and the schema relays it
widened to no longer exist; and `relayUrlOrNull` moves into the view model's
companion from the deleted `GroupRelaySet`, unchanged. The hint on the screen says
"the relays this app publishes to" rather than "where the group has said it lives".
Its two tests are rewritten around the new seed: the screen test answers for the
first two seeded relays and counts the rest as asked, and empties the list by hand
to see the empty state, since the seed always has something in it. Deleting
broadcast outright -- the tidier tree -- was the recommendation and was declined.

**Every pre-existing file is back at its pre-pull content plus the kept lines'
hunks, and nothing else.** Eleven files -- `ChatMessage.kt`, `NostrEventDao.kt`,
`NostrEvent.kt`, `LocalChatRoom.kt`, `Member.kt`, `ProposedEvent.kt`,
`ChatTranscript.kt`, `ProfileAvatar.kt`, the group screen's view model and state,
and `m3-title-case.py` -- were touched by no kept commit and are restored from
ba0830a3 byte for byte, so `inComparableGroups` is a private helper of the group
screen again rather than a shared extension one deleted screen needed, and
`ProfileAvatar` has one overload again. The rest were restored and had the kept
hunks re-applied: the back button's three on `ChatRoomDetailScreen`, broadcast's
`groupSignedEvent` read on the two chat repositories, and on the two nostr
repositories the sign-in reads, by reverse-applying the queue commit's hunk. The
check is `git diff ba0830a3 -- <file>`, which shows only those. Reverting the eight
commits was rejected because the back button and the read-only identity work landed
on top of them and would have conflicted in every one of the shared files; editing
the current files by hand was rejected because it leaves residue that a diff against
the base cannot distinguish from a decision.

**The strings that went are exactly the ones nothing references any more and that
the pull added.** Six strings were unreferenced before the pull and stay; the header
sentence and one capitalised "Mantra" that the queue commit's join carried are prose,
not feature, and stay too. The seven section comments that described removed blocks
go; the broadcast block's stays.

**The plan's third decision said "hide the two rows behind a constant".** That
covered the curated rows and not the profile, relays and posts beside them, and the
call was to take the whole identity block out. The record of what went and what
stayed is the paragraph after the built table in docs/curated-to-mantra.md, with the
seam it leaves for the next pull: an upstream commit that touches the identity block
conflicts at `ChatRoomDetailScreen`, `MantraNavHost` and `strings.xml`, and is
dropped. docs/README.md says the same in a sentence. The nsec and npub notes still
name `EditGroupCuratedSchemaViewModel`; they are records of what was built upstream
and are left as written.

Verified with :composeApp:compileDebugKotlinAndroid, :composeApp:compileKotlinJvm,
:composeApp:jvmTest (826 tests, from 1,039), :composeApp:testDebugUnitTest (413,
from 530) and :composeApp:m3Audit, every budget met, 12 adaptive uses at the floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-13 14:45:15 +02:00
parent 0fa807a6fb
commit 776455ecb0
109 changed files with 176 additions and 20910 deletions

View File

@@ -398,250 +398,17 @@
<string name="check_the_combined_result">Check the combined result and confirm it</string>
<string name="the_group_has_its_key_finish_setting_it_up">The group has its key — finish setting it up</string>
<string name="open">Open</string>
<string name="nostr_profile">Nostr profile</string>
<string name="this_group_has_not_said_anything_about_itself">This group hasn't said anything about itself on Nostr yet.</string>
<!-- The row on the group's screen that opens the profile, when there is none to name. -->
<string name="not_described_yet">Not described yet</string>
<string name="edit_nostr_profile">Edit Nostr profile</string>
<string name="propose_nostr_profile">Propose Nostr profile</string>
<string name="what_the_group_says_about_itself_on_nostr">What the group says about itself on Nostr</string>
<string name="the_group_signs_its_profile_so_it_takes_a_quorum">The group signs this, so it takes a quorum. An empty field unsays what the group had said.</string>
<string name="this_group_has_no_shared_key_to_sign_a_profile">This group has no shared key, so it cannot sign a profile. Run a shared key ceremony first.</string>
<string name="signed_by_the_group_on">Signed by the group on %1$s</string>
<string name="no_name_on_this_profile">No name on this profile</string>
<string name="about">About</string>
<string name="picture_url">Picture url</string>
<string name="website">Website</string>
<string name="nostr_address">Nostr address</string>
<string name="lightning_address">Lightning address</string>
<string name="eg_translation_collective">eg. Translation collective</string>
<string name="eg_a_group_translating_hard_books">eg. A group translating hard books into Sesotho</string>
<string name="eg_https_example_com_group_png">eg. https://example.com/group.png</string>
<string name="eg_https_example_com">eg. https://example.com</string>
<string name="eg_group_example_com">eg. group@example.com</string>
<string name="eg_group_getalby_com">eg. group@getalby.com</string>
<string name="could_not_ask_the_group_to_sign_this_profile">Couldn't ask the group to sign this profile.</string>
<string name="relays">Relays</string>
<string name="edit_relays">Edit relays</string>
<string name="not_set_yet">Not set yet</string>
<string name="no_relays_in_this_list">No relays in this list</string>
<!-- The row on the group's screen that opens the relay lists: how many of the four the group has signed. -->
<string name="n_of_m_relay_lists_agreed">%1$s of %2$s lists agreed</string>
<string name="where_the_group_lives_on_nostr">Where the group lives on Nostr</string>
<string name="the_group_signs_each_list_so_it_takes_a_quorum">The group signs each list, so it takes a quorum. Only the lists you change are proposed.</string>
<string name="this_group_has_no_shared_key_to_sign_relays">This group has no shared key, so it cannot sign a relay list. Run a shared key ceremony first.</string>
<string name="relay_url">Relay url</string>
<string name="eg_wss_relay_example_com">eg. wss://relay.example.com</string>
<string name="add_relay">Add relay</string>
<string name="that_is_not_a_relay_address">That isn't a relay address. Relays start with wss:// and cannot be on this device.</string>
<string name="that_relay_is_already_in_this_list">That relay is already in this list.</string>
<string name="propose_relays">Propose relays</string>
<string name="nothing_has_changed_to_propose">Nothing has changed, so there is nothing to propose.</string>
<string name="could_not_ask_the_group_to_sign_these_relays">Couldn't ask the group to sign these relays.</string>
<string name="relay_read">Read</string>
<string name="relay_write">Write</string>
<string name="relay_set_general">General</string>
<string name="relay_set_messages">Messages</string>
<string name="relay_set_search">Search</string>
<string name="relay_set_blocked">Blocked</string>
<string name="relay_set_general_purpose">Where the group publishes, and where a reader should look for it.</string>
<string name="relay_set_messages_purpose">Where a message addressed to the group should be sent.</string>
<string name="relay_set_search_purpose">The relays a search of the group's work runs against.</string>
<string name="relay_set_blocked_purpose">Relays the group will not talk to.</string>
<string name="relays_are_public_a_group_cannot_encrypt">A group signs and cannot decrypt, so every list here is public — including the blocked one, which most clients keep private.</string>
<string name="signed_by_the_group_on_short">Signed %1$s</string>
<string name="posts">Posts</string>
<string name="add_post">Add post</string>
<string name="propose_post">Propose post</string>
<string name="this_group_has_not_posted_anything_yet">This group hasn't posted anything yet.</string>
<!-- The row on the group's screen that opens the posts. -->
<string name="no_posts_yet">No posts yet</string>
<string name="one_post">1 post</string>
<string name="n_posts">%1$s posts</string>
<string name="what_should_the_group_say">What should the group say?</string>
<string name="the_group_signs_a_post_so_it_takes_a_quorum">The group signs a post, so it takes a quorum. It goes out in the group's name, not yours.</string>
<string name="this_group_has_no_shared_key_to_sign_a_post">This group has no shared key, so it cannot sign a post. Run a shared key ceremony first.</string>
<string name="a_post_needs_something_to_say">A post needs something to say.</string>
<string name="could_not_ask_the_group_to_sign_this_post">Couldn't ask the group to sign this post.</string>
<string name="posted_on">Posted %1$s</string>
<string name="a_reply">A reply</string>
<string name="curated_schemas">Curated schemas</string>
<string name="this_group_has_not_published_a_curated_schema_yet">This group hasn't published a curated schema yet. A schema defines a list anyone can suggest entries to and only the group can curate.</string>
<!-- The row on the group's screen that opens the schemas. -->
<string name="no_schemas_yet">No schemas yet</string>
<string name="one_schema">1 schema</string>
<string name="n_schemas">%1$s schemas</string>
<string name="add_schema">Add schema</string>
<string name="edit_schema">Edit schema</string>
<string name="propose_schema">Propose schema</string>
<string name="fields_colon">Fields: %1$s</string>
<string name="visibility_public">Public</string>
<string name="visibility_closed">Closed</string>
<string name="visibility_private">Private</string>
<string name="visibility_public_purpose">Anyone may suggest an entry, and anyone may read the list.</string>
<string name="visibility_closed_purpose">Only the group and the suggesters below may suggest. Anyone may read the list.</string>
<string name="visibility_private_purpose">Only the group and the suggesters below may suggest, and clients show the list to them alone. Relays are open, so this is a convention rather than a secret.</string>
<string name="what_an_entry_in_this_list_may_contain">What an entry in this list may contain, and who may suggest one</string>
<string name="the_group_signs_a_schema_so_it_takes_a_quorum">The group signs a curated schema, so it takes a quorum. Suggestions reply to it, and only the group can accept them into the list.</string>
<string name="this_group_has_no_shared_key_to_sign_a_schema">This group has no shared key, so it cannot sign a curated schema. Run a shared key ceremony first.</string>
<string name="could_not_ask_the_group_to_sign_this_schema">Couldn't ask the group to sign this schema.</string>
<string name="identifier">Identifier</string>
<string name="eg_bitcoin_mov">eg. bitcoin.mov</string>
<string name="the_identifier_names_this_list_in_every_reply">The identifier names this list in every reply to it, so it can't change. Add a schema to start another list.</string>
<string name="title">Title</string>
<string name="eg_bitcoin_mov_suggestion">eg. bitcoin.mov suggestion</string>
<string name="description">Description</string>
<string name="eg_fields_for_a_bitcoin_mov_entry">eg. Fields for a bitcoin.mov entry: a film, with at least one link to watch it</string>
<string name="visibility">Visibility</string>
<string name="domain">Domain</string>
<string name="eg_example_com">eg. example.com</string>
<string name="a_domain_replaces_the_name_wherever_the_list_is_shown">A domain replaces the name wherever the list is shown. It's a claim rather than proof, so only set one the group controls.</string>
<string name="fields">Fields</string>
<string name="add_field">Add field</string>
<string name="required">Required</string>
<string name="optional">Optional</string>
<string name="derived">Derived</string>
<string name="at_least_one_of">At least one of</string>
<string name="add_rule">Add rule</string>
<string name="pick_two_or_more_fields_at_least_one_of_which_an_entry_must_have">Pick two or more fields, at least one of which an entry must have. A rule with fewer than two is dropped.</string>
<string name="suggesters">Suggesters</string>
<string name="pubkeys_besides_the_group_that_may_suggest_to_this_list">Pubkeys besides the group that may suggest to this list.</string>
<string name="npub_or_hex_pubkey">npub or hex pubkey</string>
<string name="add_suggester">Add suggester</string>
<string name="that_is_not_a_pubkey">That isn't a pubkey. Paste an npub or 64 hex characters.</string>
<string name="that_pubkey_is_already_a_suggester">That pubkey is already a suggester.</string>
<string name="where_suggestions_to_this_list_are_published">Where suggestions and entries are published to and read from. Signed into the schema, so a reply can't be sent anywhere else.</string>
<string name="no_relays_named_so_a_client_will_pick_its_own">No relays named, so a client will pick its own.</string>
<string name="new_field">New field</string>
<string name="edit_field">Edit field</string>
<string name="field_name">Field name</string>
<string name="eg_watchurl">eg. watchUrl</string>
<string name="label">Label</string>
<string name="eg_watch_reference_url">eg. Watch / reference url</string>
<string name="placeholder">Placeholder</string>
<string name="eg_https_youtube_com_watch">eg. https://youtube.com/watch?v=…</string>
<string name="type">Type</string>
<string name="an_entry_must_have_this_field">An entry must have this field</string>
<string name="writes_to_tag">Writes to tag</string>
<string name="defaults_to_the_field_name_content_means_the_body">Defaults to the field name. "content" means the event's body.</string>
<string name="marker">Marker</string>
<string name="eg_watch">eg. watch</string>
<string name="the_tag_s_third_element_which_tells_fields_sharing_a_tag_apart">The tag's third element, which tells fields sharing a tag apart.</string>
<string name="maximum_characters">Maximum characters</string>
<string name="maximum_value">Maximum value</string>
<string name="minimum_value">Minimum value</string>
<string name="options">Options</string>
<string name="one_per_line_or_separated_by_commas">One per line, or separated by commas.</string>
<string name="https_only">https only</string>
<string name="may_appear_more_than_once">May appear more than once</string>
<string name="pattern">Pattern</string>
<string name="a_regular_expression_the_whole_value_must_match">A regular expression the whole value must match.</string>
<string name="filled_in_by_the_app_never_asked_for">Filled in by the app, never asked for</string>
<string name="hint">Hint</string>
<string name="helper_text_under_the_input">Helper text under the input.</string>
<string name="done">Done</string>
<string name="remove_field">Remove field</string>
<string name="a_field_needs_a_name">A field needs a name.</string>
<string name="a_field_name_is_one_word">A field name is one word, with no spaces.</string>
<string name="a_field_named_that_already_exists">A field named that already exists.</string>
<string name="an_identifier_is_required">An identifier is required.</string>
<string name="the_identifier_must_be_at_most_n_characters">The identifier must be at most %1$s characters.</string>
<string name="a_title_is_required">A title is required.</string>
<string name="the_title_must_be_at_most_n_characters">The title must be at most %1$s characters.</string>
<string name="a_name_is_required">A name is required.</string>
<string name="the_name_must_be_at_most_n_characters">The name must be at most %1$s characters.</string>
<string name="a_description_is_required">A description is required.</string>
<string name="the_description_must_be_at_most_n_characters">The description must be at most %1$s characters.</string>
<string name="say_who_may_suggest_to_this_list">Say who may suggest to this list.</string>
<string name="the_picture_must_be_an_https_url">The picture must be an https url.</string>
<string name="that_is_not_a_domain_name">That isn't a domain name.</string>
<string name="a_schema_needs_at_least_one_field">A schema needs at least one field.</string>
<string name="one_of_the_relays_is_not_a_relay_address">One of the relays isn't a relay address.</string>
<string name="eg_2100">eg. 2100</string>
<string name="eg_200">eg. 200</string>
<string name="eg_1900">eg. 1900</string>
<string name="eg_movie_documentary_short">eg. movie, documentary, short</string>
<string name="eg_a_za_z_2_8">eg. [A-Za-z]{2,8}</string>
<string name="field_type_text">Text</string>
<string name="field_type_longtext">Long text</string>
<string name="field_type_token">Token</string>
<string name="field_type_url">Url</string>
<string name="field_type_image">Image</string>
<string name="field_type_enum">One of a list</string>
<string name="field_type_year">Year</string>
<string name="field_type_duration">Duration in seconds</string>
<string name="field_type_number">Number</string>
<string name="propose_event">Propose event</string>
<string name="paste_an_unsigned_event_for_the_group_to_sign">Paste an unsigned Nostr event for the group to sign. It is filed by kind: a profile (kind 0), a post (kind 1), a relay list, or a curated schema (kind 31889).</string>
<string name="the_group_signs_an_event_so_it_takes_a_quorum">The group signs it, so it takes a quorum. The author is the group and the time is now, whatever the paste says about either.</string>
<string name="this_group_has_no_shared_key_to_sign_an_event">This group has no shared key, so it cannot sign an event. Run a shared key ceremony first.</string>
<string name="could_not_ask_the_group_to_sign_this_event">Couldn't ask the group to sign this event.</string>
<string name="unsigned_event">Unsigned event</string>
<string name="eg_kind_1_tags_content">{"kind": 1, "tags": [], "content": "…"}</string>
<string name="what_the_group_would_sign">What the group would sign</string>
<string name="kind_n">Kind %1$s</string>
<string name="that_is_not_json">That isn't JSON.</string>
<string name="an_event_is_a_json_object">An event is a JSON object with a kind, tags and content.</string>
<string name="an_event_needs_a_kind">An event needs a kind.</string>
<string name="kind_n_has_nowhere_to_go_on_this_screen">Kind %1$s has nowhere to go on the group's screen. It can file kinds %2$s.</string>
<string name="tags_must_be_a_list_of_lists_of_strings">Tags must be a list of lists of strings.</string>
<string name="content_must_be_text">Content must be text.</string>
<string name="the_content_is_not_a_profile">The content isn't a profile. A kind 0 carries a JSON object of profile fields.</string>
<string name="this_schema_cannot_be_signed_as_it_is">This schema can't be signed as it is:</string>
<string name="view_suggestions">View suggestions</string>
<string name="suggestions">Suggestions</string>
<string name="everything_anyone_has_suggested_to_this_list">Everything anyone has suggested to this list, newest first. Only the group can take one up into the list.</string>
<string name="one_suggestion">1 suggestion</string>
<string name="n_suggestions">%1$s suggestions</string>
<string name="n_curated">%1$s curated</string>
<string name="read_from_relays">Read from %1$s</string>
<string name="asking_the_list_s_relays">Asking the list's relays…</string>
<string name="nobody_has_suggested_anything_to_this_list_yet">Nobody has suggested anything to this list yet.</string>
<string name="ask_again">Ask again</string>
<string name="curated">Curated</string>
<string name="suggested_by_on">Suggested by %1$s on %2$s</string>
<string name="taken_up_by_the_group_into_the_list">Taken up by the group into the list.</string>
<string name="accept_this_suggestion">Accept this suggestion</string>
<string name="revise_the_list_s_entry">Revise the list's entry</string>
<!-- Taking one suggestion into a list the group curates: the form, and the entry it proposes. -->
<string name="take_this_suggestion_into_the_list">Take this suggestion into %1$s, edited as the group wants it. The entry keeps the suggestion's identifier and points back at it, so the suggester is credited.</string>
<string name="the_group_signs_an_entry_so_it_takes_a_quorum">The group signs an entry, so it takes a quorum. Nothing changes until one has.</string>
<string name="this_group_has_no_shared_key_to_sign_an_entry">This group has no shared key, so it cannot sign an entry. Run a shared key ceremony first.</string>
<string name="the_group_already_has_an_entry_for_this">The group already has an entry for this. Proposing again revises it in place rather than adding a second one.</string>
<string name="propose_entry">Propose entry</string>
<string name="could_not_ask_the_group_to_sign_this_entry">Couldn't ask the group to sign this entry. Please try again.</string>
<string name="this_entry_cannot_be_signed_as_it_is">This entry can't be signed as it is:</string>
<string name="the_entry_cannot_be_signed_as_it_is">The entry can't be signed as it is.</string>
<string name="kept_from_the_suggestion">Kept from the suggestion</string>
<string name="filled_in_by_the_suggester_s_app_and_kept">Filled in by the suggester's app and kept as they came, the identifier among them, so that an entry accepted twice lands on the same coordinate.</string>
<string name="add_another">Add another</string>
<string name="this_field_is_required">This field is required.</string>
<string name="only_one_value_for_this_field">Only one value for this field.</string>
<string name="one_word_without_spaces">One word, without spaces.</string>
<string name="that_is_not_a_web_address">That is not a web address.</string>
<string name="that_must_be_an_https_address">That must be an https address.</string>
<string name="pick_one_of_the_options">Pick one of the options.</string>
<string name="a_whole_number">A whole number.</string>
<string name="more_than_zero">More than zero.</string>
<string name="at_least_n">At least %1$s.</string>
<string name="at_most_n">At most %1$s.</string>
<string name="at_most_n_characters">At most %1$s characters.</string>
<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>
<!-- The row on the group's screen that opens the entries. -->
<string name="no_entries_yet">No entries yet</string>
<string name="one_entry">1 entry</string>
<string name="n_entries">%1$s entries</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>
<string name="where_the_group_has_said_it_lives_is_filled_in">Where the group has said it lives is filled in. Add or remove relays before sending.</string>
<string name="the_relays_this_app_publishes_to_are_filled_in">The relays this app publishes to are filled in. Add or remove relays before sending.</string>
<string name="no_relays_to_broadcast_to">No relays to broadcast to. Add one above.</string>
<string name="add_a_relay_to_broadcast_to">Add a relay to broadcast to.</string>
<string name="relay_sending">Sending…</string>