Skip to content

Conversation

neekolas
Copy link
Contributor

tl;dr

  • Upgrades to the latest dev release of xmtp-ios and xmtp-android, which adds new filters for listing conversations and messages.
  • Adds a shouldPush field to all ContentCodecs

@neekolas neekolas requested a review from a team as a code owner October 17, 2025 21:11
Copy link

changeset-bot bot commented Oct 17, 2025

⚠️ No Changeset found

Latest commit: 212b7d7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

macroscopeapp bot commented Oct 17, 2025

Upgrade iOS and Android XMTP SDK dependencies and add timestamp filters, JSON query params, and push visibility controls across Conversations, Dm, Group, and native XMTPModule entry points

This pull request upgrades the native XMTP SDKs on iOS and Android and expands query and visibility controls across conversation listing and message retrieval/sending APIs. It introduces JSON-based query parameter parsing on native entry points, adds timestamp-based filters and ordering for conversation lists, supports excluding content types and sender inbox IDs in message queries, and propagates a shouldPush flag from codecs through to native send/prepare operations.

📍Where to Start

Start with the JSON query parameter parsing and propagation in the native entry points: review ConversationListParamsWrapper.conversationListParamsFromJson in ConversationListParamsWrapper.swift and MessageQueryParamsWrapper.messageQueryParamsFromJson in MessageQueryParamsWrapper.swift, then follow their use in AsyncFunction("listConversations"|"listGroups"|"listDms"|"conversationMessages"|"conversationMessagesWithReactions") in XMTPModule.swift.


📊 Macroscope summarized 212b7d7. 25 files reviewed, 42 issues evaluated, 33 issues filtered, 3 comments posted

🗂️ Filtered Issues

android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationListParamsWrapper.kt — 0 comments posted, 4 evaluated, 4 filtered
  • line 30: JsonParser.parseString(paramsJson).asJsonObject performs an unchecked parse and object cast. If paramsJson is not valid JSON, is whitespace-only, or is valid JSON but not an object (e.g., a JSON array or primitive), parseString/asJsonObject will throw (JsonSyntaxException/IllegalStateException). There is no guard or fallback, so any non-empty malformed input crashes the method. [ Low confidence ]
  • line 33: Using jsonOptions.has(...) to gate scalar reads does not protect against JSON null values. If the key is present with JsonNull, has(...) returns true and subsequent calls like jsonOptions.get("limit").asInt or .asLong/.asString will throw (UnsupportedOperationException/IllegalStateException). This affects fields createdAfterNs, createdBeforeNs, lastActivityAfterNs, lastActivityBeforeNs, limit, and orderBy. [ Low confidence ]
  • line 69: jsonOptions.getAsJsonArray("consentStates") and subsequent element mapping assume the value is an array of strings. If consentStates is present but not an array, getAsJsonArray will throw. If the array contains non-string elements or null elements, state.asString will throw. No validation or safe fallback is present. [ Low confidence ]
  • line 71: lowercase() is used for string normalization in consentStates and orderBy. The default lowercase() can be locale-sensitive, which may cause unexpected behavior in locales with special casing (e.g., Turkish). This can lead to incorrect mapping (e.g., failing to match ALLOWED/DENIED or CREATED_AT/LAST_ACTIVITY), changing the behavior without a visible error. [ Code style ]
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt — 0 comments posted, 1 evaluated, 1 filtered
  • line 16: Unsafe casts based solely on conversation.type can throw at runtime. In both branches, the code uses (conversation as Conversation.Group) and (conversation as Conversation.Dm) after checking when (conversation.type). If conversation.type ever becomes inconsistent with the actual runtime subtype of conversation (e.g., due to deserialization or a bug elsewhere), these hard casts will trigger a ClassCastException, crashing the call. Safer approaches include using is Conversation.Group/is Conversation.Dm with smart-casts, or using as? and handling a null result with a defined error/fallback path. [ Out of scope ]
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageQueryParamsWrapper.kt — 0 comments posted, 3 evaluated, 3 filtered
  • line 26: JsonParser.parseString(paramsJson).asJsonObject is unchecked: if paramsJson is invalid JSON, whitespace-only, or not a JSON object, this will throw at runtime. There is no guard or error handling, so any malformed input crashes the method. [ Low confidence ]
  • line 29: Using jsonOptions.has(...) does not guard against JSON null values. If a key exists with JsonNull, calls like jsonOptions.get("limit").asInt, .asLong, or .asString will throw. This affects limit, beforeNs, afterNs, and direction. [ Low confidence ]
  • line 58: jsonOptions.getAsJsonArray("excludeContentTypes") and "excludeSenderInboxIds" assume array types with string elements. If either key is present but not an array, getAsJsonArray will throw. If elements are non-strings or null, it.asString will throw. There is no validation or safe fallback. [ Low confidence ]
example/App.tsx — 0 comments posted, 4 evaluated, 1 filtered
  • line 196: The FlatList keyExtractor={(item) => item} assumes all log file paths are unique. If Client.getXMTPLogFilePaths() returns duplicates (e.g., due to rotation naming collisions or symlinked paths), React Native may log warnings and list item updates can behave incorrectly. Consider using a stable unique key (e.g., a combination of path and an index disambiguation) or enforce uniqueness before rendering. [ Code style ]
example/src/GroupScreen.tsx — 2 comments posted, 4 evaluated, 1 filtered
  • line 1144: Potential runtime failure in attachment size calculation: using new Buffer(attachment.data, 'base64').length relies on the deprecated Buffer constructor. In React Native with the buffer polyfill, new Buffer(...) may be unsupported (it is deprecated and can be disabled), leading to a runtime error. Use Buffer.from(attachment.data, 'base64') instead and guard for invalid/empty base64 data to avoid throwing. [ Out of scope ]
ios/Wrappers/AuthParamsWrapper.swift — 1 comment posted, 4 evaluated, 3 filtered
  • line 53: Global environment mutation in authParamsFromJson via setenv is not paired with cleanup or reset and can leave stale values when historySyncUrl/customLocalUrl are absent or empty in subsequent calls. This creates persistent process-wide state that may contradict the returned AuthParamsWrapper (e.g., historySyncUrl is nil but XMTP_HISTORY_SERVER_ADDRESS remains set from a prior invocation). Additionally, the mutation is unguarded against concurrent calls, introducing race conditions. To fix: explicitly clear the variables (e.g., call unsetenv or set to a known default) when inputs are missing/empty, and serialize access if this can be called from multiple threads. [ Low confidence ]
  • line 57: Potentially malformed environment variable values in authParamsFromJson: setenv will accept empty strings or strings containing NUL bytes, resulting in variables that are empty or truncated at the first NUL. There is no validation of historySyncUrl/customLocalUrl format or emptiness before mutation. To fix: validate that inputs are non-empty and do not contain NULs; reject or sanitize invalid values; consider unsetenv when inputs are empty. [ Low confidence ]
  • line 106: Case-sensitive handling of signerType leads to unexpected defaulting: walletParamsFromJson only recognizes "SCW" exactly; inputs like "scw", "Scw", or other typical cases are silently treated as SignerType.EOA. This is inconsistent with other parsers that normalize strings via .lowercased(). To fix: normalize the input and compare in a case-insensitive way, or provide explicit error handling for unknown values. [ Low confidence ]
ios/Wrappers/ConversationListParamsWrapper.swift — 0 comments posted, 2 evaluated, 1 filtered
  • line 61: Silent coercion of invalid orderBy values to .lastActivity in conversationListParamsFromJson accepts malformed input without reporting an error, potentially violating sequencing/contract constraints. If orderBy is provided but unknown, a default is applied rather than rejecting or clearly indicating the issue. To fix: either reject unknown orderBy with an error or ensure the fallback is explicitly documented and consistent with system-wide behavior. [ Low confidence ]
src/index.ts — 0 comments posted, 8 evaluated, 8 filtered
  • line 635: Contract mismatch risk: listGroups changed its call to XMTPModule.listGroups to pass a single serialized queryParamsJson string containing limit, consentStates, createdAfterNs, createdBeforeNs, lastActivityAfterNs, lastActivityBeforeNs, and orderBy, instead of the previous separate parameters. If the native XMTPModule.listGroups implementation still expects the prior signature (e.g., separate limit and consentStates args), this will cause a runtime failure or unintended behavior. This is an externally visible contract change that requires the callee to be updated in lockstep. [ Low confidence ]
  • line 675: Contract mismatch risk: listDms changed its call to XMTPModule.listDms to pass a single serialized queryParamsJson string instead of separate limit/consentStates parameters. If XMTPModule.listDms expects the previous signature, calls will fail or yield wrong results at runtime. This change requires native module parity. [ Low confidence ]
  • line 715: Contract mismatch risk: listConversations altered its call to XMTPModule.listConversations to pass queryParamsJson rather than separate primitives. If the native side expects the old argument order and types, this will break at runtime. Such a change must be synchronized with the native implementation. [ Low confidence ]
  • line 776: conversationMessages and conversationMessagesWithReactions assume XMTPModule.* return an array of JSON strings and immediately call .map. If the native module returns null, undefined, or any non-array value (e.g., an object on error), calling .map will throw a TypeError and crash the call rather than providing a graceful failure. There are no guards to ensure messages is an array before mapping. [ Out of scope ]
  • line 806: conversationMessagesWithReactions also assumes the native return is an array and calls .map without validation. If messages is null, undefined, or non-array, a TypeError occurs. No guard ensures array shape before mapping. [ Out of scope ]
  • line 924: In the native codec branch of sendWithContentType, the code serializes the raw content (JSON.stringify(content)) and sends it via XMTPModule.sendMessage without using codec.encode(content). For NativeContentCodec, encode returns a NativeMessageContent shaped payload (e.g., { text: '...' } for TextCodec). Sending the raw value instead of the encoded structure can yield malformed payloads and break the native side's expectations for content shape. [ Out of scope ]
  • line 939: Push-behavior asymmetry: codec.shouldPush(content) is only used when sending/prepare via the encoded (JS codec) path (sendEncodedContent / prepareEncodedMessage). In the native codec branch ('contentKey' in codec), push behavior is ignored (sendMessage / prepareMessage do not receive a push flag). This introduces inconsistent behavior where native codecs cannot influence push notifications, violating interface parity since both JSContentCodec and NativeContentCodec define shouldPush(content). This can produce unexpected delivery behavior and inconsistent side effects. [ Low confidence ]
  • line 986: In the native codec branch of prepareMessageWithContentType, the code bypasses codec.encode(content) and calls prepareMessage with content directly, which serializes the raw content. For NativeContentCodec, this should produce NativeMessageContent via encode. Skipping encode can result in malformed prepared payloads and loss of content-type-specific structure. [ Out of scope ]
src/lib/Conversations.ts — 0 comments posted, 6 evaluated, 6 filtered
  • line 482: Unhandled errors thrown by user-provided callbacks inside event listeners. In stream(), the Conversation handler awaits callback(...) without a try/catch; likewise in streamAllMessages(), it awaits callback(...) without a try/catch. If the callback throws, the async listener will reject and may result in an unhandled promise rejection, leaving stream state inconsistent and without cleanup or notification. [ Out of scope ]
  • line 483: Data loss when constructing Group and Dm on stream: the conversation event payload likely contains a lastMessage field (as per GroupParams and DmParams), but the code calls new Group(this.client, conversation as unknown as GroupParams) and new Dm(this.client, conversation as unknown as DmParams) without passing the third lastMessage argument. The constructors set this.lastMessage only from the third parameter and ignore params.lastMessage, so lastMessage from the event is silently dropped. [ Out of scope ]
  • line 486: Silent drop of events when conversation.version is neither ConversationVersion.GROUP nor ConversationVersion.DM. The event handler has no else branch and simply returns without logging or error when the version is unexpected. Given the code accepts external events, a defined fallback or explicit error reporting would avoid state ambiguity. [ Out of scope ]
  • line 493: Calling stream() multiple times accumulates listeners without first removing previous ones. The code overwrites this.subscriptions[EventTypes.Conversation] with the new subscription reference but does not remove any existing listener before overwriting. This causes memory/resource leaks and duplicate message deliveries. Additionally, cancelStream() will only remove the most recently stored listener, leaving earlier ones attached to the emitter. The same leak exists for the EventTypes.ConversationClosed subscription stored in this.subscriptions[EventTypes.ConversationClosed]. [ Out of scope ]
  • line 549: Calling streamAllMessages() multiple times accumulates listeners without removing previous ones. The code overwrites this.subscriptions[EventTypes.Message] and this.subscriptions[EventTypes.MessageClosed] with new subscriptions without removing any existing listener. This causes memory/resource leaks and may invoke the callback multiple times per event. cancelStreamAllMessages() will only remove the most recently stored listener, leaving earlier ones attached. [ Out of scope ]
  • line 577: cancelStream() and cancelStreamAllMessages() call XMTPModule.unsubscribeFromConversations(this.client.installationId) and XMTPModule.unsubscribeFromAllMessages(this.client.installationId) without awaiting them or handling errors. If these functions return promises and reject, the error is dropped, leaving the system in an inconsistent subscription state without visible outcome. [ Out of scope ]
src/lib/Dm.ts — 0 comments posted, 2 evaluated, 2 filtered
  • line 213: Potential runtime signature mismatch: Dm.messages now passes two additional arguments opts?.excludeContentTypes and opts?.excludeSenderInboxIds to XMTP.conversationMessages. If the native method signature hasn't been updated to accept these extra parameters, calls may fail or the extra args may be ignored/misinterpreted, causing incorrect filtering behavior. [ Low confidence ]
  • line 229: Potential runtime signature mismatch: Dm.messagesWithReactions now forwards opts?.excludeContentTypes and opts?.excludeSenderInboxIds to XMTP.conversationMessagesWithReactions. If the native method hasn't been updated, this change can break at runtime or yield incorrect results. [ Low confidence ]
src/lib/Group.ts — 0 comments posted, 2 evaluated, 2 filtered
  • line 233: Potential runtime signature mismatch: Group.messages now passes opts?.excludeContentTypes and opts?.excludeSenderInboxIds to XMTP.conversationMessages. If the native implementation hasn't been updated to accept these extra parameters, this may cause runtime errors or filtering not being applied. [ Low confidence ]
  • line 260: Potential runtime signature mismatch: Group.messagesWithReactions now forwards two additional filter arguments to XMTP.conversationMessagesWithReactions. If the native layer is not updated to accept these new parameters, it may crash or ignore the filters, causing incorrect UI behavior. [ Low confidence ]
src/lib/NativeCodecs/ReplyCodec.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 35: In ReplyCodec.fallback, the branch if (typeof content.content === 'string') is inconsistent with the declared type of ReplyContent.content (NativeMessageContent), which is an object, not a string. Under the declared contract, this condition will never be true, so the specific message path (Replied with "..." to an earlier message) is effectively unreachable. As a result, the fallback will always return the generic string Replied to an earlier message, losing the intended context. If at runtime content.content ever is a string (e.g., due to unsafe/deserialized inputs), the declared type would be violated, but the current check would then work by accident, creating an inconsistent behavior between typed and untyped flows. [ Low confidence ]

Copy link

Choose a reason for hiding this comment

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

Issue on line in example/src/GroupScreen.tsx:1157:

codec?.decode(replyContent.content) may return undefined or throw, but the code still renders MessageContents with content={actualReplyContent}. This can recurse into the same reply branch with content=undefined, leading to a runtime TypeError when accessing replyContent.content.

Consider guarding the decode with try/catch and checking for a defined result before rendering the nested MessageContents. If decoding fails or returns undefined, consider rendering a small fallback message instead of recursing.

-    if (contentTypeId === 'xmtp.org/reply:1.0') {
-      const replyContent: ReplyContent = content
-      const codec = Client.codecRegistry[contentTypeId]
-      const actualReplyContent = codec?.decode(replyContent.content)
-
-      return (
-        <View>
-          <Text style={{ color: 'gray' }}>Reply</Text>
-          <MessageContents
-            contentTypeId={contentTypeId}
-            content={actualReplyContent}
-          />
-        </View>
-      )
-    }
+    if (contentTypeId === 'xmtp.org/reply:1.0') {
+      const replyContent: ReplyContent | undefined = content
+      let actualReplyContent: any | undefined
+      try {
+        const codec = Client.codecRegistry[contentTypeId]
+        actualReplyContent = codec?.decode(replyContent?.content)
+      } catch (e) {
+        actualReplyContent = undefined
+      }
+      return (
+        <View>
+          <Text style={{ color: 'gray' }}>Reply</Text>
+          {actualReplyContent ? (
+            <MessageContents
+              contentTypeId={contentTypeId}
+              content={actualReplyContent}
+            />
+          ) : (
+            <Text style={{ opacity: 0.5, fontStyle: 'italic' }}>
+              failed to decode reply content
+            </Text>
+          )}
+        </View>
+      )
+    }

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

Copy link

Choose a reason for hiding this comment

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

Issue on line in ios/Wrappers/AuthParamsWrapper.swift:102:

In AuthParamsWrapper (https://github.com/xmtp/xmtp-react-native/pull/737/files#diff-7cb2cf2873830a5b373878690efec6f0631a58eee8d291d48feeeea96bca4ecaR102), ConversationListParamsWrapper (https://github.com/xmtp/xmtp-react-native/pull/737/files#diff-7acd4099c21174c0cae9a9e49e07057d2852c8c610a9097887cf32f4e0ea26bcR33), and MessageQueryParamsWrapper (https://github.com/xmtp/xmtp-react-native/pull/737/files#diff-d9424643f5bb51c458eaf180ea342967aa362c93597de3e4ee625bc7bd12837bR32), JSON numeric fields are cast with as? Int64, but JSONSerialization produces NSNumber, causing the casts to fail and drop valid values. Instead, cast to NSNumber and use .int64Value, or switch to JSONDecoder with typed models for robust numeric parsing.

-        let chainId = jsonOptions["chainId"] as? Int64
-        let blockNumber = jsonOptions["blockNumber"] as? Int64
+        let chainId = (jsonOptions["chainId"] as? NSNumber)?.int64Value
+        let blockNumber = (jsonOptions["blockNumber"] as? NSNumber)?.int64Value

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

Copy link

Choose a reason for hiding this comment

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

Issue on line in example/src/GroupScreen.tsx:1159:

In the reply branch of MessageContents (https://github.com/xmtp/xmtp-react-native/pull/737/files#diff-f54a9a16dba92196ab3dcf0d8acf5d8d8501f3d49944522a325055b66b5aae85R1159), replies are decoded and rendered with the wrong contentTypeId. You currently look up the 'xmtp.org/reply:1.0' codec to decode the inner payload and pass the same contentTypeId to the nested MessageContents, which can lead to undefined results or infinite recursion. Use the nested replyContent.contentTypeId when selecting the codec and when rendering the inner MessageContents, and add a guard for missing codecs so unsupported content displays gracefully instead of crashing.

-    const codec = Client.codecRegistry[contentTypeId]
+    const codec = Client.codecRegistry[replyContent.contentTypeId]
-          contentTypeId={contentTypeId}
+          contentTypeId={replyContent.contentTypeId}

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

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