Mobile: DM messages silently ship with zero p-tags when membership query races a relay reconnect

Open Beginner friendly
#6,206 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
dart
Domain
mobile

Research direction

Start in mobile/lib/features/channels/send_message_provider.dart at SendMessage._fetchDmRecipientPubkeys() and review the existing fallback tests for empty or failed membership queries. Verify the bounded retry behavior during transient empty results, including the 400ms delay and three-attempt limit, while keeping the change scoped to the DM mention path.

Written by the indexing model from the issue text.

Description

Summary

A plain, top-level DM message can go out with zero p tags — not a thread reply, not a hand-typed-mention issue (#4309), a genuinely fresh top-level message in an active group DM. Confirmed live: checked the raw event on the relay directly, and the channel's own member list (via buzz channels members) was correct and complete at the same moment — the recipients existed, the app just didn't attach them.

Root cause

SendMessage._fetchDmRecipientPubkeys() (mobile/lib/features/channels/send_message_provider.dart) resolves DM recipients from two sources, with a documented fallback:

List<ChannelMember>? members;
try {
  members = await _fetchMembers(channelId);
} catch (_) {}

final participants = members != null && members.isNotEmpty
    ? members.map((member) => member.pubkey)
    : channel.participantPubkeys;

This correctly falls back to channel.participantPubkeys (the channel metadata's p tags) when the live membership query throws or returns empty — and that fallback path has existing test coverage (falls back to metadata DM recipients when membership is empty / ...when membership fails).

The gap: both sources can be empty at the same moment, and nothing distinguishes that from "this DM genuinely has no other participants" — which is impossible by construction (a DM always has ≥1 other participant). channelMembersProvider (channel_management_provider.dart:483) returns a plain empty list, not an error, whenever the relay session isn't in SessionStatus.connected and there's no cached snapshot:

if (sessionState.status != SessionStatus.connected) {
  final cachedMembers = snapshotCache.read(...);
  if (cachedMembers != null) return cachedMembers;
  final channelListMembers = ref.read(channelsProvider.notifier).cachedMembersForChannel(channelId);
  if (channelListMembers.isNotEmpty) { ...; return channelListMembers; }
  return const [];   // <-- silent empty, not an error
}

Mobile reconnects the relay session far more often than desktop (iOS backgrounding suspends the socket — the same underlying pattern already confirmed in #6200's presence gap). If a send happens to land during one of those reconnect windows, and the in-memory Channel object's participantPubkeys also isn't populated at that moment (e.g. it hasn't been refreshed since this specific DM's metadata event was last parsed), both fallback layers come up empty and the message ships with no recipients — silently, with no error surfaced anywhere.

Fix

mobile/lib/features/channels/send_message_provider.dart: when both the live query and the metadata fallback come back empty, retry the membership fetch up to 3 times with a 400ms delay between attempts before giving up, since "zero participants" is never a valid answer for a real DM. Bounded, so a genuinely persistent failure still resolves (with zero recipients, same as before) rather than hanging the send indefinitely.

Scope note

channelMembersProvider's silent-empty-on-disconnect behavior is shared by 7 other consumers (members_sheet.dart, channel_actions_sheet.dart, mention_candidates_provider.dart, compose_bar/helpers.dart, compose_bar_widget.dart, working_bots_provider.dart, channel_detail_page.dart) — changing the provider's own error contract would touch all of them and needs real review, not a blind change. The fix here is scoped narrowly to the DM-mention path only.

Related

  • #6199 / PR #6203 — a different, already-fixed bug in the same family: ThreadDetailPage could pass a null Channel object into this same send path. That fix doesn't cover this case — this one reproduces on ordinary top-level DM messages where channel is never null, the contents of the membership data are just transiently empty.
  • #6200 — mobile's presence display has no backstop against the same frequent-reconnect pattern that causes this.
Dominant language
Rust
Stars
33.7k
Forks
4.4k
Avg merge
1d 21h
Merged PRs (30d)
239

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from block/buzz

All issues in block/buzz

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.