fix(unified): preserve tapback metadata across MQTT ingest + cross-source merge#3105
Merged
Conversation
…urce merge Tapback reactions on the Unified Messages feed flickered between rendering correctly (grouped under the parent message as an emoji pill) and rendering as full inline messages, depending on which source's row won a non- deterministic Promise.all race in /api/unified/messages. Three interacting causes, all introduced in #3089: 1. MQTT ingest (mqttIngestion.ts TEXT_MESSAGE_APP branch) was building DbMessage rows without reading `decoded.emoji` / `decoded.reply_id`, so MQTT-sourced reactions hit the DB with both fields null. 2. channelDecryptionService synthesized a stripped `{portnum, payload}` decoded shape, dropping emoji/replyId for server-decrypted packets even when they were present in the underlying Data protobuf. 3. unifiedRoutes merge logic kept first-source-wins for emoji/replyId in the `if (existing)` branch, so when the metadata-less MQTT row was processed before the metadata-bearing TCP row, the merged entry took the null values and the unified view classified the reaction as a regular message. Fix: - Read emoji + replyId (both camelCase and snake_case) from the decoded Data in mqttIngestion's TEXT_MESSAGE_APP case. - Extend DecryptionResult + isValidProtobuf to surface the fields, and propagate them onto the synthesized decoded object after server-side decryption. - In the unifiedRoutes merge, upgrade `existing.emoji` and `existing.replyId` from any later source that has them populated when the first-seen row didn't — same pattern already used for sender display names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job Code Review Complete ✅I've thoroughly analyzed this PR that fixes tapback metadata preservation across MQTT ingest and cross-source merge. Tasks:
Review SummaryThis is an excellent fix that addresses a complex multi-layered bug with proper root cause analysis and systematic testing. The implementation demonstrates thorough understanding of the MeshMonitor architecture and follows established patterns. ✅ Strengths🎯 Problem Analysis & Solution Design
📋 Code Quality
🧪 Test Coverage
🔒 Security & Performance
📝 Minor ObservationsCode Style
Architecture Alignment
Test Quality
🚀 Technical ExcellencechannelDecryptionService.ts
mqttIngestion.ts
unifiedRoutes.ts
📊 Impact AssessmentUser Experience
System Reliability
🎯 RecommendationAPPROVE - This is a high-quality fix that:
The PR is ready for merge. The systematic approach to identifying and fixing each root cause, combined with thorough testing and clear documentation, makes this an exemplary contribution to the codebase. |
4 tasks
Yeraze
added a commit
that referenced
this pull request
May 20, 2026
* chore(release): bump version to 4.6.3 Five user-visible files bumped per the CLAUDE.md version recipe: package.json, package-lock.json (regenerated), helm/meshmonitor/Chart.yaml, desktop/src-tauri/tauri.conf.json, desktop/package.json. CLAUDE.md banner line bumped to match. CHANGELOG entry covers the five PRs since 4.6.2-1: - #3105 unified tapback metadata fix - #3106 docs: drop worktree restriction - #3107 meshcore contact advType persistence - #3108 MQTT channel permissions via channel_database - #3109 hint banner Catppuccin restyle - #3110 node.channel ingest + traceroute/neighbor channel gate Companion blog post (docs/blog/2026-05-20-v4.6.3-permissions.md) walks operators through the new Virtual Channel Permissions flow, the map-visibility behavior change, and the floating-lines fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): regenerate desktop/package-lock.json for 4.6.3 The desktop sub-project carries its own lockfile and the bump to 4.6.3 left it pinned to 4.6.1. The Windows Desktop CI job runs `npm install` without `--legacy-peer-deps` and fails on the package.json / package-lock.json version mismatch. Regenerate to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tapback reactions on the Unified Messages feed flickered between rendering correctly (grouped under the parent as an emoji pill) and rendering as full inline messages, depending on which source's row won a non-deterministic
Promise.allrace in/api/unified/messages.Three interacting causes, all introduced in #3089 (
feat(mqtt): full source-scoped ingest with channel decryption and unified-view fixes):emoji/reply_id.mqttIngestion.tsTEXT_MESSAGE_APP branch builtDbMessagerows without reading the decoded Data'semoji/reply_idfields, so MQTT-sourced reactions hit the DB with both null.channelDecryptionService.isValidProtobufdecoded the full Data protobuf but only returned{portnum, payload}, throwing away emoji/replyId.unifiedRoutes.tsmerge logic only upgraded sender display names — emoji/replyId stayed whatever the first race-winning source had, even when later sources had correct values.The result: TCP-source row has
emoji=1, replyId=parentPacketId; MQTT-source row hasemoji=null, replyId=null. Each poll races; when MQTT wins, the merged record drops the metadata and the reaction renders as a regular inline message.Fix
mqttIngestion.ts— reademojiandreplyId(accepting both camelCase and snake_case) from the decoded Data in TEXT_MESSAGE_APP.channelDecryptionService.ts— extendDecryptionResultwithemoji?/replyId?and surface them fromisValidProtobuf. Propagate onto the synthesizeddecodedobject so MQTT ingest sees them after server-decryption.unifiedRoutes.ts— in theif (existing)merge branch, upgradeemoji/replyIdfrom any source that has them populated when the first-seen row didn't (mirrors the existing sender-name upgrade pattern).Tests
unifiedRoutes.test.tsproving the merge upgrades tapback metadata across sources.replyId/ camelCase,reply_id/ snake_case, and the regular-text case where neither field is set.Test plan
🤖 Generated with Claude Code