block / block/buzz

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
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

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:

```dart
List? 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:

```dart
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.

Contributor guide

Open the contributing guide

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.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.