Skip to content

feat(messages): search conversations by message content (#3922)#3931

Merged
Yeraze merged 1 commit into
mainfrom
feat/3922-message-content-search
Jul 5, 2026
Merged

feat(messages): search conversations by message content (#3922)#3931
Yeraze merged 1 commit into
mainfrom
feat/3922-message-content-search

Conversation

@Yeraze

@Yeraze Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #3922.

The Messages conversation-list filter previously matched only on the conversation partner's long name / short name / node ID — there was no way to find a past DM by what was said. This PR extends that same filter so a term also matches conversations whose direct-message history contains the term in the message body.

This is intentionally scoped to complement, not duplicate, the existing global full-text search modal (Ctrl+K / SearchModal, added in #d9229627), which already performs exhaustive server-side search across all channels and DMs. The in-tab filter is a fast, client-side substring match over the messages already held in memory — exactly the "simple LIKE/ILIKE substring match to start" the issue proposed, surfaced on the field users naturally reach for.

What changed

  • New pure helper src/utils/messageContentFilter.tsgetMessageContentMatchNodeIds(messages, filter, minLength = 2). Returns the set of node IDs whose DM history contains the term. It mirrors getDMMessages's predicate (text messages, channel === -1, non-broadcast), matches case-insensitively, and skips matching until the trimmed term is ≥ 2 chars (to avoid matching nearly every conversation on a single keystroke).
  • MessagesTab.tsx: precomputes the match set once per (messages, messagesNodeFilter) change via useMemo, then both node-list renderers add a single O(1) membership clause alongside the existing name/id checks.
  • i18n: English messages.filter_placeholder updated from "Filter nodes...""Filter nodes or messages..." for discoverability. Other locales sync via Hosted Weblate.

Testing

  • New unit test src/utils/messageContentFilter.test.ts (10 cases): case-insensitivity, substring matching, both-parties inclusion, min-length gating, and exclusion of broadcasts / channel messages / non-text portnums / empty text.
  • Full Vitest suite: 8573 tests, 0 failures, 0 failed suites (success: true).
  • eslint on changed files: 0 errors.
  • tsc on the changed files: no new type errors.

Notes

  • No backend/database/migration changes; no per-source scoping impact (operates on the already-source-scoped in-memory message list).

🤖 Generated with Claude Code

https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

The Messages conversation-list filter previously matched only on the
conversation partner's long name, short name, or node ID. Issue #3922
asks for the ability to find a past DM by what was actually said.

This extends that same filter so a term also matches conversations whose
direct-message history contains the term in the message body — a simple
case-insensitive substring match over the already in-memory message list,
complementing (not duplicating) the exhaustive server-side Ctrl+K search
modal. The English placeholder is updated to "Filter nodes or messages..."
for discoverability.

The match logic is extracted into a pure, unit-tested helper
(getMessageContentMatchNodeIds) that mirrors getDMMessages's DM predicate
(text messages, channel === -1, non-broadcast) and is precomputed once per
filter/message change so both node-list renderers do an O(1) membership
check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review

  • Read changed files
  • Review code quality and best practices
  • Check for bugs or issues
  • Evaluate performance considerations
  • Check security concerns
  • Assess test coverage

Overall this is a clean, well-scoped addition. The new utility is properly isolated, the useMemo placement is correct, and the test coverage is solid. A few observations:

Minor Issues

Magic number for portnum (messageContentFilter.ts:42)

if (msg.channel !== -1 || msg.portnum !== 1) continue;

The project has a PortNum constants object in src/server/constants/meshtastic.ts (PortNum.TEXT_MESSAGE_APP = 1). While the existing MessagesTab.tsx:522 also uses the bare 1 (so this is consistent with existing code), it's worth noting that TEXT_MESSAGE_COMPRESSED_APP (portnum 7) is silently excluded. If the app ever handles compressed text DMs, both the getDMMessages predicate in MessagesTab.tsx:522 and this filter would need updating. Not a bug today, just a future footgun.

Broadcast check is hardcoded (messageContentFilter.ts:43)

if (msg.to === '!ffffffff') continue;

Same situation as portnum — this mirrors the existing getDMMessages predicate exactly, which is the stated goal. Just flagging that if the broadcast sentinel ever changes it needs updating in both places.

Code Quality

Duplication of DM predicate logic

The filter in messageContentFilter.ts:42-44 duplicates the predicate in MessagesTab.tsx:518-523 (getDMMessages). This is a conscious trade-off per the PR description (the utility is a pure helper with no React dependency), but if the DM definition ever changes, there are now two places to update. A comment in getDMMessages pointing at getMessageContentMatchNodeIds (or vice versa) would help future maintainers keep them in sync.

useMemo dependency array is correct[messages, messagesNodeFilter] at MessagesTab.tsx:534 covers all the inputs. No stale-closure risk.

Both node-list renderers are updated consistently — the main filtered list at line ~762 and the <select> dropdown at line ~1046 both apply the same membership check. Good.

Performance

For the scale MeshMonitor operates at (hundreds to low-thousands of in-memory messages), iterating the full message array on each filter keystroke via useMemo is fine. The useMemo ensures it only runs when messages or messagesNodeFilter changes, not on every render. No concerns here.

Security

No concerns — purely client-side substring match over already-loaded data, no new API surface, no user-controlled data reaches the backend.

Test Coverage

10 test cases cover all the key paths well:

  • Min-length gating (empty, whitespace, single char, custom minLength) ✓
  • Case-insensitivity ✓
  • Substring (not just whole-word) matching ✓
  • Both parties returned ✓
  • Broadcast exclusion ✓
  • Channel exclusion ✓
  • Non-text portnum exclusion ✓
  • Missing/empty text safety ✓
  • Multi-conversation aggregation ✓

One gap worth noting: there's no test for the case where msg.from or msg.to is undefined/null in a matched message. The implementation guards this with if (msg.from) matchIds.add(msg.from) and if (msg.to) matchIds.add(msg.to), which is safe, but a test case for a message with from: undefined would document that the guard is intentional.

i18n

"Filter nodes or messages..." is a nice discoverability improvement. The Weblate sync approach is correct.


Summary: This is a solid, minimally invasive change. The identified items are minor — the lack of a PortNum constant import is consistent with existing code patterns in the component, and the predicate duplication is an acceptable trade-off for keeping the utility pure. Good to merge as-is, or with the optional defensive test case added.

@Yeraze Yeraze merged commit 5fa382f into main Jul 5, 2026
19 checks passed
@Yeraze Yeraze deleted the feat/3922-message-content-search branch July 5, 2026 16:04
Yeraze added a commit that referenced this pull request Jul 5, 2026
… DMs (#3922) (#3935)

PR #3931 (issue #3922) added an in-tab conversation-list filter that matches
message body content, but scoped it to Meshtastic DMs only and excluded
MeshCore. This extends the same behavior to the MeshCore Direct Messages view.

- Add pure helper getMeshCoreMessageContentMatchKeys() alongside the existing
  getMessageContentMatchNodeIds() in src/utils/messageContentFilter.ts. It
  mirrors the MeshCore DM predicate (excludes room_post + channel pseudo-keys,
  requires a toPublicKey) and accepts optional canonicalize / isChannelKey
  hooks so match keys line up with the canonicalized peer keys in the DM list.
- Wire it into MeshCoreDirectMessagesView's filteredPeers useMemo so the
  contact-list search also surfaces conversations by what was said, not just
  the contact name / public key.
- Update the search placeholder ("Search contacts…" -> "Search contacts or
  messages…"), English only (inline default + new en.json key); other locales
  route through Weblate.
- Add Vitest coverage: helper unit tests mirroring messageContentFilter.test.ts
  plus a DM-view integration test asserting a content match surfaces the right
  contact.


Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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: full-text search across message content

1 participant