Skip to content

edit message mobile sdks#3345

Closed
mchenani wants to merge 1 commit intographite-base/3345from
mc/edit-message-mobile-sdks
Closed

edit message mobile sdks#3345
mchenani wants to merge 1 commit intographite-base/3345from
mc/edit-message-mobile-sdks

Conversation

@mchenani
Copy link
Copy Markdown
Contributor

@mchenani mchenani commented Mar 17, 2026

Add edit message support to mobile and WASM SDKs

  • Adds editMessage and deleteMessage methods to FfiConversation (mobile), Node Conversation, and WASM Conversation, returning event IDs.
  • Adds streamMessageEdits to FfiConversations (mobile), Node Conversations, and WASM Conversations, streaming edited messages via a callback.
  • FfiDecodedMessage and DecodedMessage now include editedAtNs and isEdited fields; the content getter returns edited content when present, preserving reply threading semantics.
  • Introduces EditMessageCodec and EditMessageRequest for iOS and Android, encoding/decoding xmtp.org/editMessage v1.0 content; codec produces no fallback string and does not request push delivery.
  • Updates generated iOS Swift bindings in xmtpv3.swift to use 64-bit handle-based FFI throughout.
  • Risk: content on previously-received decoded messages now returns edited content rather than original content if an edit is present, which is a behavioral change for existing consumers.

Macroscope summarized d666e51.

This was referenced Mar 17, 2026
Copy link
Copy Markdown
Contributor Author

mchenani commented Mar 17, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add the label mergequeue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Claude finished @mchenani's task —— View job


Code Review Complete

Review Date: March 17, 2026, 22:31 UTC

  • Review Android codec implementation
  • Review iOS codec implementation
  • Review test coverage
  • Provide feedback on issues beyond Macroscope findings

Critical Issues (Already Identified by Macroscope)

The two issues flagged by Macroscope are blocking and must be fixed:

  1. Android (EditMessageCodec.kt:82): NPE risk when Content.Compression.forNumber() returns null for unrecognized compression values
  2. iOS (EditMessageCodec.swift:81-101): Missing compression field reconstruction in decode - causes decompression to be skipped after round-trip

Additional Issues Found

1. Inconsistent null handling between platforms

Android (EditMessageCodec.kt:45):

compression = encoded.compression?.let { it.number }

Encodes compression, but the decode at line 82 has the NPE issue Macroscope found.

iOS (EditMessageCodec.swift:69):

compression: encoded.hasCompression ? Int32(encoded.compression.rawValue) : nil

Encodes compression, but decode (lines 81-101) completely drops it.

Both codecs fail to properly round-trip the compression field, but for different reasons.

2. Test gap: No compression testing

Neither platform's tests verify that editedContent with compression round-trips correctly:

  • EditMessageCodecTest.kt: All tests use editedContent = null
  • EditMessageCodecTests.swift: All tests use default editedContent without compression

Recommendation: Add test cases that encode content with compression (GZIP/DEFLATE) inside editedContent and verify it decodes correctly. This would have caught both compression bugs.

3. Android: Potential issue with compressionValue setter

In ContentCodec.kt:89, there's a utility function encodedContentFromFfi that uses:

builder.compressionValue = it

However, EditMessageCodec.kt:82 uses:

setCompression(Content.Compression.forNumber(it))

The existing codebase pattern in encodedContentFromFfi is more direct and handles unknown values gracefully by just storing the int value. Consider aligning with this pattern instead of using forNumber().

4. Android: Unused import

EditMessageCodec.kt:3 imports Content but the code uses fully qualified Content.Compression and Content.EncodedContent. The import can be removed or the references can be shortened.


Positive Observations

✅ Good test coverage structure with unit tests, integration tests, and cross-client tests
✅ Both platforms follow consistent API patterns (shouldPush = false, fallback = null)
✅ Content type identifiers match exactly across platforms
✅ FFI integration looks solid for basic encode/decode flows


Summary

The PR adds important edit message functionality, but has 2 critical bugs (both related to compression field handling) that will cause failures when compressed content is edited. Once the Macroscope-identified issues are fixed and compression tests are added, this should be good to merge.


@mchenani mchenani marked this pull request as ready for review March 17, 2026 22:28
@mchenani mchenani requested a review from a team as a code owner March 17, 2026 22:28
@mchenani mchenani changed the base branch from mc/edit-message-core to graphite-base/3345 March 17, 2026 22:30
@mchenani mchenani force-pushed the mc/edit-message-mobile-sdks branch from c42d6ba to d666e51 Compare March 17, 2026 22:30
}
putAllParameters(ffiContent.parameters)
ffiContent.fallback?.let { setFallback(it) }
ffiContent.compression?.let { setCompression(Content.Compression.forNumber(it)) }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High codecs/EditMessageCodec.kt:82

Content.Compression.forNumber(it) returns null for unrecognized compression values, which is passed directly to setCompression(). This throws NullPointerException when the FFI layer returns an unknown compression number. Consider using ?.let to handle the null case.

-                        ffiContent.compression?.let { setCompression(Content.Compression.forNumber(it)) }
+                        ffiContent.compression?.let { Content.Compression.forNumber(it)?.let { setCompression(it) } }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sdks/android/library/src/main/java/org/xmtp/android/library/codecs/EditMessageCodec.kt around line 82:

`Content.Compression.forNumber(it)` returns `null` for unrecognized compression values, which is passed directly to `setCompression()`. This throws `NullPointerException` when the FFI layer returns an unknown compression number. Consider using `?.let` to handle the null case.

Evidence trail:
sdks/android/library/src/main/java/org/xmtp/android/library/codecs/EditMessageCodec.kt line 82 shows `ffiContent.compression?.let { setCompression(Content.Compression.forNumber(it)) }`. The `?.let` only guards against null `ffiContent.compression`, but `Content.Compression.forNumber(it)` can return null for unrecognized enum values (standard protobuf behavior), which would be passed directly to `setCompression()` causing NPE.

return try EncodedContent(serializedBytes: encodeEditMessage(request: ffi))
}

public func decode(content: EncodedContent) throws -> EditMessageRequest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High Codecs/EditMessageCodec.swift:81

The decode method drops the compression field when reconstructing EncodedContent from ffiContent. encode passes compression to FfiEncodedContent, but decode never reads ffiContent.compression back into the result. This causes editedContent.hasCompression to be false after round-tripping, so subsequent calls to decompressContent() will skip decompression and return the raw compressed bytes instead of the decompressed content.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sdks/ios/Sources/XMTPiOS/Codecs/EditMessageCodec.swift around line 81:

The `decode` method drops the `compression` field when reconstructing `EncodedContent` from `ffiContent`. `encode` passes `compression` to `FfiEncodedContent`, but `decode` never reads `ffiContent.compression` back into the result. This causes `editedContent.hasCompression` to be false after round-tripping, so subsequent calls to `decompressContent()` will skip decompression and return the raw compressed bytes instead of the decompressed content.

Evidence trail:
sdks/ios/Sources/XMTPiOS/Codecs/EditMessageCodec.swift:
- Line 69: `compression: encoded.hasCompression ? Int32(encoded.compression.rawValue) : nil,` (encode passes compression)
- Lines 81-101: decode method reconstructs EncodedContent but never reads ffiContent.compression

sdks/ios/Sources/XMTPiOS/Codecs/ContentCodec.swift:
- Lines 21-23: `if hasCompression { encodedContent = try decompressContent() }`
- Lines 52-54: `func decompressContent() throws -> EncodedContent { if !hasCompression { return self }`

The missing compression assignment in decode means hasCompression will be false, causing decompressContent to be skipped.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Mar 17, 2026

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@insipx insipx deleted the branch graphite-base/3345 March 30, 2026 16:54
@insipx insipx closed this Mar 30, 2026
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.

2 participants