Skip to content

[Bug] Mobile: replies sent in a nested thread never appear — thread query is root-scoped #2415

Description

@mdmohsin7

Summary

On mobile, replies sent inside a nested thread (a thread opened on a reply, rather than on a top-level channel message) never appear. The message is signed, published, accepted by the relay, and visible to other clients — but the sender's own screen stays empty, with no error and no way to recover. Leaving the channel, pull-to-refresh, and restarting the app all fail to surface it.

It reads as data loss from the user's side: you type a message, it disappears, and only by opening desktop do you discover it was delivered the whole time.

Reproduction

  1. On mobile, open a channel and post a message — call it R.
  2. Open R's thread and post a reply — call it M.
  3. From inside R's thread, open M's own thread (the nested-thread row).
  4. Send any message there.

Expected: the message appears in M's thread.
Actual: nothing appears. The message is on the relay and renders on desktop.

Not specific to @-mentions or agents — plain text reproduces it. Verified on 7e34bee6.

Root cause

The relay's thread query is root-scoped (buzz-db/src/thread.rs#L397-L398):

WHERE tm.community_id = $1
  AND tm.root_event_id = $2

It returns rows whose root is the supplied id, relying on depth (not parentage) to bound the result.

Mobile passes the nested thread's head as if it were a root (thread_detail_page.dart#L56-L58):

final repliesState = ref.watch(
  threadRepliesProvider(
    ThreadRepliesArgs(channelId: channelId, rootId: threadHead.id),
  ),
);

which becomes '#e': [args.rootId] in thread_replies_provider.dart#L56.

But a reply to M is stored with root_event_id = R, parent_event_id = M — the relay derives the root from the root marker and only falls back to the parent when absent (ingest.rs#L632):

let effective_root = meta.root_event_id.unwrap_or_else(|| parent_bytes.clone());

And mobile's own send path writes exactly that marker pair for a nested reply (send_message_provider.dart#L122-L136):

return [
  ['e', root, '', 'root'],
  ['e', parentEventId, '', 'reply'],
];

So root_event_id = M matches zero rows, and M's thread view is permanently empty. The write side is correct; only the read query is wrong.

Why desktop is unaffected

Desktop has no nested thread view. openThreadHeadId is only ever set from a top-level timeline message (ChannelScreen.tsx#L163-L166), so the id it queries with is always a genuine root and the root-scoped query returns every descendant at any depth. Mobile added nested-thread navigation (thread_detail_page.dart#L300) without adapting the query.

Possible fixes

Option A — query the true root, filter client-side (mobile only).
ThreadDetailPage already computes the right value one screen further down, for the compose bar (#L136):

final effectiveRootId = threadHead.rootId ?? threadHead.id;

Passing that to threadRepliesProvider is sufficient — the page already indexes replies by parent and selects childrenByParent[threadHead.id] (#L71-L78), so the narrowing is already implemented. Live refresh also already invalidates on both root and parent (channel_messages_provider.dart#L187-L208).

Small and matches desktop's effective behaviour. Cost: a nested view fetches the entire root thread and discards most of it, which grows with thread size.

Option B — support parent-scoped thread queries in the relay.
Add an opt-in filter so the query can bound on parent_event_id (or on a subtree) instead of root_event_id, and have mobile use it. No over-fetching and it makes nested threads a first-class concept, but it is a relay change with migration and API-surface implications.

Happy to send a PR for Option A, or for Option B if that is the preferred direction — I'd rather confirm before writing relay code.

Environment

  • main @ 7e34bee6
  • Buzz Mobile on a physical iPhone XR (not a simulator)
  • Reproduced against a relay where the same events render correctly on desktop

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions