Skip to content

Notes: toolbar avatar indicator only shows one thread's participants when a block has multiple notes #82273

Description

@adamsilverstein

I ran into this while testing the new avatar indicator from PR #75147 - originally noted in this comment when closing PR #78205.

Claude dug through the indicator code and wrote up the details:

Description

When a block carries multiple note threads from multiple users, the avatar indicator in the block toolbar only reflects a single thread. In particular, when a block-level note exists alongside inline (selection) notes, only the block-level note's author shows in the toolbar - other users who left inline notes on the same block are not represented at all.

Step-by-step reproduction instructions

Note: this requires a collaborative setup with two users (eg. two browsers logged in as different users on the same post, with Notes enabled).

  1. As user A, select a block and add a block-level note. User A's avatar shows on the toolbar indicator.
  2. As user A, select some text in the same block and add a selection (inline) note.
  3. As user B, select different text in the same block and add another selection note.
  4. Select the block and check the avatar indicator in the block toolbar.

Expected: both users' avatar icons show.

Actual: only the block-level note's avatar shows.

Block toolbar showing a single avatar while the sidebar lists notes from two users

Next, deleting the block-level note:

Deleting the block-level note

Now both icons show:

Block toolbar showing two avatars after the block-level note is deleted

The block-level note appears to take precedence when multiple users have left inline notes.

What the code does

Reading trunk, the behavior traces to the indicator being wired to exactly one "primary" thread per block rather than aggregating across the block's threads:

  • NotesSidebar collects all threads for the selected block, then narrows to a single thread via pickPrimaryNote() and renders one NoteAvatarIndicator with just that thread:
    // Surface one thread for the avatar indicator.
    const currentThreads =
    blockNoteIds.length > 0
    ? notes.filter( ( thread ) => blockNoteIds.includes( thread.id ) )
    : [];
    const currentThread = pickPrimaryNote( currentThreads );
    if ( isDistractionFree ) {
    return <AddNoteMenuItem isDistractionFree />;
    }
    return (
    <>
    <NoteHighlightStyles
    threads={ unresolvedNotes }
    selectedId={ selectedNoteId }
    />
    { !! currentThread && (
    <NoteAvatarIndicator
    note={ currentThread }
    onClick={ () => openNoteForBlock( clientId ) }
    />
    ) }
  • pickPrimaryNote() returns the first unresolved (hold) thread, falling back to the first thread:
    export function pickPrimaryNote( threads ) {
    return (
    threads.find( ( thread ) => thread.status === 'hold' ) ??
    threads[ 0 ] ??
    null
    );
    }
  • Thread ordering within a block puts block-level notes first (BLOCK_LEVEL_NOTE_START = -1, so they sort ahead of inline notes ordered by marker offset):
    // Order within a block: block-level notes (no inline anchor) come
    // first as the "overall comment", then inline notes ascending by
    // marker start offset. Ties (rare; two markers at the same offset)
    // fall back to creation order via thread id. Blocks themselves are
    // already iterated in document order above.
    const unresolved = [];
    const resolved = [];
    for ( const [ clientId, noteIds ] of Object.entries(
    blocksWithNotes
    ) ) {
    const attributes = getBlockAttributes( clientId );
    const orderedThreads = noteIds
    .map( ( noteId ) => {
    const thread = threadsById.get( noteId );
    if ( ! thread ) {
    return null;
    }
    return {
    thread,
    start: getInlineMarkerStart( thread, attributes ),
    };
    } )
    .filter( Boolean )
    .sort( ( a, b ) => {
    if ( a.start !== b.start ) {
    return a.start - b.start;
    }
    return a.thread.id - b.thread.id;
    } );
    and
    export const BLOCK_LEVEL_NOTE_START = -1;
  • NoteAvatarIndicator then builds its participant list only from that single thread - the root note plus its replies ([ note, ...note.reply ]):
    export function NoteAvatarIndicator( { onClick, note } ) {
    const threadParticipants = useMemo( () => {
    if ( ! note ) {
    return [];
    }
    // Track thread participants (original author + repliers), sorted by
    // date so they appear in chronological order.
    const participantsMap = new Map();
    const allNotes = [ note, ...note.reply ].sort(
    ( a, b ) => new Date( a.date ) - new Date( b.date )
    );
    for ( const entry of allNotes ) {
    if ( ! entry.author_name || participantsMap.has( entry.author ) ) {
    continue;
    }
    participantsMap.set( entry.author, {
    id: entry.author,
    name: entry.author_name,
    avatar:
    entry.author_avatar_urls?.[ '48' ] ||
    entry.author_avatar_urls?.[ '96' ],
    } );
    }
    return Array.from( participantsMap.values() );
    }, [ note ] );
    if ( ! threadParticipants.length ) {
    return null;
    }

So the "block-level note wins" symptom is really the ordering plus the single-thread pick: an unresolved block-level thread always sorts first, gets picked, and its participants are the only avatars rendered. Other users' separate threads on the block never contribute an avatar.

Worth noting: in the repro above, the two avatars that appear after deleting the block-level note are (per the code) the participants of a single thread - the newly picked inline thread happened to have a reply from the second user. Two inline threads by two users with no cross-replies would likely still show only one avatar, even with no block-level note involved. Untested, but that is what the code reads as.

Possible direction

The indicator could aggregate participants across all of the block's unresolved threads instead of a single picked thread - eg. compute the participant map in NotesSidebar over currentThreads rather than passing one thread to NoteAvatarIndicator. The existing overflow handling ("+n" past three avatars) already accommodates larger participant lists. There may be a deliberate reason for surfacing only the primary thread, though - input from the folks who worked on PR #75147 would help.

Related

AI Use

The investigation and this write-up came out of a session with 🤖 Claude Code; no code changes here, just the report.

Activity

  1. added
    [Type] BugAn existing feature does not function as intended
    [Feature] NotesPhase 3 of the Gutenberg roadmap around block commenting
    on Aug 31, 2026
  2. adamsilverstein commented on Aug 31, 2026

    @adamsilverstein
    MemberAuthor

    The indicator could aggregate participants across all of the block's unresolved threads instead of a single picked thread - eg. compute the participant map in NotesSidebar over currentThreads rather than passing one thread to NoteAvatarIndicator. The existing overflow handling ("+n" past three avatars) already accommodates larger participant lists. There may be a deliberate reason for surfacing only the primary thread, though - input from the folks who worked on #75147 would help.

    That makes sense to me, the button is designed to handle multiple avatars and when we enabled multiple inline notes per block in 7.1, the intention was to show the avatars of each active participant here - it helps users get some quick context about the notes. I feel like it should be showing the avatars of any users participating in unresolved threads (with the 3+ caveat). This would include block level, inline as well as replies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

[Feature] NotesPhase 3 of the Gutenberg roadmap around block commenting[Package] Editor/packages/editor[Status] In ProgressTracking issues with work in progress[Type] BugAn existing feature does not function as intended

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions