Install

npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/message.json

Dependencies

No required npm packages — this item installs through the registry dependencies below.

https://reactnativereusables.com/r/nativewind/avatar.json
https://reactnativereusables.com/r/nativewind/text.json
https://rnraielements.com/r/nativewind/markdown.json

AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92

Source

message.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/nativewind/components/ui/avatar'; import { Text, TextClassContext } from '@/registry/nativewind/components/ui/text'; import { repairIncompleteMarkdown } from '@/registry/nativewind/lib/markdown'; import { cn } from '@/registry/nativewind/lib/utils'; import * as React from 'react'; import { View, type ViewProps } from 'react-native'; /** * Message — one turn in the transcript. * * THE PROP IS `from`, NOT `role`. The web original uses `from={message.role}` and getting * this wrong is documented as the single most common port error, so it is preserved * verbatim rather than "improved". * * User messages get a contained bubble and reverse the row; assistant messages render * full-width and unbubbled. STYLE-PARITY REMEDIATION (design/style-parity-remediation.md * row 1): the bubble's values converge to the pinned web original — `bg-secondary` on * `text-foreground`, `rounded-lg`, `px-4 py-3` — replacing the port's invented * `bg-primary`/`text-primary-foreground`/`rounded-xl`/`px-3 py-2` family. The * MessageAvatar and the flex-row items-end/reverse orientation are a RECORDED MOBILE * ADDITION, kept per remediation row 1: the pinned web Message has no avatar. * * MARKDOWN IS INJECTED, NOT BUNDLED. `MessageResponse` takes `renderMarkdown` and defaults * to plain RNR Text. That ~3-line seam is load-bearing: the native markdown renderer * (react-native-enriched-markdown) is a Fabric module absent from Expo 57's bundled list, * so hard-binding it here would make the entire chat shell dev-client-only. With the seam, * the core stays Expo Go-clean and markdown is an opt-in registry item. */ type MessageRole = 'user' | 'assistant' | 'system'; const MessageContext = React.createContext<{ from: MessageRole }>({ from: 'assistant' }); const useMessage = () => React.useContext(MessageContext); type MessageProps = ViewProps & { from: MessageRole }; function Message({ from, className, ...props }: MessageProps) { return ( <MessageContext.Provider value={{ from }}> <View className={cn( 'w-full flex-row items-end gap-2', from === 'user' ? 'flex-row-reverse' : 'flex-row', className, )} {...props} /> </MessageContext.Provider> ); } function MessageAvatar({ source, fallback, className, }: { source?: string; fallback: string; className?: string }) { return ( // Explicit RN style sizing: same rationale as persona.tsx — the vendored Avatar's // percent-sized fallback balloons to the nearest definite ancestor if the root's // class-driven size is ever missing from the runtime stylesheet. <Avatar alt={fallback} className={cn('size-8', className)} style={{ width: 32, height: 32 }}> {source ? <AvatarImage source={{ uri: source }} /> : null} <AvatarFallback> <Text className="text-xs">{fallback}</Text> </AvatarFallback> </Avatar> ); } type MessageContentProps = ViewProps & { variant?: 'contained' | 'flat' }; function MessageContent({ className, variant, ...props }: MessageContentProps) { const { from } = useMessage(); // The web default: user is contained, assistant is flat and full-width. Remediation // row 1: the user bubble converges to bg-secondary, so its text runs on // text-foreground like every other branch — the provider value no longer forks on // role. const contained = variant ? variant === 'contained' : from === 'user'; return ( <TextClassContext.Provider value="text-foreground"> <View className={cn( 'max-w-[85%] gap-2', contained && 'rounded-lg px-4 py-3', contained && from === 'user' && 'bg-secondary', contained && from !== 'user' && 'bg-muted', !contained && 'max-w-full flex-1', className, )} {...props} /> </TextClassContext.Provider> ); } export type RenderMarkdown = (markdown: string) => React.ReactNode; type MessageResponseProps = { children: string; /** * Inject a markdown renderer. Omit it and the body renders as plain Text, which is why * the core has no native-module dependency. */ renderMarkdown?: RenderMarkdown; /** * Repair a half-streamed token so `**bold` does not flash literal asterisks mid-stream. * Removing this is the documented regression — every streamed message flickers raw syntax. */ parseIncompleteMarkdown?: boolean; className?: string; }; function MessageResponse({ children, renderMarkdown, parseIncompleteMarkdown = true, className, }: MessageResponseProps) { const text = React.useMemo( () => (parseIncompleteMarkdown ? repairIncompleteMarkdown(children) : children), [children, parseIncompleteMarkdown], ); if (renderMarkdown) return <>{renderMarkdown(text)}</>; return <Text className={cn('text-base', className)}>{text}</Text>; } /** * Actions are ALWAYS VISIBLE. The web original reveals them on hover, which on touch * renders identically at rest and is simply unreachable. */ function MessageActions({ className, ...props }: ViewProps) { return ( <TextClassContext.Provider value="text-xs text-muted-foreground"> <View className={cn('flex-row items-center gap-1 pt-1', className)} {...props} /> </TextClassContext.Provider> ); } export { Message, MessageActions, MessageAvatar, MessageContent, MessageResponse, useMessage, };

Props

NameTypeDefaultDescription
fromMessageRole——

Theming

Themed via bg-secondary, text-foreground, bg-primary, text-primary-foreground, bg-muted, text-muted-foreground.

Usage

(o) => ( <Message from={(o.from as 'user' | 'assistant') ?? 'assistant'}> <Text className="text-sm text-foreground"> The 56-item set installs through the real RNR CLI at Expo 57 pins. </Text> </Message> )