Commit Graph

438 Commits

Author SHA1 Message Date
Kgothatso Ngako
c0897645ae Merge branch 'claude/sweet-keller-9c933e' into artifacts 2026-07-27 01:23:14 +02:00
Kgothatso Ngako
3356e58c06 Fix notary observer lifecycle and robustness bugs
- Observers now run as children of collectLatest keyed on the derived
  nostr private key: previously every active-wallet emission spawned
  four more eternal collectors on the app scope, and after a wallet
  switch stale collectors kept signing with the old key (duplicate
  signatures, gift wraps and key package bundles).
- Follow the keyManager StateFlow instead of snapshotting .value, so
  the notary still starts when the key loads after the wallet emits.
- Guard per-item processing so one failing row logs instead of killing
  the collector (and the queue) for the rest of the session.
- Derive the real nsecPassword for self-healed key package bundles via
  a new PrivateKey.nsecPassword() extension instead of passing "".
- Fix a copy-pasted log tag in observeUnprocessedMarmotInnerEvents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:22:40 +02:00
Kgothatso Ngako
2259b25f3f Remember scopes and repositories in MantraNavHost
The coroutine scopes, AuxDatabaseManager, and repositories were
created as plain vals in the composable body, so every recomposition
recreated them — leaking the old scopes' jobs and duplicating
repository instances. Wrap them in remember so they are created once
per composition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:21:56 +02:00
Kgothatso Ngako
0503464f6a Fix profile creation bugs
- platformWriteSeed (Android + iOS) invoked onSeedWritten twice on
  success, doubling wallet switch/navigation and navigating from the
  IO dispatcher; keep only the main-thread invocation.
- Gate profile event creation on the seed actually being written to
  disk: writeSeed now reports success/error, so a failed seed write no
  longer leaves orphaned unsigned events the notary can never sign.
- Stop rethrowing from createAccount's CoroutineExceptionHandler
  (crashed the app); failures now show the Error state and reset the
  pending flag instead of spinning forever. Add a re-entry guard
  against double taps.
- Reset WritingSeedState after a completed attempt so retries are not
  silently skipped, and record WrittenToDisk on success.
- Derive the nostr key with NodeParamsManager.chain instead of a
  hardcoded Chain.Mainnet.
- Build the SearchRelayListEvent from DefaultSearchRelayList instead
  of DM relays, and drop its empty privateTags array that caused a
  pointless NIP-44 encrypted empty list in content.
- Compare pubKey, privateTags and signedAt in UnsignedNostrEvent
  equals/hashCode so distinctUntilChanged cannot conflate rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 01:09:50 +02:00
Kgothatso Ngako
5e92e7f613 Minor bug fixes 2026-07-27 00:52:22 +02:00
Kgothatso Ngako
57652b98e1 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts
# Conflicts:
#	composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/AddArtifactViewModel.kt
2026-07-26 03:04:33 +02:00
Kgothatso Ngako
5621493b98 Open the new artifact after adding it
On a successful add-artifact, navigate to the artifact detail for the newly
created artifact instead of the implementation-pending screen. The ViewModel
now passes the created artifact's id (the returned inner event's id) to
onSuccess, and the screen opens ArtifactDetailRoute via the pop-inclusive
callback so the add form leaves the back stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 03:03:10 +02:00
Kgothatso Ngako
66aabc24e0 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:50:25 +02:00
Kgothatso Ngako
5973ab1ca5 Reorder translation detail: chapters first
Move the Chapters section above the Details section on the
TranslationArtifactVersionDetailScreen so the actionable content leads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:50:06 +02:00
Kgothatso Ngako
1391462640 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:46:48 +02:00
Kgothatso Ngako
3370e354c3 Reorder artifact detail: chapters and translations first
Move the Chapters and Translations sections to the top of the artifact
detail screen and the descriptive Details and Versions sections to the
bottom, so the actionable content leads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:46:16 +02:00
Kgothatso Ngako
c41c7083f5 Fix for the correct thread again 2026-07-26 02:42:31 +02:00
Kgothatso Ngako
b0c523c0f0 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:41:29 +02:00
Kgothatso Ngako
dc23295d6e Rename TranslationDetail screen family to TranslationArtifactVersionDetail
Rename the screen and its ViewModel, UI state, and route to reflect that
they describe a MantraTranslationArtifactVersion:
- TranslationDetailScreen -> TranslationArtifactVersionDetailScreen
- TranslationDetailViewModel -> TranslationArtifactVersionDetailViewModel
- TranslationDetailUIState -> TranslationArtifactVersionDetailUIState
- TranslationDetailRoute -> TranslationArtifactVersionDetailRoute

Files moved with git mv to preserve history; all references (nav host,
artifact detail screen) updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:40:42 +02:00
Kgothatso Ngako
90c6194071 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:30:46 +02:00
Kgothatso Ngako
c494cf9e11 Add per-chunk translation editor
The translation column of the chapter table is now a TextButton (with a
ChevronRight) per chunk; tapping it opens an editor for that chunk.

- TranslateChunkScreen (route + screen/ViewModel/UIState) shows the full
  original chunk and a text input prefilled with any existing translation.
  Saving persists the translation and returns to a freshly-loaded chapter
  table (popUpTo<TranslationChapterRoute>) so the update shows.
- MantraRepository.saveTranslationChunk builds a MantraTranslationChunk from
  the entered text (content-hash id, index mirrored from the source chunk),
  upserts it plus its MarmotInnerEvent rumor, and replaces any prior
  translation chunk for the same source chunk (deleting the stale row and its
  rumor) so there is exactly one per source chunk. New DAO queries getChunkById,
  MantraTranslationChunkDao.deleteById, MarmotInnerEventDao.deleteById, and
  repository getChunk.
- TranslationChapterScreen renders the translation cell as the button and
  navigates to TranslateChunkRoute with the source chunkId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:29:58 +02:00
Kgothatso Ngako
72f0233bed Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:19:56 +02:00
Kgothatso Ngako
2170f434dc Add TranslationChapter screen with side-by-side chunks
Chapter cards in the translation detail now open a two-column chapter
translation screen.

- The screen loads the source MantraChapter's chunks paired with their
  MantraTranslationChunks (by chunkId). Column one is the original chunks,
  column two the translated chunks; the header row shows the original dialect
  name and the translation dialect name. When a chunk has no translation
  (missing or blank text), the original text is shown greyed out as a
  placeholder in the second column.
- The ViewModel resolves the original dialect by walking chapter -> version
  -> artifact -> dialect and the translation dialect via the translation
  version. New DAO queries getArtifactVersionById and getTranslationChapterById
  plus repository accessors getArtifactVersion/getTranslationChapter/getDialect.
- New TranslationChapterRoute + screen/ViewModel/UIState; TranslationDetail
  chapter cards navigate to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:19:30 +02:00
Kgothatso Ngako
05e200319a Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 02:06:06 +02:00
Kgothatso Ngako
e2fe415a1a Add MantraTranslationArtifactVersion detail screen
Translation cards in the artifact detail's Translations list are now
clickable, opening a translation detail screen.

- New TranslationDetailRoute + TranslationDetailScreen/ViewModel/UIState.
  The screen shows the translation's details (name, dialect, visibility,
  license, source version, author, created) and its chapters ordered by
  index, each with translation progress (translated/total chunks, where a
  chunk counts as translated once its text is non-empty).
- DAO queries getTranslationById, translation chapters by
  translationArtifactVersionId, and translation chunks by
  translationChapterId (validated by Room), exposed via MantraRepository
  (impl + NO_OP).
- Extract the DetailRow composable (was private to ArtifactDetailScreen)
  into a shared widgets/DetailRow.kt used by both detail screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:05:55 +02:00
Kgothatso Ngako
f097d71237 Name a translation after its dialect
addTranslation set the MantraTranslationArtifactVersion name to the source
artifact's name; use the selected dialect's name instead (visibility and
license still inherit from the artifact). Add a getDialectById query to look
the dialect up by id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:59:32 +02:00
Kgothatso Ngako
de75823426 Merge branch 'claude/admiring-mahavira-a862d1' into artifacts 2026-07-26 01:53:02 +02:00
Kgothatso Ngako
99f99fa959 Add the add-translation workflow from the artifact detail screen
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>
2026-07-26 01:52:15 +02:00
Kgothatso Ngako
6b07bcd78f Apply correct thread 2026-07-26 01:39:15 +02:00
Kgothatso Ngako
b2664bf7a5 Add ChapterDetailScreen showing chapter text and chunks
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>
2026-07-26 01:34:12 +02:00
Kgothatso Ngako
f8e4a5b158 Add the add-chapter flow with paragraph chunking
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>
2026-07-26 01:28:30 +02:00
Kgothatso Ngako
d671c7c300 Add ArtifactDetailScreen with chapters and translations
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>
2026-07-26 01:10:22 +02:00
Kgothatso Ngako
308a28c073 List locally stored artifacts on the ChatRoomDetailScreen
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>
2026-07-26 00:57:53 +02:00
Kgothatso Ngako
24a00f793a Add dialect selection/creation to the add-artifact flow
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>
2026-07-26 00:42:59 +02:00
Kgothatso Ngako
12189dcd75 Address high-severity MLS/nostr review findings
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>
2026-07-26 00:29:29 +02:00
Kgothatso Ngako
07e1db950a Implement DatabaseMantraRepository.addArtifact
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>
2026-07-26 00:17:17 +02:00
Kgothatso Ngako
8e3c1cffe2 Split nip30303 compound metadata tags into per-field tags
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>
2026-07-26 00:06:12 +02:00
Kgothatso Ngako
c45a310641 Fix nip30303 tag parsing bugs
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>
2026-07-25 23:56:18 +02:00
Kgothatso Ngako
0d79c3ecee Basic logic for adding new artifact 2026-07-24 23:13:47 +02:00
Kgothatso Ngako
9444f8280b Some foundational logic to add artifact 2026-07-22 23:12:36 +02:00
Kgothatso Ngako
499bd54704 Add artifact UI flow 2026-07-20 02:30:15 +02:00
Kgothatso Ngako
3a830d77d6 Add chat room id on mantra tables 2026-07-19 21:08:44 +02:00
Kgothatso Ngako
3640a77413 Add more associations 2026-07-19 20:58:11 +02:00
Kgothatso Ngako
03c3e56b26 Bug fix for translationChapter primary key 2026-07-19 19:49:07 +02:00
Kgothatso Ngako
0bb421aefc Save groupEvent 2026-07-19 19:45:22 +02:00
Kgothatso Ngako
9cf95483e8 Consume inbound mantra events 2026-07-19 19:38:13 +02:00
Kgothatso Ngako
1b19c6a1be Create nostr events from database data 2026-07-19 19:15:42 +02:00
Kgothatso Ngako
ed70df2c11 Consume mantra nostr events 2026-07-19 03:21:38 +02:00
Kgothatso Ngako
9cd06f3b40 Add associations 2026-07-19 02:47:47 +02:00
Kgothatso Ngako
d2b64b5d91 Maintain contributors with role attributor instead of exact role having it's own class 2026-07-17 21:28:39 +02:00
Kgothatso Ngako
803b9b6755 Relay hints and such 2026-07-17 11:05:55 +02:00
Kgothatso Ngako
bdcf685537 Make mantra tags event references 2026-07-17 10:44:02 +02:00
Kgothatso Ngako
3bc12ca344 Add tag builders 2026-07-17 01:02:29 +02:00
Kgothatso Ngako
9b2022e79f Add dao for mantra objects 2026-07-16 00:08:01 +02:00
Kgothatso Ngako
5bd85a5750 Use ListItem on Profile 2026-07-15 23:46:09 +02:00