An Add Translation button in the artifact detail's Translations section
opens a workflow that requires selecting or creating a dialect (reusing the
chip picker + create-new-dialect fields), then scaffolds the translation.
- MantraRepository.addTranslation creates a MantraTranslationArtifactVersion
for the artifact's latest version and the chosen dialectId (inheriting the
source artifact's name/visibility/license), then mirrors the source
structure: a MantraTranslationChapter per chapter and a
MantraTranslationChunk (empty text scaffold) per chunk. Each created entity
also gets a MarmotInnerEvent rumor (kinds 30306/30308/30309). When a new
dialect is requested the ViewModel creates it first via addDialect.
- The three translation events' build methods now take real fields (fixing
their phantom generics); toXEvent tag order matches build and
fromXEventTemplate companions are added so the event ids round-trip.
- New AddTranslationScreen (route + ViewModel + UIState). On success it lands
on a freshly-loaded artifact detail via popUpTo<ArtifactDetailRoute>.
No MantraTranslation rows / translation text are created here — that is the
per-chunk authoring step, intentionally left out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a chapter in the artifact detail now opens a chapter detail screen.
- New ChapterDetailRoute + ChapterDetailScreen/ViewModel/UIState. The screen
shows the chapter's index and word/character counts, its original markdown
text (rendered as plain text — no markdown renderer available), and the
chapter's paragraph chunks (index, text, counts) with an empty state.
- DAO queries getChapterById and getChunksByChapterId (validated by Room),
exposed via MantraRepository.getChapter / getChunksForChapter (impl + NO_OP).
- ArtifactDetailScreen chapter cards are now clickable, navigating to
ChapterDetailRoute; MantraNavHost registers the route.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a chapter (markdown original text) to an artifact from its detail
screen, deriving word/character counts from the text and splitting it into
paragraph chunks.
- MantraRepository.addChapter attaches the chapter to the artifact's latest
version (index = existing chapter count), computes word/character counts,
and persists the MantraChapter plus a MarmotInnerEvent rumor
(kind = ChapterEvent.KIND). It then splits the markdown by paragraph into
MantraChunks, each with its own index/counts and a rumor
(kind = ChunkEvent.KIND). New DAO query getChaptersByArtifactVersionId.
- New press.mantra.compose.text.Markdown helpers: wordCount, characterCount,
splitParagraphs (blank-line separated).
- ChapterEvent.build / ChunkEvent.build now take the real fields (fixing the
phantom generics); toChapterEvent / toChunkEvent tag order matches build
and fromChapterEventTemplate / fromChunkEventTemplate are added so the
event ids round-trip, mirroring the other models.
- New AddChapterScreen (route + ViewModel + UIState) with a name field and a
markdown text field plus a live "words · characters · chunks" preview,
reached from an Add Chapter button in ArtifactDetailScreen (disabled until
the artifact has a version). On success it lands on a freshly-loaded
artifact detail via popUpTo<ArtifactDetailRoute>.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking an artifact in a chat room's library now opens an artifact
detail screen instead of the placeholder route.
- New ArtifactDetailRoute + ArtifactDetailScreen/ViewModel/UIState. The
screen shows the artifact's details (name, url, visibility, license,
dialect, author, created), its versions, and — walked via the artifact's
versions — the associated chapters and translations, each with an empty
state.
- DAO queries (validated by Room): getArtifactById, versions by artifactId,
chapters by artifactId (Chapter JOIN ArtifactVersion, ordered by index),
and translations by artifactId (TranslationArtifactVersion JOIN
ArtifactVersion). Exposed via MantraRepository (impl + NO_OP).
- ChatRoomDetailScreen navigates to ArtifactDetailRoute on artifact click;
MantraNavHost registers the route with a back-stack pop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Library section hardcoded "No artifacts exists". Load and render the
chat room's stored artifacts instead.
- MantraArtifactDao.getArtifactsByChatRoomId + MantraRepository.getArtifacts
expose a chat room's artifacts (newest first).
- ChatRoomDetailUIState.Loaded carries artifacts; ChatRoomDetailViewModel
gains mantraRepository and loads them in initiateChatRoomDetail.
- ChatRoomDetailScreen renders the empty-state text only when there are no
artifacts, otherwise a Card/ListItem per artifact (name, url, chevron to a
pending detail route). The screen takes mantraRepository, threaded through
the factory, the nav host (databaseMantraRepository), and the preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MantraArtifact requires a dialectId (FK), but the add-artifact screen had
no way to provide one. Let the user either reuse an existing source
dialect or create a new one inline.
- DatabaseMantraRepository.addDialect builds a DialectEvent, derives a
MantraDialect with the real chatRoomId/userPublicKey, and persists it
plus a MarmotInnerEvent rumor (kind = DialectEvent.KIND) for the
outbound pipeline, mirroring addArtifact. getDialects exposes the chat
room's dialects via a new MantraDialectDao query.
- MantraDialect.fromDialectEventTemplate mirrors the other models for
consistent id derivation; DialectEvent.build now takes name/country/
language (emitting NameTag/CountryTag/LanguageTag), and toDialectEvent's
tag order matches build so the event id round-trips. Also fixes the
DialectEvent.build / toDialectEvent phantom generics.
- AddArtifactViewModel loads the dialects on init and its addArtifact now
takes existingDialectId: reuse it when set, otherwise create a new
dialect (fields required only in create mode), then create the artifact.
- AddArtifactScreen shows a FilterChip row (one chip per existing dialect
plus a "New dialect" chip); the name/country/language fields appear only
when creating a new dialect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verify inbound nostr signatures (C1): NostrEvent.fromEvent now calls
quartz Event.verify() (id-hash integrity + Schnorr) and drops the event
on failure. fromEvent is the single inbound parse choke point, so forged
events from a malicious/compromised relay can no longer reach the DB
(profile overwrites, fake notes/reactions, etc.).
Persist + retain PrivateMessage commits (H2): applyCommit's
PRIVATE_MESSAGE branch captured no epoch secrets and never persisted the
advance, so decrypt()'s inline commit was lost on reload and prior-epoch
messages became undecryptable. Capture retainedSecrets before decrypt and
pushRetainedEpoch + persistGroup on a successful advance, matching
decryptMessageBytes/processCommit.
Fix membership/admin reconciliation inversions (H3/M1/M2) in
processGroupMembershipChanges: new-member filter now negates correctly so
new joiners are persisted; the new-member admin flag uses == true instead
of == null (previously marked everyone admin when adminPubkeys was absent,
nobody when present); removedAdmins uses != true so admins actually
dropped from the list are demoted instead of stripping current admins.
Document retained-epoch impersonation (C2) and plaintext-at-rest (H1):
these can't be fixed in-app (RetainedEpochSecrets carries no tree/leaf sig
keys/groupContext to verify a past-epoch signature; encryption-at-rest
needs a platform keystore). Add prominent SECURITY warnings/TODOs at
tryDecryptWithRetainedEpoch, the ncryptsec* key-package fields, and the
mlsGroupState persistence, with the required fix in each case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
addArtifact was a stub: it returned null unconditionally (so the
ViewModel's success path never fired), called a TODO() addArtifactVersion
that threw mid-flow, and passed empty chatRoomId/userPublicKey — yielding
a wrong event id and foreign-key violations.
- Persist the MantraArtifact with the real chatRoomId and userPublicKey,
plus a MarmotInnerEvent rumor (marmotGroupEventId IS NULL) that the
outbound pipeline encrypts into the group, mirroring sendChatMessage.
Set the inner event kind to ArtifactEvent.KIND so it matches the hashed
id (it previously defaulted to ChatEvent.KIND).
- Implement addArtifactVersion (was TODO) and create the initial version;
add MantraArtifactVersion.fromArtifactVersionEventTemplate mirroring
MantraArtifact for consistent id derivation.
- Return the created inner event so the caller can detect success; wrap
writes in try/catch to fail without crashing.
Thread the required dialectId plus chatRoomId/userPublicKey and the
visibility/license params (defaulting to private/cc) through
MantraRepository and AddArtifactViewModel. The ViewModel now validates
inputs up front, routes exceptions to onFailure, clears fields only on
success, and uses isActionPending to block double submits. The screen
passes dialectId = null with a TODO until a dialect picker exists; the
flow fails gracefully in the meantime.
Also fix the ArtifactVersionEvent.build phantom generic
(TagArrayBuilder<ArtifactEvent> -> <ArtifactVersionEvent>) so it returns
the correct EventTemplate type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The artifact/dialect/chapter/translation-version events packed several
independent attributes into single positional tags
(["artifact", url, visibility, license], ["dialect", name, country,
language], etc.). Positional packing made every field mandatory (a
missing or malformed field nulled the whole metadata blob), blocked
extension, and forced ArtifactMetadataTag and
TranslationArtifactVersionMetadataTag to share TAG_NAME "artifact" with
field 1 meaning url vs name.
Replace the four compound tags with seven shared single-field tags
(NameTag, UrlTag, VisibilityTag, LicenseTag, CountryTag, LanguageTag,
OriginalTextTag), following the NIP-23 one-tag-per-field convention.
Event accessors now read each field independently and build()/toXEvent()
emit one tag per field; from...Event() readers let-chain the per-field
accessors. This also removes the duplicate "artifact" tag-name collision.
Note: breaking wire-format change for kinds 30300/30302/30304/30306;
safe now since these events are not published yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WordStatisticsTag.parse used has(3) but the tag has only 3 elements
(indices 0-2), so the guard never passed and parse always returned null
— silently breaking all Chapter and Chunk event parsing. Correct to
has(2).
Also replace unsafe tag[n].toInt() with toIntOrNull() in WordStatisticsTag
and IndexTag so a malformed numeric field from a relay returns null
instead of throwing NumberFormatException up through the parse loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>