Message
One turn. The prop is `from`, not `role`. Markdown is INJECTED via renderMarkdown and defaults to plain Text, which keeps the core Expo Go-clean.
- v0.2.1
- component
- nativewind
- uniwind
- runs in Expo Go
Install
npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/message.jsonnpx @react-native-reusables/cli@latest add https://rnraielements.com/r/uniwind/message.jsonDependencies
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 |
https://reactnativereusables.com/r/uniwind/avatar.json |
https://reactnativereusables.com/r/uniwind/text.json |
https://rnraielements.com/r/uniwind/markdown.json |
AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92
Source
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
| Name | Type | Default | Description |
|---|---|---|---|
| from | MessageRole | — | — |
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>
)