JS SDK — React Native (@loopchat/react-native)

React Native bindings: re-exports core + the react hooks (they're platform-neutral), replaces the DOM components with RN-native UI, and adds FCM push helpers.

  • Repo: js-sdk/packages/react-native.
  • Dependencies: @loopchat/core, @loopchat/react (workspace). Peers: react ^18 || ^19, react-native >= 0.73.
  • Firebase messaging is a peer concern: the push helpers match @react-native-firebase/messaging's shape without a hard dependency on it.
  • Tests: jest + @testing-library/react-native; the Expo demo (js-sdk/examples/rn-demo) is the manual smoke target.
import {
  Chat, Channel, MessageList, MessageInput,
  registerPushDevice, parsePushPayload,
} from '@loopchat/react-native';

<Chat client={client}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput keyboardVerticalOffset={headerHeight} />
  </Channel>
</Chat>

What's re-exported vs native

  • From @loopchat/react: the <Chat>/<Channel> providers and every hook (useChatClient, useChannel, useStore, useMessages, useMembers, useCurrentUser, useUnreadCount, useChannelUnreadCount, useConnectionStatus, useChannelList). The DOM components stay in @loopchat/react.
  • From @loopchat/core: LoopChatClient, Filter, devToken, LoopChatNetworkError, Store, and the common types (including PushPayload).
  • RN-native UI (this package): <MessageList>, <MessageInput>, <Avatar>.

RN-native components

Component Props Behavior
<MessageList> onMessagePress?(message) Inverted FlatList (windowed, keyboard-friendly) bound to the enclosing <Channel> — the RN equivalent of Flutter's LoopMessageListView. Own messages right-aligned, sender name + avatar for others, day separators, end-reach pagination for older history (channel.query()), and marks the channel read while mounted.
<MessageInput> onMessageSent?(message), placeholder?, disabled?, keyboardVerticalOffset? Composer wrapped in KeyboardAvoidingView (iOS padding behavior): multiline TextInput, send button + send-on-submit, sending/disabled states, inline error display. keyboardVerticalOffset is forwarded (e.g. header height).
<Avatar> name, image? Round avatar via Image, initials fallback.

Push helpers

Wire FCM token acquisition and rotation to TheLoopChat device registration. PushTokenSource matches @react-native-firebase/messaging without depending on it:

interface PushTokenSource {
  getToken(): Promise<string>;
  onTokenRefresh?(listener: (token: string) => void): () => void;
}
Function Description
registerPushDevice(client, source): Promise<() => void> Gets the current token, calls client.addDevice(token, 'firebase'), and keeps the registration fresh across token rotations (removes the previous token best-effort, registers the new one). Returns a cleanup function that stops listening for refreshes — it does not unregister the device; do that with unregisterPushDevice on logout.
unregisterPushDevice(client, token) Removes a device registration (e.g. on logout).
parsePushPayload(remoteMessage): PushPayload | null Parses an FCM data message (RemoteMessageLike — the shape @react-native-firebase/messaging hands to handlers) into the documented PushPayload (type/cid/channel_id/…), or null when the message is not a TheLoopChat push.
import messaging from '@react-native-firebase/messaging';

const stop = await registerPushDevice(client, {
  getToken: () => messaging().getToken(),
  onTokenRefresh: (cb) => messaging().onTokenRefresh(cb),
});

messaging().onMessage((remoteMessage) => {
  const push = parsePushPayload(remoteMessage);
  if (push) navigateToChannel(push.channel_type, push.channel_id);
});

See API — Push and Webhooks for the payload contract and worker behavior.