Skip to content

feat: enhanced mentions#3631

Open
isekovanic wants to merge 23 commits into
developfrom
feat/enhanced-mentions
Open

feat: enhanced mentions#3631
isekovanic wants to merge 23 commits into
developfrom
feat/enhanced-mentions

Conversation

@isekovanic
Copy link
Copy Markdown
Contributor

@isekovanic isekovanic commented Jun 5, 2026

🎯 Goal

This PR implements the enhanced mentions featureinto the SDK so RN apps can mention not just users but also @channel, @here, custom roles and user groups - with per type colors in the rendered message and full offline draft round tripping. Bundles the architectural fixes that surfaced while wiring this up (Android a11y bounds, cross screen portal teleport leak, composer/list animation sync, iOS multiline regression).

🛠 Implementation details

Enhanced mentions

Consumes the LLC's five variant MentionSuggestion/MentionEntity union (user | channel | here | role | user_group).

Composer suggestion rows - package/src/components/AutoCompleteInput/

  • AutoCompleteSuggestionItem.tsx's MentionSuggestionItem is now a dispatcher that switches on item.mentionType and routes to a per type row. Still overridable via useComponentsContext().MentionSuggestionItem so integrators can replace just the mention branch.
  • New mentionItems/ directory with one component per variant (MentionUserItem, MentionBroadcastItem, MentionRoleItem, MentionUserGroupItem), a shared MentionItem primitive, plus reusable
    EnhancedMentionContent, EnhancedMentionIcon, TokenizedSuggestionParts — all exported for custom dispatcher composition.
  • New icons: megaphone.tsx (broadcast), shield.tsx (role). User-group rows reuse the existing PeopleIcon.

Rendered message text - Message/MessageItemView/utils/renderText.tsx

  • Builds a MentionEntity[] from mentioned_users + mentioned_channel + mentioned_here + mentioned_roles + mentioned_groups (mentioned_group_ids fallback).
  • Regex alternation built longest-first to avoid prefix collisions (@here mustn't shadow @here-team).
  • Per type color via semantic tokens (chatTextMentionUser / …Broadcast / …Role / …Group), each defaulting to the umbrella chatTextMention so existing themes look identical.
  • onPress now carries additionalInfo: { mentionedEntity, user? }. user stays populated for user mentions (for back compatibility reasons).
  • Markdown cache key extended to all five mention sources so the text rerenders when only non-user mentions change.

Memo comparator — MessageItemView/MessageTextContainer.tsx

  • React.memo comparator extended to diff mentioned_channel, mentioned_here, mentioned_roles, and mentioned_groups/mentioned_group_ids in addition to mentioned_users. Without this, messages differing only
    in non-user mentions would skip re-render.

Offline draft persistence has also been modified to reflect enhanced mentions.

Suggestion list architecture

The mount location of <AutoCompleteSuggestionList /> is now moved to MessageList.tsx and MessageFlashList.tsx - not MessageComposer.tsx, inside its own <PortalWhileClosingView portalHostName='overlay-suggestion-list' portalName='autocomplete-suggestion-list'> wrapper.

Why: Android's getBoundsInScreen() clamps a11y bounds to the parent's measured rect. The composer's wrapping View (~228 px with safe area padding) was clipping the absolutely positioned suggestion list to inverted/empty bounds - TalkBack saw nothing, taps didn't activate. Hoisting into the flex: 1 MessageList container restores valid a11y bounds. Verified with uiautomator dump.

PortalWhileClosingView cross screen leak fix

Removed the early return guard in syncPortalLayout:

if (!width || !height) {
  return;
}

The guard kept unmeasured (0×0) wrappers off the closing portal stack, but as a side effect, wrappers with no children (e.g. autocomplete list before the user types @) never registered. Navigating Channel -> Thread (both mount such wrappers, as an example) left the previous screen's stale entry as the only thing on the host stack and the closing overlay teleport then stamped Channel autocomplete content into the Thread screen. Removing the guard lets empty wrappers register; teleport for an empty wrapper renders null children so nothing visible.

Accessibility

  • New hook useAnnounceOnShow(visible, message, { delayMs?, priority? }) - announces on each visible: false -> true transition and resets on hide. Unlike useAnnounceOnStateChange, it doesn't dedupe consecutive identical strings, so reshows reannounce.
  • Applied to BottomSheetModal (replaces adhoc ref + useEffect) and AutoCompleteSuggestionList
  • ai-docs/accessibility.md and the team a11y skill updated to document useAnnounceOnShow, the menu/menuitem iOS only caveat and a new "floating overlays need a tall parent for Android a11y" rule.

ClippingFadeBottom

New UIComponents/ClippingFadeBottom.tsx reusable fade primitive used at the bottom edge of the suggestion list so long lists fade out instead of hard clipping at the composer edge.

Bundled bug fixes

  • iOS multiline TextInput regression after RN upgrade - caret jumping on newline
  • AutoCompleteSuggestionList animation desync when swithcing between attachment picker and keyboard
  • Accessibility bugs with the suggestions list

🎨 UI Changes

iOS
Before After
Android
Before After

🧪 Testing

☑️ Checklist

  • I have signed the Stream CLA (required)
  • PR targets the develop branch
  • Documentation is updated
  • New code is tested in main example apps, including all possible scenarios
    • SampleApp iOS and Android
    • Expo iOS and Android

@isekovanic isekovanic requested review from oliverlaz and szuperaz June 5, 2026 21:45
@Stream-SDK-Bot
Copy link
Copy Markdown
Contributor

Stream-SDK-Bot commented Jun 5, 2026

SDK Size

title develop branch diff status
js_bundle_size 1725 KB 1749 KB +24883 B 🔴

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.

2 participants