Ethora Chat Component (@ethora/chat-component)

May 10, 2026 · View on GitHub

GitHub watchers GitHub forks GitHub Repo stars GitHub repo size GitHub language count GitHub top language GitHub commit activity (branch) GitHub issues GitHub closed issues GitHub GitHub contributors

JavaScript React TypeScript JWT

Discord Twitter URL Website YouTube Channel Subscribers

React + TypeScript chat UI component powered by Ethora backend APIs and XMPP.
Use it as a standalone chat page, as an embedded widget in your existing app, or as a customizable chat foundation with your own auth and UI.

Table of Contents

Overview

@ethora/chat-component gives you a production-oriented chat interface with:

  • Room list and room chat UI
  • Message history, replies, reactions, edits, deletes
  • Typing indicators
  • In-app notifications + Web Push integration
  • Configurable auth modes (default/login form/google/jwt/custom user)
  • Custom render components for message/input/scroll/day separator/new-message label

The package exports:

  • Chat (main component)
  • XmppProvider
  • useUnread
  • logoutService
  • useQRCodeChat, handleQRChatId
  • useInAppNotifications
  • usePushNotifications
  • resendMessage

Why Ethora

Ethora provides hosted and customizable messaging infrastructure plus a wider product ecosystem.

DimensionEthora Chat ComponentFull Ethora Platform
Primary goalEmbed chat quickly in a React appEnd-to-end product stack (chat, profiles, wallets, AI, admin)
Time to first chatMinutesHigher initial setup, broader capabilities
Frontend scopeFocused web chat UI packageMulti-product ecosystem and broader SDK/tooling
Custom UI controlHigh via props + custom componentsHigh, with additional platform-specific tooling
Best fitSupport chat, portal messaging, embedded chat widgetFull social/messaging app platforms with extended modules

Quick Start

1. Install

ToolCommand
npmnpm i @ethora/chat-component
yarnyarn add @ethora/chat-component
pnpmpnpm add @ethora/chat-component
bunbun add @ethora/chat-component

2. Render the chat

import { Chat, XmppProvider } from '@ethora/chat-component';
import './App.css';

export default function App() {
  return (
    <XmppProvider>
      <Chat />
    </XmppProvider>
  );
}

Required wrapper (XmppProvider)

Chat relies on internals that use useXmppClient(). In real integrations, wrap Chat (or your entire app shell) with XmppProvider:

import { Chat, XmppProvider } from '@ethora/chat-component';

export default function App() {
  return (
    <XmppProvider>
      <Chat config={{ baseUrl: 'https://api.chat.ethora.com/v1' }} />
    </XmppProvider>
  );
}

XmppProvider also accepts a pushNotifications prop for headless push setup (works even if Chat is not rendered yet):

import { XmppProvider } from '@ethora/chat-component';

export default function App() {
  return (
    <XmppProvider
      config={{ baseUrl: 'https://api.chat.ethora.com/v1', initBeforeLoad: true }}
      pushNotifications={{
        enabled: true,
        softAsk: false,
        vapidPublicKey: 'PLACEHOLDER_VAPID_PUBLIC_KEY',
        iconPath: '/icons/push-icon-192.png',
        badgePath: '/icons/push-badge-72.png',
        firebaseConfig: {
          apiKey: 'PLACEHOLDER_API_KEY',
          authDomain: 'PLACEHOLDER_AUTH_DOMAIN',
          projectId: 'PLACEHOLDER_PROJECT_ID',
          storageBucket: 'PLACEHOLDER_STORAGE_BUCKET',
          messagingSenderId: 'PLACEHOLDER_MESSAGING_SENDER_ID',
          appId: 'PLACEHOLDER_APP_ID',
        },
      }}
    >
      {/* Chat can be mounted later or omitted */}
      <div>App shell</div>
    </XmppProvider>
  );
}

Single XMPP Initialization Contract

To avoid duplicated wss://.../ws connections, keep a single XMPP init source:

  • initBeforeLoad: true -> XmppProvider is the only place that initializes XMPP.
  • initBeforeLoad: false -> Chat (useChatWrapperInit) initializes XMPP.

If your app also has external client.login(...) logic, guard it:

if (chatConfig.initBeforeLoad) {
  console.warn('[XMPP] initBeforeLoad=true, skip external client.login()');
  return;
}

await client.login(...);

Also pass a memoized config object to both XmppProvider and Chat:

const chatConfig = useMemo(() => ({ ...baseConfig }), [baseConfig]);

<XmppProvider config={chatConfig}>
  <Chat config={chatConfig} />
</XmppProvider>;

3. Run

npm run dev

Open http://localhost:5173.

Integration Modes

All modes below assume XmppProvider wraps Chat.

A) Minimal demo mode (provider + chat)

<XmppProvider>
  <Chat />
</XmppProvider>

Useful for local proof-of-concept and quick UI validation.

B) Auto default credential fallback (legacy behavior)

If no googleLogin, no jwtLogin, no userLogin, and no defaultLogin, LoginWrapper currently triggers internal email/password fallback logic.

<XmppProvider>
  <Chat config={{ colors: { primary: '#2563eb', secondary: '#dbeafe' } }} />
</XmppProvider>

C) Explicit email/password via user prop

<XmppProvider>
  <Chat
    user={{
      email: 'user@example.com',
      password: 'PLACEHOLDER_PASSWORD',
    }}
  />
</XmppProvider>

D) Injected logged-in user (userLogin)

<XmppProvider>
  <Chat
    config={{
      userLogin: {
        enabled: true,
        user: {
          _id: 'PLACEHOLDER_USER_ID',
          appId: 'PLACEHOLDER_APP_ID',
          walletAddress: 'PLACEHOLDER_WALLET_ADDRESS',
          defaultWallet: { walletAddress: 'PLACEHOLDER_WALLET_ADDRESS' },
          firstName: 'Jane',
          lastName: 'Doe',
          xmppPassword: 'PLACEHOLDER_XMPP_PASSWORD',
          token: 'PLACEHOLDER_ACCESS_TOKEN',
          refreshToken: 'PLACEHOLDER_REFRESH_TOKEN',
          username: 'PLACEHOLDER_USERNAME',
        },
      },
    }}
  />
</XmppProvider>

E) JWT login

<XmppProvider>
  <Chat
    config={{
      jwtLogin: {
        enabled: true,
        token: 'PLACEHOLDER_JWT_TOKEN',
      },
    }}
  />
</XmppProvider>

F) Google login

<XmppProvider>
  <Chat
    config={{
      googleLogin: {
        enabled: true,
        firebaseConfig: {
          apiKey: 'PLACEHOLDER_API_KEY',
          authDomain: 'PLACEHOLDER_AUTH_DOMAIN',
          projectId: 'PLACEHOLDER_PROJECT_ID',
          storageBucket: 'PLACEHOLDER_STORAGE_BUCKET',
          messagingSenderId: 'PLACEHOLDER_MESSAGING_SENDER_ID',
          appId: 'PLACEHOLDER_APP_ID',
        },
      },
    }}
  />
</XmppProvider>

G) Single-room entry + URL/QR behavior

<XmppProvider>
  <Chat
    roomJID="ROOM_JID@conference.xmpp.chat.ethora.com"
    config={{
      setRoomJidInPath: true,
      qrUrl: 'https://your-app.example/chat/?qrChatId=',
    }}
  />
</XmppProvider>

roomJID forces entry room.
setRoomJidInPath syncs room identity to URL path.
useQRCodeChat / handleQRChatId support QR/deep-link room opening.

Behavior Notes and Legacy Quirks

  • newArch is now default-on. If omitted, runtime uses new architecture paths.
  • Old architecture is used only when you explicitly set config.newArch = false.
  • defaultLogin currently has legacy inverted behavior in LoginWrapper:
    • internal fallback login runs when login modes are not configured and defaultLogin is not set.
    • keep this in mind when migrating; prefer explicit userLogin / jwtLogin / googleLogin.

Chat Props Reference

These are the top-level props accepted by Chat (exported from ReduxWrapper).

PropTypeRequiredNotes
configIConfigNoMain behavior/configuration object.
roomJIDstringNoForce specific room JID on load.
user{ email: string; password: string }NoCredentials for email/password login helper path.
loginData{ email: string; password: string }NoOptional login payload.
MainComponentStylesReact.CSSPropertiesNoOuter container style override.
tokenstringNoOptional token input (legacy/integration-specific usage).
CustomMessageComponentReact.ComponentType<MessageProps>NoReplace message bubble rendering.
CustomInputComponentReact.ComponentType<SendInputProps & { onSendMessage?; onSendMedia?; placeholderText?; }>NoReplace chat input area.
CustomScrollableAreaReact.ComponentType<CustomScrollableAreaProps>NoReplace list/scroll wrapper behavior.
CustomDaySeparatorReact.ComponentType<DaySeparatorProps>NoReplace day separator node.
CustomNewMessageLabelReact.ComponentType<NewMessageLabelProps>NoReplace "new message" marker.

Full Config Reference (IConfig)

Below is a grouped reference for all config options.

Core

OptionTypeDescription
appIdstringApp identifier for backend context.
baseUrlstringAPI base URL (defaults to https://api.chat.ethora.com/v1, the Ethora Cloud production endpoint).
customAppTokenstringCustom app token for API initialization.
xmppSettings{ devServer; host; conference?; xmppPingOnSendEnabled? }XMPP connectivity settings.
initBeforeLoadbooleanInitialize XMPP before normal chat load flow.
clearStoreBeforeInitbooleanClear local store before initialization.
newArchbooleanDefaults to true; set false to explicitly force legacy/old architecture paths.
useStoreConsoleEnabledbooleanEnable verbose internal logging in console.

UI and Layout

OptionTypeDescription
disableHeaderbooleanHide chat header.
disableMediabooleanDisable media sending/processing paths.
disableRoomsbooleanHide/disable room list area.
disableRoomMenubooleanDisable room menu controls.
disableRoomConfigbooleanDisable room configuration actions.
disableNewChatButtonbooleanHide new chat/create room action.
disableUserCountbooleanHide user count in header/UI.
disableChatInfo{ disableHeader?; disableDescription?; disableType?; disableMembers?; hideMembers?; disableChatHeaderMenu? }Fine-grained chat info panel toggles.
chatHeaderBurgerMenubooleanToggle burger menu in chat header.
chatHeaderSettings{ hide?; disableCreate?; disableMenu?; hideSearch? }Additional header-level controls.
chatHeaderAdditional{ enabled: boolean; element: any }Inject custom element into header area.
headerLogostring | React.ReactElementCustom logo in header.
headerMenu() => voidCustom menu handler.
headerChatMenu() => voidCustom room header menu handler.
colors{ primary: string; secondary: string }Theme colors for component UI.
roomListStylesReact.CSSPropertiesStyles for room list pane.
chatRoomStylesReact.CSSPropertiesStyles for chat pane.
noMessagesPlaceholderReact.ComponentTypeReplace empty-chat placeholder component (same render position as default placeholder).
backgroundChat{ color?: string; image?: string | File }Chat background customization.
bubleMessageMessageBubbleBubble-level style overrides (as defined in types).
setRoomJidInPathbooleanSync room JID to URL path.
qrUrlstringBase URL for QR deep link behavior.

Auth and Identity

OptionTypeDescription
defaultLoginbooleanLegacy quirk: current runtime fallback behavior is inverted; see Behavior Notes section.
googleLogin{ enabled: boolean; firebaseConfig: FBConfig }Google login support via Firebase config.
jwtLogin{ token: string; enabled: boolean; handleBadlogin?: React.ReactElement }Log user in using JWT exchange flow.
userLogin{ enabled: boolean; user: User | null }Inject already-authenticated user directly.
customLogin{ enabled: boolean; loginFunction: () => Promise<User | null> }Provide your custom async login function.
refreshTokens{ enabled: boolean; refreshFunction?: () => Promise<{ accessToken: string; refreshToken?: string } | null> }Token refresh strategy.

Rooms and Data

OptionTypeDescription
defaultRoomsConfigRoom[]Seed/default rooms.
customRooms{ rooms: PartialRoomWithMandatoryKeys[]; disableGetRooms?: boolean; singleRoom: boolean }Fully controlled room source.
forceSetRoombooleanForce room setup path in init flow.
enableRoomsRetry{ enabled: boolean; helperText: string }Enable retry UX when rooms fail to load.

Messaging and Interactions

OptionTypeDescription
disableInteractionsbooleanDisable message interaction menu/actions.
disableProfilesInteractionsbooleanDisable profile interactions from chat UI.
disableSentLogicbooleanDisable default sent-state logic when needed.
secondarySendButton{ enabled: boolean; messageEdit: string; label?: React.ReactNode; buttonStyles?: React.CSSProperties; hideInputSendButton?: boolean; overwriteEnterClick?: true }Extra send action/button config.
botMessageAutoScrollbooleanForce auto-scroll behavior on bot messages.
messageTextFilter{ enabled: boolean; filterFunction: (text: string) => string }Transform/filter outgoing message text.
eventHandlers{ onMessageSent?; onMessageFailed?; onMessageEdited? }Lifecycle callbacks for message operations.
translates{ enabled: boolean; translations?: Iso639_1Codes }Message translation-related options.
whitelistSystemMessagestring[]Restrict/render only selected system message types.
customSystemMessageReact.ComponentType<MessageProps>Replace system message component renderer.

Typing and Sending Control

OptionTypeDescription
disableTypingIndicatorbooleanDisable typing indicator UI logic.
customTypingIndicator{ enabled: boolean; text?: string | ((usersTyping: string[]) => string); position?: 'bottom' | 'top' | 'overlay' | 'floating'; styles?: React.CSSProperties; customComponent?: React.ComponentType<{ usersTyping: string[]; text: string; isVisible: boolean; }> }Customize typing indicator content and rendering.
blockMessageSendingWhenProcessingboolean | { enabled: boolean; timeout?: number; onTimeout?: (roomJID: string) => void }Gate sends while processing in-flight state.

Notifications

OptionTypeDescription
inAppNotifications{ enabled?; showInContext?; position?; maxNotifications?; duration?; onClick?; customComponent? }In-app toast notification behavior and custom rendering.

Push

OptionTypeDescription
pushNotifications.enabledbooleanEnable browser push subscription flow.
pushNotifications.vapidPublicKeystringVAPID public key for push registration.
pushNotifications.firebaseConfigFBConfigFirebase app config for push messaging.
pushNotifications.serviceWorkerPathstringService worker path, default /firebase-messaging-sw.js.
pushNotifications.serviceWorkerScopestringService worker scope, default /.
pushNotifications.iconPathstringCustom icon URL/path for OS push notifications.
pushNotifications.badgePathstringCustom badge URL/path for OS push notifications. Falls back to iconPath.
pushNotifications.softAskbooleanDo not immediately trigger browser permission prompt.
pushNotifications.onClick(params) => void | Promise<void>Callback invoked when the user clicks an OS push notification (including cold start via URL marker).

Custom Widgets and Overrides

You can replace key UI parts without forking the package.

Override propPurpose
CustomMessageComponentFully custom message bubble/row rendering.
CustomInputComponentCustom composer and send controls.
CustomScrollableAreaCustom scroll/list container (virtualized or custom behavior).
CustomDaySeparatorCustom date separator component.
CustomNewMessageLabelCustom "new message" divider label.

Example:

import { Chat } from '@ethora/chat-component';
import CustomMessageBubble from './CustomMessageBubble';
import CustomChatInput from './CustomChatInput';
import CustomScrollableArea from './CustomScrollableArea';
import CustomDaySeparator from './CustomDaySeparator';
import CustomNewMessageLabel from './CustomNewMessageLabel';

export default function App() {
  return (
    <Chat
      CustomMessageComponent={CustomMessageBubble}
      CustomInputComponent={CustomChatInput}
      CustomScrollableArea={CustomScrollableArea}
      CustomDaySeparator={CustomDaySeparator}
      CustomNewMessageLabel={CustomNewMessageLabel}
      config={{
        colors: { primary: '#1d4ed8', secondary: '#dbeafe' },
      }}
    />
  );
}

Reference example components in repository:

  • src/examples/customComponents/CustomMessageBubble.tsx
  • src/examples/customComponents/CustomChatInput.tsx
  • src/examples/customComponents/CustomScrollableArea.tsx
  • src/examples/customComponents/CustomDaySeparator.tsx
  • src/examples/customComponents/CustomNewMessageLabel.tsx

Push Notifications

Prerequisites

RequirementWhy
HTTPS origin (or localhost)Browser push APIs require secure contexts.
Firebase projectFCM token + push transport setup.
VAPID public keyRequired for web push subscription.
Service worker fileRequired for background notification handling.

Setup steps

  1. Copy service worker into your app's public assets:
npx @ethora/chat-component ethora-chat
  1. Configure push in config:
<Chat
  config={{
    pushNotifications: {
      enabled: true,
      vapidPublicKey: 'PLACEHOLDER_VAPID_PUBLIC_KEY',
      firebaseConfig: {
        apiKey: 'PLACEHOLDER_API_KEY',
        authDomain: 'PLACEHOLDER_AUTH_DOMAIN',
        projectId: 'PLACEHOLDER_PROJECT_ID',
        storageBucket: 'PLACEHOLDER_STORAGE_BUCKET',
        messagingSenderId: 'PLACEHOLDER_MESSAGING_SENDER_ID',
        appId: 'PLACEHOLDER_APP_ID',
      },
      serviceWorkerPath: '/firebase-messaging-sw.js',
      serviceWorkerScope: '/',
      iconPath: '/icons/push-icon-192.png',
      badgePath: '/icons/push-badge-72.png',
      softAsk: false,
      onClick: async ({ roomJID, messageId, url, data }) => {
        // Your app-level routing/analytics can live here.
        console.log('Push clicked:', { roomJID, messageId, url, data });
      },
    },
  }}
/>
  1. Optional: use hook directly for controlled permission flow:
import { usePushNotifications } from '@ethora/chat-component';

function PushPermissionButton() {
  const { requestPermission } = usePushNotifications({
    enabled: true,
    softAsk: true,
    vapidPublicKey: 'PLACEHOLDER_VAPID_PUBLIC_KEY',
  });

  return <button onClick={() => requestPermission()}>Enable Push</button>;
}

iconPath and badgePath should point to public, reachable assets (for example, files from your app public/ directory).

Auth Strategies

StrategyConfig shapeBest for
Default fallback (legacy quirk)no auth block / defaultLoginLegacy/demo flows; prefer explicit auth modes in production
Injected useruserLogin: { enabled: true, user }App already has authenticated user/session
JWT loginjwtLogin: { enabled: true, token }Token-based backend auth flow
Google logingoogleLogin: { enabled: true, firebaseConfig }Google SSO using Firebase
Custom login functioncustomLogin: { enabled: true, loginFunction }Fully custom identity provider

Example: injected user (bypass login screen)

<Chat
  config={{
    userLogin: {
      enabled: true,
      user: {
        _id: 'PLACEHOLDER_USER_ID',
        appId: 'PLACEHOLDER_APP_ID',
        walletAddress: 'PLACEHOLDER_WALLET_ADDRESS',
        defaultWallet: { walletAddress: 'PLACEHOLDER_WALLET_ADDRESS' },
        firstName: 'Jane',
        lastName: 'Doe',
        xmppPassword: 'PLACEHOLDER_XMPP_PASSWORD',
        token: 'PLACEHOLDER_ACCESS_TOKEN',
        refreshToken: 'PLACEHOLDER_REFRESH_TOKEN',
        username: 'PLACEHOLDER_USERNAME',
      },
    },
  }}
/>

Example: JWT login

<Chat
  config={{
    jwtLogin: {
      enabled: true,
      token: 'PLACEHOLDER_JWT_TOKEN',
    },
    refreshTokens: {
      enabled: true,
      refreshFunction: async () => {
        return {
          accessToken: 'PLACEHOLDER_NEW_ACCESS_TOKEN',
          refreshToken: 'PLACEHOLDER_NEW_REFRESH_TOKEN',
        };
      },
    },
  }}
/>

Hooks and API Exports

ExportTypePurpose
ChatReact componentMain chat component.
XmppProviderReact providerProvides XMPP client context for internal hooks/state.
useUnreadhookReturns unread counters.
logoutServiceserviceProgrammatic logout utility.
useQRCodeChathookHandle QR-based room links.
handleQRChatIdfunctionParse/process QR chat ID from URL.
useInAppNotificationshookEnables and handles in-app notifications.
usePushNotificationshookPush subscription + foreground handling workflow.
resendMessagefunctionRetry sending failed/pending messages.

Basic hook usage:

import { useUnread, logoutService } from '@ethora/chat-component';

function HeaderActions() {
  const { totalCount } = useUnread();

  return (
    <div>
      <span>Unread: {totalCount}</span>
      <button onClick={() => logoutService.performLogout()}>Logout</button>
    </div>
  );
}

logoutService.performLogout() behavior:

  • Dispatches chatSettingStore/logout
  • Dispatches rooms/setLogoutState
  • Dispatches roomHeap/clearHeap
  • Triggers logout middleware, which emits ethora-xmpp-logout
  • XmppProvider listens to that event and disconnects active XMPP client

Use Cases and Feature Coverage

AreaStatus in this package
One-room embedded chatAvailable
Multi-room chat UIAvailable
Message interactions (reply/copy/edit/delete/report/reactions)Available
Typing indicatorAvailable
Profile interactions in chatAvailable (can be disabled)
File/media attachmentsAvailable with ongoing enhancements
In-app notificationsAvailable
Web push notificationsAvailable
Wallet/assets and extended social modulesPrimarily in full Ethora platform

Hosted vs Self-Host Guidance

ModelBest forProsTradeoffs
Hosted Ethora backendFast time-to-market, smaller teams, MVPsFast setup, managed backend operations, easier push/auth onboardingLess infrastructure-level control
Self-hosted Ethora stackRegulated environments, deep infra controlFull control over infrastructure, compliance customization, internal network deployment optionsHigher DevOps/maintenance overhead
HybridGradual migration or split workloadsCan start fast and migrate critical paths laterMore architecture complexity

Feature Roadmap

This is a practical planning snapshot for cross-platform consumers. It is not a release commitment.

SurfaceCurrent stateNotes
Web React (@ethora/chat-component)Available nowThis repository.
React NativeVia broader Ethora stackTrack platform-specific implementation in Ethora repos/docs.
Swift (iOS native)Planned / ecosystem-levelConfirm status with Ethora team for production timelines.
Kotlin (Android native)Planned / ecosystem-levelConfirm status with Ethora team for production timelines.
FlutterPlanned / ecosystem-levelConfirm status with Ethora team for production timelines.
Additional roadmap itemsOngoingMedia improvements, richer profile/wallet experiences, broader integration guides.

Troubleshooting

IssueLikely causeFix
useXmppClient must be used within an XmppProviderUsing internal XMPP-dependent logic without provider contextWrap the app tree with XmppProvider where needed.
Chat loads but no rooms appearAuth/app context mismatch or room fetching restrictionsVerify appId, user credentials/tokens, baseUrl, and customRooms settings.
Push permission never appearssoftAsk: true without manual trigger, insecure origin, or missing VAPID keyTrigger requestPermission(), use HTTPS/localhost, set valid VAPID key.
Service worker not foundfirebase-messaging-sw.js missing in public dirRun npx @ethora/chat-component ethora-chat or copy file manually.
Login loop / auth failureWrong token/user object shapeValidate jwtLogin, userLogin.user, and refresh token flow.

Testing

Same two-layer split as the Android and iOS SDKs.

Layer 1 — Vitest + React Testing Library (this repo)

Hermetic component tests, no real server, no XMPP. Run with:

npm test          # one-shot
npm run test:watch # watch mode
WhereWhatRun with
src/**/*.test.tsxComponent tests using @testing-library/react — render the component in isolation, drive it with userEvent, assert behaviornpm test
src/test/setup.tsVitest global setup (jest-dom matchers, jsdom polyfills for matchMedia / IntersectionObserver / scrollIntoView)(loaded automatically)
src/test/renderWithProviders.tsxTest render helper that wraps a component with a fresh Redux store + ToastProvider — no persisted-store / saga / XMPP middleware leaking between tests(imported by tests)
src/test/testIds.tsStable data-testid constants matching Android *TestTags and iOS *AccessibilityID, so a single Maestro flow exercises the same intent on either mobile platform(imported by tests + components)

Current Layer-1 coverage

ComponentTestAsserts
<Login />renders email + password fields and submit buttondata-testid selectors all resolve
<Login />shows email validation error for an invalid emailBad email → error message + no API call fires
<Login />shows password length error for short passwordsPassword < 6 chars → error + no API call
<Login />calls loginEmail with valid credentialsValid creds → mocked loginEmail called once with the right args

Gaps to cover in follow-up PRs (file an issue + a test in the same PR when you tackle one):

  • <Register /> — same shape as Login + Google sign-up branch
  • <MessageBubble /> — body / deleted / sendFailed / reaction states
  • <RoomList /> — search filter, active-room highlight, badge counts
  • <MessageInput /> (chat input) — send callback fires, edit mode, reply mode, disabled state
  • <URLPreviewCard /> — link extraction + image fallback

Layer 2 — End-to-end Playwright

The host app ethora-app-reactjs runs a Playwright suite. Its current scope is public-page smoke (login / register / 404). The chat-component flows themselves are not yet covered by Playwright — when they are, those tests should resolve nodes via the same data-testid values exported from src/test/testIds.ts here.

Cross-platform parity

IdentifierThis repoAndroidiOS
chat_inputChatInputTestIds.inputFieldChatInputTestTags.INPUT_FIELDChatInputAccessibilityID.inputField
chat_send_buttonChatInputTestIds.sendButtonChatInputTestTags.SEND_BUTTONChatInputAccessibilityID.sendButton
chat_attach_buttonChatInputTestIds.attachButtonChatInputTestTags.ATTACH_BUTTONChatInputAccessibilityID.attachButton
chat_message_imageMessageBubbleTestIds.mediaContentMessageBubbleTestTags.MEDIA_CONTENTMessageBubbleAccessibilityID.mediaContent
rooms_listRoomListTestIds.roomsListRoomListViewTestTags.ROOMS_LISTRoomListAccessibilityID.roomsList
room_rowRoomListTestIds.roomRowRoomListViewTestTags.ROOM_ROWRoomListAccessibilityID.roomRow
auth_email_inputAuthTestIds.emailInput(web-only)(web-only)
auth_submit_buttonAuthTestIds.submitButton(web-only)(web-only)

Adding a test for a fix or new feature

  • Behavior bug in a chat component → add a Vitest test in this repo, in the same PR as the fix.
  • Integration bug (something a host app sees but a hermetic test can't) → add a Playwright test in ethora-app-reactjs, in a paired PR.
  • Cross-platform parity gap → make sure the matching Android Compose test or Maestro flow exists too.

Product

Developer Docs

Community and Support

License

AGPL. See LICENSE.txt.