Skip to content

Wire kind-1009 message editing into the conversation#152

Merged
mubarakcoded merged 8 commits into
masterfrom
feature/message-editing
Jun 12, 2026
Merged

Wire kind-1009 message editing into the conversation#152
mubarakcoded merged 8 commits into
masterfrom
feature/message-editing

Conversation

@mubarakcoded

@mubarakcoded mubarakcoded commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Wires kind-1009 message editing into the Android client. Sender taps Edit on their own kind-9 chat, the composer pre-fills with the message's current text, submitting publishes a kind-1009 edit which the timeline picks up and the bubble rebinds to in place.

What changes

  • Bindings refresh. Native .so regenerated across all four ABIs and
    marmot_uniffi.kt re-emitted so the new Marmot.editMessage(...) FFI
    surface is callable from Kotlin.
  • MessageEdits — pure aggregator that walks the timeline, groups
    kind-1009 events by their target message id (single e tag per spec),
    applies client-side authorship enforcement (edit signer must match
    target signer, case-insensitive on the hex pubkey), drops blank
    replacements, and orders versions chronologically. Returns a
    target → EditState(latestText, count, versions[]) map.
  • MessageProjectorKindEdit = kind-1009 constant, isEdit
    predicate, editTargetMessageId extractor.
  • ControllersprojectedPreviewText skips kind-1009 (in-place
    edits must never bump the chat-list "last message" or reorder the
    conversation, per the Rust runtime's docstring on edit_message).
    Unread-count helpers (firstUnreadReceivedIndex,
    countUnreadIncoming) skip derived-state kinds (1009 edits + 1210
    group system events) so an edit doesn't inflate the badge or shift the
    read anchor away from real chat.
  • ConversationControllereditsByTarget map recomputed on every
    timeline publish; editingMessageId mutable state; editMessage()
    posts the kind-1009 via marmotIo; displayedText(record) returns
    the latest edited text when one exists. Submitting the composer with
    edit-mode active short-circuits the normal send path and routes through
    editMessage().
  • MessageActionMenu — new "Edit" action button gated on
    canEdit = mine && record.kind == 9uL && record.messageIdHex.isNotBlank() && !deleted.
  • ComposerBareditingMessageId + editingInitialText + onCancelEdit
    parameters. Snapshots the in-flight draft on edit-mode entry so cancel
    restores it; pre-fills the input with the message's current text on
    entry. Edit banner ("Editing message" with cancel-X) replaces the
    reply banner when edit mode is active (mutually exclusive).
  • Bubble text overlay — bubble body reads through controller.editsByTarget
    so any edit echoes into the rendered text in place; reply previews
    read through the same path automatically.
  • "(edited · N)" tap-for-history affordance — tappable label next to the
    bubble timestamp opens a ModalBottomSheet listing each version with its
    relative timestamp, newest first, with the original at the bottom.
  • Timeline skipMessageProjector.isEdit skip in the bubble timeline
    iteration so kind-1009 events never render as a standalone bubble (same
    derived-state trap kind-1210 hit at last review).
  • Strings in all 10 locales.

Test plan

  • ./gradlew :app:testDebugUnitTest :app:ktlintMainSourceSetCheck :app:assembleDebug — green
  • 8 new MessageEditsTest cases (empty, single edit, multiple, authorship rejection, case-insensitive hex match, blank rejected, missing target, multi-target)
  • Installed debug APK on Pixel; long-press → Edit → modify → send → bubble shows new text on the original row, no phantom bubble, no chat-list bump, no unread inflation
  • Cancel-edit restores the in-flight draft
  • History modal shows every prior version with timestamps + the original at the bottom

Depends on

  • marmot-protocol/darkmatter#299 — the uniffi edit_message export plus the storage-sqlite materialization of kind-1009 into the message timeline. Without that storage fix the FFI accepts the send but the row never reaches the timeline subscription. The .so + Kotlin binding in this branch are regenerated from feature/uniffi-edit-message and carry that fix.

Open in Stage

Summary by CodeRabbit

  • New Features

    • In-place message editing with edit mode in the composer, edit action in message menu, and opt-in publish-as-edit behavior.
    • Edit history sheet showing prior versions and timestamps; edited messages show latest text and an “edited” indicator with count.
  • Localization

    • Added UI strings for editing and edit history across many locales.
  • Tests

    • Added unit tests covering edit aggregation, ordering, authorship checks, and edge cases.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mubarakcoded, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 hours, 44 minutes, and 53 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afbd82c0-278e-4edb-b168-37b9944d69b6

📥 Commits

Reviewing files that changed from the base of the PR and between 63e8567 and f9354a3.

📒 Files selected for processing (4)
  • app/src/main/java/dev/ipf/darkmatter/core/MessageEdits.kt
  • app/src/main/java/dev/ipf/darkmatter/core/MessageProjector.kt
  • app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt
  • app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt

Walkthrough

Adds in-place message editing: aggregates edit records into per-message edit state, surfaces edited text in message bubbles and an edit-history sheet, provides composer edit mode and publish path, hides edit events as standalone rows, and exposes a Rust FFI to send edits.

Changes

In-Place Message Editing

Layer / File(s) Summary
Edit history data model and aggregation
app/src/main/java/dev/ipf/darkmatter/core/MessageEdits.kt, app/src/test/java/dev/ipf/darkmatter/core/MessageEditsTest.kt
EditVersion and EditState represent edit history. aggregateEdits(...) scans edit records, resolves targets, enforces case-insensitive authorship matching, drops blank replacements, sorts by recordedAt, and builds targetId -> EditState. Tests cover filtering, ordering, authorship validation, blank-content dropping, missing-target skipping, and per-target aggregation.
Message kind edit recognition
app/src/main/java/dev/ipf/darkmatter/core/MessageProjector.kt
Adds KindEdit = 1009uL, isEdit(record) predicate, and editTargetMessageId(record) to parse the e tag and return the referenced target message id for edit records.
Chat list and unread count adjustments
app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt (partial)
ChatListItem.projectedPreviewText routes edit-derived previews through the original message projection. firstUnreadReceivedIndex and countUnreadIncoming skip derived-state kinds when computing unread anchors and counts; isDerivedStateKind() classifies derived kinds.
Conversation controller edit mode and publishing
app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt (partial)
ConversationController adds editsByTarget (derived via aggregateEdits()), editingMessageId, short-circuits send() to publish edits via editMessage(), and exposes displayedText() to return edited content when present.
Conversation UI message editing flow
app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt
Composer is seeded for editing with draft snapshot/restore and an "editing message" banner; MessageBubble displays editState.latestText for edited messages, shows an "(edited …)" affordance that opens EditHistorySheet, and hides edit events as standalone bubbles. MessageActionMenu exposes an Edit action that enters edit mode.
Rust FFI edit publishing
app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
Adds public MarmotInterface.editMessage(...) and Marmot.editMessage(...) implementation that calls the new async Rust FFI marmot_edit_message to publish edits; updates JNA extern and checksum verification.
Localization and test support
app/src/main/res/values*/strings.xml, app/src/test/java/dev/ipf/darkmatter/LocalizationResourceTest.kt
Adds eight new string keys for editing UI across default + locales (de, es, fr, it, pt, ru, tr, zh, zh-Hant) and permits two keys to be identical in localized resources.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Wire kind-1009 message editing into the conversation' directly and clearly summarizes the main change: integrating message editing functionality (kind-1009 events) throughout the conversation UI and controller logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/message-editing

Comment @coderabbitai help to get the list of available commands and usage tips.

@stage-review

stage-review Bot commented Jun 12, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 6 individual chapters for you:

Title
1 Update UniFFI bindings for message editing
2 Define core message edit logic and aggregation
3 Integrate edits into conversation state management
4 Add localized strings for edit features
5 Update conversation UI for message editing
6 Other changes
Open in Stage

Chapters generated by Stage for commit f9354a3 on Jun 12, 2026 4:52pm UTC.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt (1)

5738-5745: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Edited captions never render for messages with attachments.

The menu now allows editing any owned kind-9 message, but the media-caption branch still uses record.plaintext instead of the edit-aware body. For an image/document message, the edit publishes successfully yet the bubble keeps showing the original caption.

💡 Suggested fix
                         val bodyTextToRender: String? =
                             when {
                                 // Deleted/invalidated tombstones show only the
                                 // tombstone copy, never an inline image/caption.
                                 deleted || invalidated -> displayedBody
                                 mediaPendingName != null && !anyConfirmedMedia -> null
-                                anyConfirmedMedia -> record.plaintext.takeIf { it.isNotBlank() }
+                                anyConfirmedMedia ->
+                                    editState?.latestText?.takeIf { it.isNotBlank() }
+                                        ?: record.plaintext.takeIf { it.isNotBlank() }
                                 else -> displayedBody
                             }

Also applies to: 5822-5841

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt` around lines 5738 -
5745, The media-caption branch uses record.plaintext (old caption) so edited
captions aren’t shown; update the anyConfirmedMedia branch inside the
bodyTextToRender when-expression to use the edit-aware displayedBody (or same
edit-resolved value used elsewhere) instead of record.plaintext, and make the
identical change at the other occurrence referenced (the block around lines
5822-5841) to ensure edited captions render for messages with attachments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt`:
- Around line 6442-6455: When entering edit mode (LaunchedEffect block that sets
preEditDraft/text) you must prevent normal draft persistence and send completion
side effects from running while editing: stop onDraftChange from updating the
persisted draft and prevent submit logic from clearing the persisted draft or
invoking onAfterSend when editingMessageId != null; instead keep changes only in
the transient text/preEditDraft and, on cancel or successful edit-submit,
restore preEditDraft and only then re-enable normal draft persistence and call
onAfterSend if appropriate. Apply this guard around the onDraftChange handler
and the submit/clear logic (the code that clears stored drafts and calls
onAfterSend) referenced by editingMessageId and the LaunchedEffect, and mirror
the same behavior in the other similar blocks mentioned (the sections around the
other LaunchedEffect usages).

---

Outside diff comments:
In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt`:
- Around line 5738-5745: The media-caption branch uses record.plaintext (old
caption) so edited captions aren’t shown; update the anyConfirmedMedia branch
inside the bodyTextToRender when-expression to use the edit-aware displayedBody
(or same edit-resolved value used elsewhere) instead of record.plaintext, and
make the identical change at the other occurrence referenced (the block around
lines 5822-5841) to ensure edited captions render for messages with attachments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 832fa244-02c4-47ed-b297-7845920c4ef1

📥 Commits

Reviewing files that changed from the base of the PR and between a895309 and 507d2b8.

⛔ Files ignored due to path filters (4)
  • app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86_64/libmarmot_uniffi.so is excluded by !**/*.so
📒 Files selected for processing (17)
  • app/src/main/java/dev/ipf/darkmatter/core/MessageEdits.kt
  • app/src/main/java/dev/ipf/darkmatter/core/MessageProjector.kt
  • app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt
  • app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt
  • app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
  • app/src/main/res/values-b+zh+Hant/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-pt/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/dev/ipf/darkmatter/LocalizationResourceTest.kt
  • app/src/test/java/dev/ipf/darkmatter/core/MessageEditsTest.kt

Comment thread app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt
@mubarakcoded mubarakcoded changed the title Wire kind-1010 message editing into the conversation Wire kind-1009 message editing into the conversation Jun 12, 2026
@mubarakcoded

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

@mubarakcoded Sure! I'll kick off a full review of this PR right away.

✅ Action performed

Full review finished.

@mubarakcoded
mubarakcoded marked this pull request as ready for review June 12, 2026 16:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt (1)

4158-4170: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Filter edit records before they enter the lazy list.

return@items skips composition, but it does not remove the lazy-list slot. With verticalArrangement = Arrangement.spacedBy(8.dp), each kind-1009 record can still leave a visible gap, so edits still affect transcript layout even though the bubble is hidden. The safer fix is to derive one rendered timeline and use that same filtered list for the LazyColumn and any index/scroll math that targets visible rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt` around lines 4158 -
4170, The lazy list is still reserving slots for edit records because you call
return@items inside items; instead, filter out edits before composing the
LazyColumn so they never enter the list. Create a derived list (e.g. val
renderedTimeline = controller.timeline.filterNot {
MessageProjector.isEdit(it.record) }) and use renderedTimeline (with
items(renderedTimeline, key = { it.id })) for the LazyColumn and any
index/scroll calculations (such as the unread-divider logic and
entryFirstUnreadMessageId lookups) so visible row indexes and spacing match the
rendered rows.
♻️ Duplicate comments (1)
app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt (1)

6547-6560: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep edit mode out of normal draft persistence and send side effects.

While editingMessageId != null, keystrokes still flow through onDraftChange(...), and submit still clears the stored draft plus runs onAfterSend(). That leaves the persisted draft wrong after cancel/submit and still triggers the “scroll to newest” path for an in-place edit.

Proposed fix
             OutlinedTextField(
                 value = text,
-                onValueChange = {
-                    text = it
-                    onDraftChange(it)
+                onValueChange = { value ->
+                    text = value
+                    if (editingMessageId == null) {
+                        onDraftChange(value)
+                    }
                 },
                 modifier = Modifier.weight(1f),
                 placeholder = { Text(stringResource(R.string.message)) },
                 maxLines = 5,
@@
             FloatingActionButton(
                 onClick = {
                     if (text.isNotBlank()) {
+                        val sendingEdit = editingMessageId != null
                         onSend(text)
-                        text = ""
-                        onDraftChange("")
-                        onAfterSend()
+                        if (!sendingEdit) {
+                            text = ""
+                            onDraftChange("")
+                            onAfterSend()
+                        }
                     }
                 },

Also applies to: 6663-6693

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt` around lines 6547 -
6560, When entering edit mode (editingMessageId != null) stop routing keystrokes
and send-side effects through the normal draft persistence and post-send path:
add an early guard in the draft change handler (the onDraftChange/handler that
writes persisted drafts) to return immediately when editingMessageId != null so
typing only updates the local text and preEditDraft, and modify the submit/send
handler (the function that clears persisted draft and calls onAfterSend) to
branch on editingMessageId — when non-null do not clear the persisted draft or
call onAfterSend but instead run the edit-commit flow (update message/edit
state) and then restore/clear preEditDraft as appropriate; apply the same
guard/branching to any other places that trigger scroll-to-newest or persistence
(the other LaunchedEffect/submit-like path referenced in the diff) so edits do
not affect normal draft persistence or trigger send-side effects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt`:
- Around line 6117-6158: The edit history sheet currently lays out all entries
in a plain Column causing unbounded content to overflow and become unreachable;
replace the inner static Column that iterates rows with a scrollable list (e.g.,
LazyColumn or Column + verticalScroll) inside ModalBottomSheet so older
revisions can be scrolled to. Locate the current ModalBottomSheet block and
change the child that contains rows.forEach to a LazyColumn (using items or
itemsIndexed on rows) or wrap the Column with
Modifier.verticalScroll(rememberScrollState()) and ensure proper padding/spacing
is preserved (keep the Texts that use IdentityFormatter.relativeTime and
stringResource unchanged).

---

Outside diff comments:
In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt`:
- Around line 4158-4170: The lazy list is still reserving slots for edit records
because you call return@items inside items; instead, filter out edits before
composing the LazyColumn so they never enter the list. Create a derived list
(e.g. val renderedTimeline = controller.timeline.filterNot {
MessageProjector.isEdit(it.record) }) and use renderedTimeline (with
items(renderedTimeline, key = { it.id })) for the LazyColumn and any
index/scroll calculations (such as the unread-divider logic and
entryFirstUnreadMessageId lookups) so visible row indexes and spacing match the
rendered rows.

---

Duplicate comments:
In `@app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt`:
- Around line 6547-6560: When entering edit mode (editingMessageId != null) stop
routing keystrokes and send-side effects through the normal draft persistence
and post-send path: add an early guard in the draft change handler (the
onDraftChange/handler that writes persisted drafts) to return immediately when
editingMessageId != null so typing only updates the local text and preEditDraft,
and modify the submit/send handler (the function that clears persisted draft and
calls onAfterSend) to branch on editingMessageId — when non-null do not clear
the persisted draft or call onAfterSend but instead run the edit-commit flow
(update message/edit state) and then restore/clear preEditDraft as appropriate;
apply the same guard/branching to any other places that trigger scroll-to-newest
or persistence (the other LaunchedEffect/submit-like path referenced in the
diff) so edits do not affect normal draft persistence or trigger send-side
effects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 567d5af9-2540-418e-b404-a3dd34f8d928

📥 Commits

Reviewing files that changed from the base of the PR and between 507d2b8 and 63e8567.

⛔ Files ignored due to path filters (4)
  • app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86_64/libmarmot_uniffi.so is excluded by !**/*.so
📒 Files selected for processing (5)
  • app/src/main/java/dev/ipf/darkmatter/core/MessageProjector.kt
  • app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt
  • app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt
  • app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
  • app/src/test/java/dev/ipf/darkmatter/core/MessageEditsTest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/test/java/dev/ipf/darkmatter/core/MessageEditsTest.kt
  • app/src/main/java/dev/ipf/darkmatter/state/Controllers.kt
  • app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
  • app/src/main/java/dev/ipf/darkmatter/core/MessageProjector.kt

Comment thread app/src/main/java/dev/ipf/darkmatter/ui/DarkMatterApp.kt
… the lazy list, refined history sheet, caret-at-end on entry
@mubarakcoded
mubarakcoded merged commit b3a31d8 into master Jun 12, 2026
1 check passed
@mubarakcoded
mubarakcoded deleted the feature/message-editing branch June 12, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant