Skip to content

feat: show outbound interface in message details (#646)#666

Merged
torlando-tech merged 11 commits intomainfrom
feat/outbound-interface-message-details
Mar 12, 2026
Merged

feat: show outbound interface in message details (#646)#666
torlando-tech merged 11 commits intomainfrom
feat/outbound-interface-message-details

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Tracks which network interface sent messages go through and displays it as a "Sent Via" card in the Message Details screen (closes Feature: Show outgoing interface in message details, and show specific incoming interface #646)
  • Creates python/rns_api.py as the Strangler Fig Phase 0 bootstrap — the first thin Python API file alongside the legacy reticulum_wrapper.py, with just 2 pass-through methods
  • Adds sentInterface column to the messages table via Room migration 42→43
  • Plumbs getNextHopInterfaceName() through the full AIDL IPC stack (PythonWrapperManager → RoutingManager → ReticulumServiceBinder → AIDL → ServiceReticulumProtocol)
  • Queries the outbound interface at send time, and enriches on delivery callback if still null (covers opportunistic sends where path isn't known yet)
  • Renames ReceivingInterfaceInfoInterfaceInfo for bidirectional use (sent + received)

Architecture

MessageDetailScreen (UI) ← sentInterface field
    ↑
MessageUi → MessageMapper → Message (domain) → MessageEntity (Room)
    ↑ sentInterface populated at send time + enriched on delivery callback
MessagingViewModel
    ├─ handleSendSuccess() → query interface immediately after send
    └─ handleDeliveryStatusUpdate() → query interface if still null
          ↓ (AIDL IPC)
PythonWrapperManager.getNextHopInterfaceName()
          ↓
rns_api.py (NEW) → RNS.Transport.next_hop_interface(dest_hash)

Test plan

  • Build verification: :app:compileNoSentryDebugKotlin passes
  • InterfaceInfoTest passes (renamed from ReceivingInterfaceInfoTest)
  • MessageDetailViewModelTest passes with sentInterface mapping
  • On-device: send message over TCP → long-press → Message Details → "Sent Via" card shows interface name
  • On-device: send via propagation → "Sent Via" card shows propagation node interface
  • On-device: received messages still show "Received Via" correctly (regression check)
  • Migration test: install from main, upgrade to this branch → no crash (null sentInterface for old messages)

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR adds end-to-end tracking of the outbound network interface for sent messages, surfaced as a "Sent Via" card in the Message Details screen. It plumbs a new getNextHopInterfaceName() call through the full AIDL IPC stack (Python rns_api.pyPythonWrapperManagerRoutingManagerReticulumServiceBinderIReticulumService.aidlServiceReticulumProtocol), adds a Room migration (42→43) for the sentInterface column, and renames ReceivingInterfaceInfoInterfaceInfo for bidirectional use.

  • The concerns raised in previous reviews have been addressed: enrichment is now gated strictly to update.status == "delivered" (excluding failed and retrying_propagated), the propagation guard (deliveryMethod == "propagated") is in place, rns_api.py now logs errors via log_debug instead of silently swallowing them, and the unreachable get_active_propagation_node_hash stub has been removed from rns_api.py.
  • The send-time capture correctly uses receipt.destinationHash (which resolves to the propagation node hash for propagated sends), while the delivery-time enrichment uses message.conversationHash only for non-propagated, non-enriched messages.
  • The updateSentInterface DAO method signature accepts a nullable sentInterface parameter — see inline comment for a minor tightening suggestion.
  • The MockReticulumProtocol stub and all test renames are complete and consistent.

Confidence Score: 4/5

  • Safe to merge — no regressions to existing receive-side interface display, migration is backward-compatible, and all previous critical concerns have been addressed.
  • The logic is sound and previously flagged issues are resolved. The only remaining observation is the nullable DAO parameter, which is a style concern rather than a correctness issue. The best-effort, null-fallback design of the entire feature means any edge-case failure surfaces as a missing "Sent Via" card rather than a crash or data corruption.
  • No files require special attention. MessagingViewModel.kt carries the most complexity but the enrichment guard logic is now well-scoped.

Important Files Changed

Filename Overview
python/rns_api.py New Strangler Fig bootstrap file: thin pass-through to RNS.Transport. Correctly handles Chaquopy jarray→bytes conversion, guards against None from next_hop_interface, and now logs errors via log_debug instead of swallowing them silently. The get_active_propagation_node_hash stub noted in a previous review has been removed.
app/src/main/java/com/lxmf/messenger/viewmodel/MessagingViewModel.kt Adds send-time interface capture via receipt.destinationHash and delivery-time enrichment gated strictly to update.status == "delivered". The propagated-message guard (deliveryMethod == "propagated") is in place. The stale entity concern is mitigated because deliveryMethod is set to "propagated" in a prior retrying_propagated callback, so the freshly-fetched entity at delivery time should reflect the correct value.
data/src/main/java/com/lxmf/messenger/data/di/DatabaseModule.kt Migration 42→43 correctly adds sentInterface TEXT (nullable, no default) and is properly registered in the migration list.
data/src/main/java/com/lxmf/messenger/data/db/dao/MessageDao.kt updateSentInterface correctly scopes by both id and identityHash. The sentInterface parameter is typed String? which technically allows callers to null out the field; current callers always pass non-null values, but the nullable type is a minor footgun for future callers.
app/src/main/java/com/lxmf/messenger/service/manager/PythonWrapperManager.kt RnsApi initialization is non-fatal (logged as warning on failure) and the reference is properly cleared on shutdown. getNextHopInterfaceName correctly checks for Python shutdown before proceeding.
app/src/main/aidl/com/lxmf/messenger/IReticulumService.aidl New getNextHopInterfaceName(in byte[] destHash) AIDL method is correctly declared. Fully implemented in ReticulumServiceBinder → RoutingManager → PythonWrapperManager chain.

Sequence Diagram

sequenceDiagram
    participant UI as MessageDetailScreen
    participant VM as MessagingViewModel
    participant RP as ReticulumProtocol (AIDL)
    participant PW as PythonWrapperManager
    participant PY as rns_api.py
    participant DB as Room DB (messages)

    Note over VM: Send path
    VM->>RP: getNextHopInterfaceName(receipt.destinationHash)
    RP->>PW: getNextHopInterfaceName(destHash)
    PW->>PY: rnsApi.get_next_hop_interface_name(dest_hash)
    PY->>PY: RNS.Transport.has_path(dest_hash)
    PY->>PY: RNS.Transport.next_hop_interface(dest_hash)
    PY->>PY: format_interface_name(iface)
    PY-->>PW: "TCPInterface[Server/1.2.3.4:4242]" or None
    PW-->>RP: String? 
    RP-->>VM: sentInterface?
    VM->>DB: insertMessage(... sentInterface=sentInterface)

    Note over VM: Delivery enrichment path (status == "delivered" && sentInterface == null)
    VM->>VM: enrichSentInterfaceOnDelivery(message, messageHash)
    VM->>RP: getNextHopInterfaceName(conversationHash bytes)
    RP-->>VM: sentInterface?
    VM->>DB: updateSentInterface(messageHash, sentInterface)

    Note over UI: Display
    DB-->>VM: MessageEntity (sentInterface field)
    VM->>UI: MessageUi.sentInterface
    UI->>UI: Show "Sent Via" card (if non-null)
Loading

Comments Outside Diff (1)

  1. data/src/main/java/com/lxmf/messenger/data/db/dao/MessageDao.kt, line 739-743 (link)

    Nullable sentInterface parameter can silently clear the field

    sentInterface: String? means any future caller can pass null and erase a previously captured interface name. There is currently no use-case for nulling this field out — all existing callers always pass a non-null value (guarded by if (sentInterface != null) in the call site). Using a non-null type here makes the intent explicit and prevents accidental data loss.

    The corresponding repository method updateMessageSentInterface(messageId, sentInterface: String?) signature can be tightened to String as well, since the only caller already null-checks before invoking it.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: data/src/main/java/com/lxmf/messenger/data/db/dao/MessageDao.kt
Line: 739-743

Comment:
**Nullable `sentInterface` parameter can silently clear the field**

`sentInterface: String?` means any future caller can pass `null` and erase a previously captured interface name. There is currently no use-case for nulling this field out — all existing callers always pass a non-null value (guarded by `if (sentInterface != null)` in the call site). Using a non-null type here makes the intent explicit and prevents accidental data loss.

```suggestion
    suspend fun updateSentInterface(
        messageId: String,
        identityHash: String,
        sentInterface: String,
    )
```

The corresponding repository method `updateMessageSentInterface(messageId, sentInterface: String?)` signature can be tightened to `String` as well, since the only caller already null-checks before invoking it.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 514d8ba

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Comment on lines 902 to 910
conversationLinkManager.recordPeerActivity(message.conversationHash, update.timestamp)
}

// Enrich sentInterface on delivery if it wasn't captured at send time
enrichSentInterfaceOnDelivery(message, update.messageHash)

// Trigger refresh to ensure UI updates (Room invalidation doesn't always propagate with cachedIn)
_messagesRefreshTrigger.value++

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.

Bug: The enrichSentInterfaceOnDelivery function is called with a stale MessageEntity, causing the check for deliveryMethod == "propagated" to fail and apply incorrect logic.
Severity: MEDIUM

Suggested Fix

After updating the message in the database within handleDeliveryStatusUpdate, refetch the MessageEntity from the conversationRepository before calling enrichSentInterfaceOnDelivery. This will ensure the function operates on the most recent version of the message data.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/src/main/java/com/lxmf/messenger/viewmodel/MessagingViewModel.kt#L902-L910

Potential issue: In `handleDeliveryStatusUpdate`, a `MessageEntity` is fetched before
its `deliveryMethod` is updated to "propagated" in the database. This stale message
object is then passed to `enrichSentInterfaceOnDelivery`. Inside this function, a check
`message.deliveryMethod == "propagated"` fails because it's evaluating the old, stale
value. This causes the function to incorrectly proceed with interface enrichment for
propagated messages, which can result in associating the wrong `sentInterface` with the
message or leaving it as null, as the logic queries with the incorrect
`conversationHash` for this message type.

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 37.93103% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python/rns_api.py 0.00% 18 Missing ⚠️

📢 Thoughts on this report? Let us know!

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

1 similar comment
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

torlando-tech and others added 9 commits March 11, 2026 21:13
Track which network interface sent messages go through and display it
as a "Sent Via" card in the Message Details screen. Creates python/rns_api.py
as the Strangler Fig Phase 0 bootstrap alongside reticulum_wrapper.py.

- Add sentInterface column to messages table (Room migration 42→43)
- Create rns_api.py with thin RNS.Transport.next_hop_interface() passthrough
- Plumb getNextHopInterfaceName through AIDL IPC stack
- Query interface at send time, enrich on delivery callback if still null
- Rename ReceivingInterfaceInfo → InterfaceInfo for bidirectional use
- Display "Sent Via" card for sent messages in MessageDetailScreen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Propagated sends route through the propagation node (a different
destination hash than conversationHash), so querying conversationHash
in the delivery callback would return the wrong interface or null.
The send-time capture already uses receipt.destinationHash which
handles propagated sends correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract sentInterface enrichment into helper method to reduce
  NestedBlockDepth in handleDeliveryStatusUpdate
- Remove unused identityResolutionManager constructor parameter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Log exceptions in rns_api.py instead of silently swallowing
- Guard enrichSentInterfaceOnDelivery against retrying_propagated status
  to prevent stale message state from bypassing the propagation check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t to delivered

- Remove unreachable get_active_propagation_node_hash from rns_api.py
  and its unused router/reticulum init plumbing
- Only enrich sentInterface on "delivered" status — other statuses
  (failed, retrying_propagated) have too much routing ambiguity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Kotlin ByteArray arrives as a Chaquopy jarray('b'), not Python bytes.
RNS Transport dict key lookups (has_path, next_hop_interface) require
bytes keys — jarray silently mismatches and returns False/None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech torlando-tech force-pushed the feat/outbound-interface-message-details branch from 514d8ba to 7900279 Compare March 12, 2026 01:16
suspend fun updateMessageSentInterface(
messageId: String,
sentInterface: String?,
) {
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.

Bug: The message update on delivery confirmation can silently fail if the user's active identity changes, due to an identityHash mismatch in the database query.
Severity: MEDIUM

Suggested Fix

Modify the message lookup during delivery confirmation to not rely on the current active identity. Either remove the identityHash filter from the database query to find the message across all identities, or ensure the original sender's identityHash is available and used during the update.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
data/src/main/java/com/lxmf/messenger/data/repository/ConversationRepository.kt#L594

Potential issue: In `enrichSentInterfaceOnDelivery`, the logic to update a message's
`sentInterface` uses the currently active identity's hash (`identityHash`) for the
database query. If a user switches their active identity between the time a message is
sent and when its delivery confirmation is received, this will cause a mismatch. The
`UPDATE` query's `WHERE` clause will fail to find the message, as it was stored with the
original identity's hash. This causes the update to fail silently, and the 'Sent Via'
information for that message will not be displayed in the UI.

torlando-tech and others added 2 commits March 11, 2026 21:23
Replace inline class-name-only extraction with format_interface_name()
for incoming messages. Previously stored just "TCPInterface" which the
UI mapped to generic "TCP/IP"; now stores "TCPInterface[Beleth RNS Hub]"
so the UI shows the specific server name, matching outbound behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…erface_name

format_interface_name returns type(obj).__name__ verbatim, so
AutoInterfacePeer stays AutoInterfacePeer — not stripped to AutoInterface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech torlando-tech merged commit 8bb7c3e into main Mar 12, 2026
13 of 14 checks passed
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.

Feature: Show outgoing interface in message details, and show specific incoming interface

1 participant