Cariosan

React Native SDK

Drop-in mobile chat UI for iOS and Android, built on the same headless core as the web kit.

5 min readUpdated Jun 1, 2026

@cariosan/react-native is the mobile UI kit — native components (View/FlatList/TextInput) rendering the same chat experience as the web kit. It reuses the framework-agnostic core: @cariosan/client (transport) and the DOM-free hooks from @cariosan/react run unchanged in React Native, so engagement features (quoted reply, @-mentions, read receipts, search) behave identically to web.

What the kit gives you — and what you build

The kit ships building blocks, not whole screens. That's deliberate: chat UX is app-specific, and we don't want to drag a navigation library, a media picker, or Firebase into your bundle. The kit stays dependency-light; you bring your own platform pieces.

In the kit (@cariosan/react-native + @cariosan/client) — the engine + reusable UI:

  • Components: MessageList, MessageInput, ChannelList, TypingIndicator, PresenceBadge.
  • Hooks: useMessages (with reconnect resync), useChannels, usePresence, useReadReceipts, useMessageActions, useMessageSelection, useReplyDraft, …
  • All the logic on client / channel: send / edit / delete, reactions, receipts, search, @-mentions, attachment upload, group avatars + roles (updateChannel / addMembers / setMemberRole / removeMember), presence (with role), lastMessage preview.

You build (the app layer) — what's UX- or platform-specific:

  • Screens + navigation — chat list, chat room, group info, etc. Use any router (Expo Router, React Navigation, or a simple state switch); the kit doesn't impose one.
  • Auth — get a user JWT from your backend. (The example's "paste a JWT" login is a test shim, not production auth.)
  • Attachment picker UI — the kit gives you the hooks (MessageInput's onAttachPress + attachmentPreview + onSendAttachment, plus channel.uploadAttachment); you supply the picker with the library you prefer (expo-image-picker, react-native-image-picker, …).
  • Push — register the token via client.registerDevice(...); the Firebase / native wiring is yours (BYO).

The example is a reference, not a dependency

examples/mobile-react-native wires all of the above into a full app (chat list, group info, member actions, attachment panel). Copy its screens as a starting point — but they live in the example, not in the published @cariosan/* packages. A real app composes the kit the same way, with its own design.

Install

pnpm add @cariosan/client @cariosan/react @cariosan/react-native
# peers: react + react-native (provided by your app, e.g. Expo)

Provider

Wrap your app once. You own the SDK lifecycle (construct, connect, pause on background) — the provider just shares the client:

App.tsx
import { Cariosan } from "@cariosan/client";
import { ChatProvider, ReplyDraftProvider, MessageSelectionProvider } from "@cariosan/react-native";
 
const client = new Cariosan({ apiUrl: "https://api.example.com", token });
await client.connect();
 
export default function App() {
  return (
    <ChatProvider client={client}>
      <ReplyDraftProvider>
        {/* optional: enables hold-to-select (highlight + quick reactions) */}
        <MessageSelectionProvider>
          <ChatScreen />
        </MessageSelectionProvider>
      </ReplyDraftProvider>
    </ChatProvider>
  );
}

Components

ChatScreen.tsx
import {
  ChannelList,
  MessageList,
  MessageInput,
  TypingIndicator,
  PresenceBadge,
} from "@cariosan/react-native";
 
<MessageList channelId="order_42" currentUserId={myUserId} />
<TypingIndicator channelId="order_42" />
<MessageInput channelId="order_42" />
  • MessageList — a FlatList of messages in a familiar chat layout: day separators (Today / Yesterday / date), left/right bubbles (received in slate, your own right-aligned in emerald), bottom-right time + ✓/✓✓/✓✓-blue read-receipt ticks on your own messages, plus @-mention highlighting, quoted-reply previews (tap to scroll to the original), reaction pills, and edited/tombstone states. Inside a MessageSelectionProvider, long-press a message to highlight it and float a quick-reactions row (❤️ 👍 😂 …) above it. Props: currentUserId (internal id or external_id) decides which bubbles are "yours"; showSenderNames={false} hides the sender name on received bubbles for 1:1 chats (it shows by default, which suits group channels).
  • MessageInput — text composer with an @-mention autocomplete (tap to insert; pass mentionMembers or it lazily loads the roster), a 🙂 emoji inserter, a quote-reply chip, and a send button. Pass onAttachPress to show a 📎 button — your handler picks a file and stages it via attachmentPreview (a preview node) + onSendAttachment(caption), which fires when send is tapped (upload + send happen then, not on pick), e.g. channel.uploadAttachment(blob, { filename, mimeType })sendMessage({ attachmentUrl }). In MessageList, image attachments render as thumbnails and other files as a tappable document card; emoji-only messages render large and bubble-less.
  • ChannelList — the caller's channels with unread + mention badges.
  • TypingIndicator, PresenceBadge — live presence/typing.

Hold-to-select: drive your own action bar

MessageList only renders the in-place highlight + quick-reactions. The contextual action bar (reply / copy / delete / …) is yours to render — read useMessageSelection() in your header and swap the title for actions while a message is held, WhatsApp-style:

ChatHeader.tsx
import { useMessageSelection, useReplyDraft, useMessageActions } from "@cariosan/react-native";
import * as Clipboard from "expo-clipboard";
 
function ChatHeader({ channelId }: { channelId: string }) {
  const selection = useMessageSelection();
  const replyDraft = useReplyDraft();
  const { deleteMessage } = useMessageActions(channelId);
  const msg = selection?.selected;
  if (!msg) return <ContactTitle />; // normal header
 
  return (
    <ActionBar
      onClose={selection.clear}
      onReply={() => { replyDraft?.setReplyTo(msg); selection.clear(); }}
      onCopy={() => { void Clipboard.setStringAsync(msg.content); selection.clear(); }}
      onDelete={() => { void deleteMessage(msg.id); selection.clear(); }} // your own messages
    />
  );
}

deleteMessage is author-only (the server rejects deleting others' messages), so only show Delete when msg.user.id/external_id matches the current user. Copy needs a clipboard module — expo-clipboard works in Expo; the kit stays dependency-free.

Wrap your screen in a safe area

The kit components fill their parent and don't add device insets — that's the app's job. Without one, MessageInput's text field and send button sit behind the Android navigation bar (and the iOS home indicator) and can't be tapped. Use react-native-safe-area-context (bundled with Expo) — not react-native's SafeAreaView, which is iOS-only:

App.tsx
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
 
export default function App() {
  return (
    <SafeAreaProvider>
      <SafeAreaView style={{ flex: 1 }} edges={["top", "bottom"]}>
        {/* ChatProvider → MessageList / MessageInput … */}
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

Hooks

The kit re-exports every hook from @cariosan/react (they're DOM-free) — useMessages, useMessageSender, useChannels, useTyping, usePresence, useConnectionStatus, useReadReceipts, useMentions, useReplyDraft, useSearch, plus ChatProvider/useCariosan. Build fully custom RN UI on the hooks while reusing all the subscription/cursor plumbing. See the React hooks reference — the API is identical.

Styling

Components ship a sensible dark-slate default styled with StyleSheet. React Native has no CSS, so theming is via standard RN style props / forking the components — there's no stylesheet import like the web kit.

One core, every platform

@cariosan/client and the hooks are the single source of truth. The web (@cariosan/react) and mobile (@cariosan/react-native) kits differ only in their rendered components — so a feature shipped to the core lands on both.

Was this page helpful?

On this page