mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-13 21:01:16 +08:00
832 lines
28 KiB
Markdown
832 lines
28 KiB
Markdown
# Chatwoot React Components - Architecture Documentation
|
|
|
|
This document provides comprehensive context for the embeddable chat UI components built with React by wrapping Vue components as Web Components.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Architecture Diagram](#architecture-diagram)
|
|
3. [Build System](#build-system)
|
|
4. [Component Hierarchy](#component-hierarchy)
|
|
5. [Key Patterns](#key-patterns)
|
|
6. [Runtime Theme Customization](#runtime-theme-customization)
|
|
7. [Real-time Communication](#real-time-communication)
|
|
8. [File Structure](#file-structure)
|
|
9. [Extension Guide](#extension-guide)
|
|
10. [Known Limitations](#known-limitations)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The system enables embedding Chatwoot's conversation UI into any React application. It achieves this through a **three-layer architecture**:
|
|
|
|
1. **React Layer** - Provider pattern for configuration and React-friendly API
|
|
2. **Web Component Layer** - Vue components converted to Custom Elements via `defineCustomElement`
|
|
3. **Vue Layer** - Existing Chatwoot Vue components reused without modification
|
|
|
|
### Why This Approach?
|
|
|
|
- **Code Reuse**: Leverage existing battle-tested Vue components (`components-next/message/*`)
|
|
- **Framework Agnostic**: Web Components work in any framework (React, Angular, vanilla JS)
|
|
- **Isolated Styles**: Shadow DOM encapsulation prevents CSS conflicts
|
|
- **Shared State**: Single Vuex store instance across all components
|
|
|
|
---
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ React Application │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ <ChatwootProvider> │
|
|
│ ├── Sets up globals (window.__WOOT_*) │
|
|
│ ├── Initializes Vuex store │
|
|
│ ├── Registers Web Components │
|
|
│ └── Initializes ActionCable (WebSocket) │
|
|
│ │
|
|
│ <ChatwootConversation> │
|
|
│ └── <ChatwootMessageListWrapper> │
|
|
│ └── <chatwoot-message-list> (Web Component) │
|
|
│ └── [Shadow DOM] │
|
|
│ └── MessageList.vue │
|
|
│ ├── Message.vue (components-next) │
|
|
│ ├── TypingIndicator.vue │
|
|
│ └── LiteReplyBox.vue │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Build System
|
|
|
|
### Build Modes (vite.config.ts)
|
|
|
|
The Vite configuration supports multiple build modes via `BUILD_MODE` environment variable:
|
|
|
|
| Mode | Command | Output | Purpose |
|
|
|------|---------|--------|---------|
|
|
| `library` | `pnpm build:sdk` | `public/packs/js/sdk.js` | Widget SDK (IIFE) |
|
|
| `ui` | `pnpm build:ui` | `public/packs/js/ui.js` | Standalone UI (IIFE) |
|
|
| `react-components` | `pnpm build:react` | `public/packs/react-components.{es,cjs}.js` | NPM package (ES + CJS) |
|
|
| (default) | `bin/vite build` | Standard Vite output | Main app build |
|
|
|
|
### React Components Build Pipeline
|
|
|
|
```bash
|
|
# Full build and package
|
|
pnpm package:react
|
|
|
|
# Build only (no packaging)
|
|
pnpm build:react
|
|
|
|
# Copy mode (skip build, just package)
|
|
node scripts/package-react-components.js --copyMode
|
|
```
|
|
|
|
**Build Output** (`dist/react-components/`):
|
|
- `index.js` - ES module entry
|
|
- `index.cjs` - CommonJS entry
|
|
- `style.css` - Bundled styles
|
|
- `package.json` - Generated with timestamp versioning
|
|
|
|
### Plugin Configuration
|
|
|
|
```javascript
|
|
// react-components mode plugins
|
|
plugins = [
|
|
vue({ ...vueOptions, customElement: true }), // Enable CE mode
|
|
react() // React JSX transform
|
|
];
|
|
|
|
// Rollup externals - React is peer dependency
|
|
rollupOptions = {
|
|
external: ['react', 'react-dom'],
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Component Hierarchy
|
|
|
|
### React Components (`src/components/`)
|
|
|
|
#### ChatwootProvider.jsx
|
|
**Purpose**: Root provider that initializes the entire Chatwoot environment.
|
|
|
|
**Key Responsibilities**:
|
|
1. Validates required props (`baseURL`, `userToken`)
|
|
2. Registers Vue Web Components via `registerVueWebComponents()`
|
|
3. Sets up global window variables for Vue components
|
|
4. Initializes axios with authentication
|
|
5. Dispatches `setUser` to hydrate auth state
|
|
6. Initializes ActionCable for real-time updates
|
|
|
|
**Props**:
|
|
```typescript
|
|
interface ChatwootProviderProps {
|
|
baseURL: string; // Required: Chatwoot API URL
|
|
userToken: string; // Required: Agent access token
|
|
accountId: number; // Account ID
|
|
conversationId: number; // Conversation to display
|
|
websocketURL?: string; // WebSocket server URL
|
|
pubsubToken?: string; // PubSub authentication token
|
|
disableUpload?: boolean; // Disable file attachments
|
|
disableEditor?: boolean; // Disable reply editor
|
|
disableSignature?: boolean; // Disable signature controls and insertion
|
|
signature?: string; // Custom message signature (overrides user profile signature)
|
|
}
|
|
```
|
|
|
|
**Global Variables Set**:
|
|
```javascript
|
|
window.__WOOT_API_HOST__ // API base URL
|
|
window.__WOOT_ACCOUNT_ID__ // Account ID
|
|
window.__WOOT_ACCESS_TOKEN__ // Auth token (used by axios)
|
|
window.__WEBSOCKET_URL__ // WebSocket URL
|
|
window.__PUBSUB_TOKEN__ // Real-time auth token
|
|
window.__WOOT_CONVERSATION_ID__ // Active conversation
|
|
window.__EDITOR_DISABLE_UPLOAD__ // Upload flag
|
|
window.__DISABLE_EDITOR__ // Editor flag
|
|
window.__WOOT_DISABLE_SIGNATURE__ // Disable signature controls and insertion
|
|
window.__WOOT_CUSTOM_SIGNATURE__ // Custom signature (overrides profile)
|
|
window.__WOOT_ISOLATED_SHELL__ // Disables audio notifications
|
|
window.__CHATWOOT_STORE__ // Vuex store reference
|
|
window.WootConstants // Global constants
|
|
window.axios // Configured axios instance
|
|
```
|
|
|
|
#### ChatwootConversation.jsx
|
|
**Purpose**: High-level wrapper providing styled container.
|
|
|
|
**Pattern**: Uses `useChatwoot()` hook to enforce Provider context.
|
|
|
|
#### ChatwootMessageListWrapper.jsx
|
|
**Purpose**: Bridge between React and Web Component.
|
|
|
|
**Key Pattern - Imperative Handle Updates**:
|
|
```jsx
|
|
// React props → Web Component properties
|
|
useEffect(() => {
|
|
element.conversationId = conversationId;
|
|
}, [conversationId]);
|
|
```
|
|
|
|
**Event Forwarding**:
|
|
```jsx
|
|
element.addEventListener('chatwoot:loaded', handleLoad);
|
|
element.addEventListener('chatwoot:error', handleError);
|
|
```
|
|
|
|
### Web Component Layer (`src/vue-components/`)
|
|
|
|
#### registerWebComponents.js
|
|
**Purpose**: Converts Vue SFCs to Web Components and registers them.
|
|
|
|
**Key Pattern - Style Injection**:
|
|
```javascript
|
|
const ceOptions = {
|
|
configureApp(app) {
|
|
app.use(store);
|
|
app.use(VueDOMPurifyHTML, domPurifyConfig);
|
|
app.use(i18n);
|
|
app.provide(I18nInjectionKey, i18n);
|
|
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
|
|
},
|
|
// Styles bundled into Shadow DOM
|
|
styles: [chatwootStyles, multiselectStyles, floatingVueStyles, uploadStyles],
|
|
};
|
|
|
|
const ChatwootMessageListElement = defineCustomElement(
|
|
ChatwootMessageListWebComponent,
|
|
ceOptions
|
|
);
|
|
```
|
|
|
|
**Important**: Styles are imported with `?inline` query to get CSS as string:
|
|
```javascript
|
|
import chatwootStyles from '../../../dashboard/assets/scss/app.scss?inline';
|
|
```
|
|
|
|
#### ChatwootMessageListWebComponent.vue
|
|
**Purpose**: Thin wrapper that renders the actual MessageList.
|
|
|
|
**Pattern - Additional Styles for Third-Party Components**:
|
|
```vue
|
|
<style>
|
|
/* vue-upload-component requires these styles */
|
|
.file-uploads { /* ... */ }
|
|
</style>
|
|
```
|
|
|
|
### Vue Layer (`/app/javascript/ui/`)
|
|
|
|
#### MessageList.vue
|
|
**Purpose**: Core conversation view with infinite scroll.
|
|
|
|
**Key Features**:
|
|
1. **Infinite Scroll**: Uses `@vueuse/core`'s `useInfiniteScroll`
|
|
2. **Typing Indicators**: Real-time via `conversationTypingStatus` store module
|
|
3. **Message Rendering**: Uses `components-next/message/Message.vue`
|
|
4. **Reply Box**: Integrated `LiteReplyBox.vue`
|
|
|
|
**Data Flow**:
|
|
```javascript
|
|
// Gets conversation ID from global
|
|
const conversationId = computed(() => window.__WOOT_CONVERSATION_ID__);
|
|
|
|
// Fetches conversation data on mount
|
|
onMounted(async () => {
|
|
await store.dispatch('inboxes/get');
|
|
await Promise.all([
|
|
store.dispatch('getConversation', conversationId.value),
|
|
store.dispatch('fetchAllAttachments', conversationId.value),
|
|
]);
|
|
});
|
|
```
|
|
|
|
**Key Pattern - Store Access Without Vue Instance**:
|
|
```javascript
|
|
// From composables/store.js
|
|
export const useStore = () => {
|
|
if (window.__CHATWOOT_STORE__) {
|
|
return window.__CHATWOOT_STORE__;
|
|
}
|
|
// Fallback to Vue instance
|
|
const vm = getCurrentInstance();
|
|
return vm.proxy.$store;
|
|
};
|
|
```
|
|
|
|
#### LiteReplyBox.vue
|
|
**Purpose**: Message composition editor.
|
|
|
|
**Key Features**:
|
|
- Rich text editing via ProseMirror (`WootMessageEditor`)
|
|
- File attachments with preview
|
|
- Typing indicators (on/off status)
|
|
- Keyboard shortcuts (Cmd/Ctrl+Enter to send)
|
|
- Private notes support
|
|
|
|
**Conditional Features via Globals**:
|
|
```javascript
|
|
isEditorDisabled() {
|
|
return window.__DISABLE_EDITOR__;
|
|
},
|
|
allowFileUpload() {
|
|
return window.__EDITOR_DISABLE_UPLOAD__ !== true;
|
|
},
|
|
```
|
|
|
|
#### axios.js (UI-specific axios factory)
|
|
**Purpose**: Creates axios instance configured for external use.
|
|
|
|
**Pattern - Token from Global**:
|
|
```javascript
|
|
export default axios => {
|
|
const apiHost = window.__WOOT_API_HOST__;
|
|
const accessToken = window.__WOOT_ACCESS_TOKEN__;
|
|
|
|
const wootApi = axios.create({ baseURL: `${apiHost}/` });
|
|
|
|
Object.assign(wootApi.defaults.headers.common, {
|
|
api_access_token: accessToken,
|
|
});
|
|
|
|
return wootApi;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Key Patterns
|
|
|
|
### 1. Global Variable Bridge
|
|
|
|
Vue components expect certain globals. The React provider sets these before rendering:
|
|
|
|
```javascript
|
|
// Set by ChatwootProvider
|
|
window.__WOOT_API_HOST__ = config.baseURL;
|
|
window.__WOOT_ACCOUNT_ID__ = config.accountId;
|
|
// ...etc
|
|
|
|
// Consumed by Vue components
|
|
const accountIdFromRoute = window.__WOOT_ACCOUNT_ID__;
|
|
```
|
|
|
|
### 2. Vuex Store as Global Singleton
|
|
|
|
The store is created once and shared via `window.__CHATWOOT_STORE__`:
|
|
|
|
```javascript
|
|
// In ChatwootProvider
|
|
import store from '../../../dashboard/store';
|
|
window.__CHATWOOT_STORE__ = store;
|
|
|
|
// In Vue composables
|
|
export const useStore = () => {
|
|
if (window.__CHATWOOT_STORE__) {
|
|
return window.__CHATWOOT_STORE__;
|
|
}
|
|
// ...
|
|
};
|
|
```
|
|
|
|
### 3. I18n in Web Components
|
|
|
|
Vue I18n requires special injection for Web Components:
|
|
|
|
```javascript
|
|
// Must provide I18nInjectionKey for useI18n() to work in CE mode
|
|
app.use(i18n);
|
|
app.provide(I18nInjectionKey, i18n);
|
|
```
|
|
|
|
Reference: https://vue-i18n.intlify.dev/guide/advanced/wc
|
|
|
|
### 4. Isolated Shell Mode
|
|
|
|
The `__WOOT_ISOLATED_SHELL__` flag disables features not needed in embedded mode:
|
|
|
|
```javascript
|
|
// In actionCable.js
|
|
if (!window.__WOOT_ISOLATED_SHELL__) {
|
|
DashboardAudioNotificationHelper.onNewMessage(data);
|
|
}
|
|
```
|
|
|
|
### 5. CamelCase Transformation
|
|
|
|
API returns snake_case, Vue components expect camelCase:
|
|
|
|
```javascript
|
|
import { useCamelCase } from '../dashboard/composables/useTransformKeys';
|
|
|
|
const allMessages = computed(() => {
|
|
return useCamelCase(conversation.value.messages, { deep: true }).reverse();
|
|
});
|
|
```
|
|
|
|
### 6. Web Component Event Communication
|
|
|
|
Web Components emit custom events that React can listen to:
|
|
|
|
```javascript
|
|
// In Web Component
|
|
this.$emit('chatwoot:loaded');
|
|
this.$emit('chatwoot:error', { message: 'Error details' });
|
|
|
|
// In React wrapper
|
|
element.addEventListener('chatwoot:loaded', handleLoad);
|
|
element.addEventListener('chatwoot:error', handleError);
|
|
```
|
|
|
|
### 7. CSS Variable Theming for Bubbles
|
|
|
|
Message bubble styles use CSS custom properties that can be overridden from outside the Shadow DOM:
|
|
|
|
```javascript
|
|
// In Base.vue - uses CSS variables for colors
|
|
const varaintBaseMap = {
|
|
[MESSAGE_VARIANTS.AGENT]: 'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))]',
|
|
[MESSAGE_VARIANTS.USER]: 'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))]',
|
|
// ...
|
|
};
|
|
|
|
// Border radius also uses variables
|
|
const orientationMap = {
|
|
[ORIENTATION.LEFT]: 'rounded-[var(--bubble-radius)] ltr:rounded-bl-[var(--bubble-radius-sm)]...',
|
|
// ...
|
|
};
|
|
```
|
|
|
|
Variables are defined in `_next-colors.scss` and can be overridden via `bubble-overrides.css`.
|
|
|
|
---
|
|
|
|
## Runtime Theme Customization
|
|
|
|
The Web Component supports runtime theming via CSS custom properties. These variables **inherit through the Shadow DOM boundary**, allowing consumers to customize bubble appearance without modifying the source.
|
|
|
|
### Available CSS Variables
|
|
|
|
#### Colors (RGB values without commas)
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `--chatwoot-bubble-agent-bg` | Agent message background | `--solid-blue` |
|
|
| `--chatwoot-bubble-agent-text` | Agent message text | `--slate-12` |
|
|
| `--chatwoot-bubble-user-bg` | User/customer message background | `--slate-4` |
|
|
| `--chatwoot-bubble-user-text` | User message text | `--slate-12` |
|
|
| `--chatwoot-bubble-private-bg` | Private note background | `--solid-amber` |
|
|
| `--chatwoot-bubble-private-text` | Private note text | `--amber-12` |
|
|
| `--chatwoot-bubble-bot-bg` | Bot/template message background | `--solid-iris` |
|
|
| `--chatwoot-bubble-bot-text` | Bot message text | `--slate-12` |
|
|
| `--chatwoot-bubble-error-bg` | Error message background | `--ruby-4` |
|
|
| `--chatwoot-bubble-error-text` | Error message text | `--ruby-12` |
|
|
| `--chatwoot-bubble-unsupported-bg` | Unsupported message background | `--amber-4` |
|
|
| `--chatwoot-bubble-unsupported-text` | Unsupported message text | `--amber-12` |
|
|
|
|
#### Meta (timestamp) - Per Variant
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `--chatwoot-bubble-agent-meta` | Meta text for agent messages | `--slate-11` |
|
|
| `--chatwoot-bubble-user-meta` | Meta text for user messages | `--slate-11` |
|
|
| `--chatwoot-bubble-private-meta` | Meta text for private notes (50% opacity applied) | `--amber-12` |
|
|
| `--chatwoot-bubble-bot-meta` | Meta text for bot messages | `--slate-11` |
|
|
| `--chatwoot-bubble-error-meta` | Meta text for error messages | `--ruby-11` |
|
|
|
|
#### Message Status Icons - Per Variant
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `--chatwoot-bubble-agent-status` | Agent: sent/delivered/progress color | `--slate-10` |
|
|
| `--chatwoot-bubble-agent-status-read` | Agent: read receipt color | `126 182 255` |
|
|
| `--chatwoot-bubble-user-status` | User: sent/delivered/progress color | `--slate-10` |
|
|
| `--chatwoot-bubble-user-status-read` | User: read receipt color | `126 182 255` |
|
|
| `--chatwoot-bubble-private-status` | Private: sent/delivered/progress color | `--amber-11` |
|
|
| `--chatwoot-bubble-private-status-read` | Private: read receipt color | `126 182 255` |
|
|
| `--chatwoot-bubble-bot-status` | Bot: sent/delivered/progress color | `--slate-10` |
|
|
| `--chatwoot-bubble-bot-status-read` | Bot: read receipt color | `126 182 255` |
|
|
|
|
#### Spacing
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `--chatwoot-bubble-spacing-ratio` | Scale factor for border radius | `1` |
|
|
|
|
#### Border
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `--chatwoot-bubble-border-width` | Border width (0 = invisible) | `0` |
|
|
| `--chatwoot-bubble-agent-border` | Agent message border color | `--blue-6` |
|
|
| `--chatwoot-bubble-user-border` | User message border color | `--slate-6` |
|
|
| `--chatwoot-bubble-private-border` | Private note border color | `--amber-6` |
|
|
| `--chatwoot-bubble-bot-border` | Bot message border color | `--iris-6` |
|
|
| `--chatwoot-bubble-error-border` | Error message border color | `--ruby-6` |
|
|
| `--chatwoot-bubble-unsupported-border` | Unsupported message border color | `--amber-12` |
|
|
|
|
### Usage Examples
|
|
|
|
#### Option 1: Global CSS
|
|
|
|
```css
|
|
/* In your app's CSS file */
|
|
:root {
|
|
--chatwoot-bubble-agent-bg: 59 130 246; /* Blue */
|
|
--chatwoot-bubble-user-bg: 243 244 246; /* Light gray */
|
|
--chatwoot-bubble-spacing-ratio: 1.5; /* 50% more rounded */
|
|
--chatwoot-bubble-border-width: 1px; /* Add borders */
|
|
--chatwoot-bubble-agent-border: 59 130 246; /* Blue border */
|
|
}
|
|
```
|
|
|
|
#### Option 2: Wrapper with Inline Styles
|
|
|
|
```jsx
|
|
function App() {
|
|
return (
|
|
<div
|
|
style={{
|
|
'--chatwoot-bubble-agent-bg': '59 130 246',
|
|
'--chatwoot-bubble-user-bg': '243 244 246',
|
|
'--chatwoot-bubble-spacing-ratio': '1.5',
|
|
'--chatwoot-bubble-border-width': '1px',
|
|
'--chatwoot-bubble-agent-border': '59 130 246',
|
|
}}
|
|
>
|
|
<ChatwootProvider {...props}>
|
|
<ChatwootConversation />
|
|
</ChatwootProvider>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Option 3: Dynamic Theming
|
|
|
|
```jsx
|
|
function App() {
|
|
const [theme, setTheme] = useState({
|
|
'--chatwoot-bubble-agent-bg': '59 130 246',
|
|
'--chatwoot-bubble-border-width': '1px',
|
|
'--chatwoot-bubble-agent-border': '59 130 246',
|
|
});
|
|
|
|
return (
|
|
<div style={theme}>
|
|
<button onClick={() => setTheme({
|
|
'--chatwoot-bubble-agent-bg': '34 197 94', // Switch to green
|
|
'--chatwoot-bubble-border-width': '2px',
|
|
'--chatwoot-bubble-agent-border': '34 197 94', // Green border
|
|
})}>
|
|
Change Theme
|
|
</button>
|
|
<ChatwootProvider {...props}>
|
|
<ChatwootConversation />
|
|
</ChatwootProvider>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Option 4: Borders Only
|
|
|
|
```jsx
|
|
function App() {
|
|
return (
|
|
<div
|
|
style={{
|
|
'--chatwoot-bubble-border-width': '2px',
|
|
'--chatwoot-bubble-agent-border': '59 130 246', // Blue border
|
|
'--chatwoot-bubble-user-border': '156 163 175', // Gray border
|
|
'--chatwoot-bubble-private-border': '251 191 36', // Amber border
|
|
'--chatwoot-bubble-bot-border': '129 140 248', // Iris border
|
|
}}
|
|
>
|
|
<ChatwootProvider {...props}>
|
|
<ChatwootConversation />
|
|
</ChatwootProvider>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### How It Works
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Consumer's CSS │
|
|
│ :root { │
|
|
│ --chatwoot-bubble-agent-bg: 59 130 246; │
|
|
│ --chatwoot-bubble-border-width: 1px; │
|
|
│ } │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼ (CSS inheritance)
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Shadow DOM (:host) │
|
|
│ bubble-overrides.css: │
|
|
│ --bubble-agent-bg: var(--chatwoot-bubble-agent-bg, │
|
|
│ var(--solid-blue)); │
|
|
│ --bubble-border-width: var(--chatwoot-bubble-border-width, │
|
|
│ 0); │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼ (used by components)
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Base.vue │
|
|
│ class="bg-[rgb(var(--bubble-agent-bg))] │
|
|
│ border-[length:var(--bubble-border-width)] │
|
|
│ border-[rgb(var(--bubble-agent-border))]" │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Key Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `src/styles/bubble-overrides.css` | Defines `:host` overrides that bridge external variables |
|
|
| `dashboard/assets/scss/_next-colors.scss` | Default theme values |
|
|
| `dashboard/components-next/message/bubbles/Base.vue` | Consumes CSS variables |
|
|
|
|
---
|
|
|
|
## Real-time Communication
|
|
|
|
### ActionCable Setup
|
|
|
|
```javascript
|
|
// BaseActionCableConnector.js
|
|
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
|
|
this.consumer = createConsumer(websocketURL);
|
|
this.subscription = this.consumer.subscriptions.create({
|
|
channel: 'RoomChannel',
|
|
pubsub_token: pubsubToken,
|
|
account_id: app.$store.getters.getCurrentAccountId,
|
|
user_id: app.$store.getters.getCurrentUserID,
|
|
});
|
|
```
|
|
|
|
### Events Handled
|
|
|
|
| Event | Action |
|
|
|-------|--------|
|
|
| `message.created` | Add message to conversation |
|
|
| `message.updated` | Update existing message |
|
|
| `conversation.typing_on` | Show typing indicator |
|
|
| `conversation.typing_off` | Hide typing indicator |
|
|
| `presence.update` | Update online status |
|
|
| `conversation.updated` | Refresh conversation data |
|
|
|
|
### Presence Updates
|
|
|
|
Heartbeat every 20 seconds:
|
|
|
|
```javascript
|
|
const PRESENCE_INTERVAL = 20000;
|
|
this.triggerPresenceInterval = () => {
|
|
setTimeout(() => {
|
|
this.subscription.updatePresence();
|
|
this.triggerPresenceInterval();
|
|
}, PRESENCE_INTERVAL);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
```
|
|
app/javascript/
|
|
├── react-components/
|
|
│ ├── src/
|
|
│ │ ├── index.jsx # Package exports
|
|
│ │ ├── components/
|
|
│ │ │ ├── ChatwootProvider.jsx # Root provider
|
|
│ │ │ ├── ChatwootConversation.jsx # Container component
|
|
│ │ │ └── ChatwootMessageListWrapper.jsx # WC bridge
|
|
│ │ ├── vue-components/
|
|
│ │ │ ├── registerWebComponents.js # WC registration
|
|
│ │ │ └── ChatwootMessageListWebComponent.vue
|
|
│ │ └── styles/
|
|
│ │ ├── upload.css # File upload styles
|
|
│ │ └── bubble-overrides.css # Runtime theme customization
|
|
│ └── doc.md # This file
|
|
│
|
|
├── ui/
|
|
│ ├── MessageList.vue # Core message list
|
|
│ ├── LiteReplyBox.vue # Reply editor
|
|
│ ├── axios.js # Axios factory
|
|
│ └── usage.md # Usage docs
|
|
│
|
|
├── dashboard/
|
|
│ ├── store/ # Vuex store
|
|
│ │ ├── index.js
|
|
│ │ └── modules/
|
|
│ │ ├── conversations/ # Conversation state
|
|
│ │ ├── auth.js # Auth state
|
|
│ │ └── conversationTypingStatus.js
|
|
│ ├── components-next/
|
|
│ │ └── message/ # Message components
|
|
│ │ ├── Message.vue
|
|
│ │ ├── TypingIndicator.vue
|
|
│ │ └── bubbles/
|
|
│ ├── composables/
|
|
│ │ ├── store.js # Store helpers
|
|
│ │ └── useTransformKeys.js # Case conversion
|
|
│ ├── helper/
|
|
│ │ ├── actionCable.js # WebSocket connector
|
|
│ │ └── commons.js # Utility functions
|
|
│ └── api/
|
|
│ ├── ApiClient.js # Base API client
|
|
│ └── auth.js # Auth API
|
|
│
|
|
└── shared/
|
|
└── helpers/
|
|
└── BaseActionCableConnector.js # WebSocket base class
|
|
|
|
scripts/
|
|
└── package-react-components.js # NPM packaging script
|
|
|
|
vite.config.ts # Multi-mode build config
|
|
```
|
|
|
|
---
|
|
|
|
## Extension Guide
|
|
|
|
### Adding a New Web Component
|
|
|
|
1. **Create Vue Component** (`vue-components/NewComponent.vue`):
|
|
```vue
|
|
<script setup>
|
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
|
// Use composables that check window.__CHATWOOT_STORE__
|
|
</script>
|
|
```
|
|
|
|
2. **Register Web Component** (`registerWebComponents.js`):
|
|
```javascript
|
|
import NewComponent from './NewComponent.vue';
|
|
|
|
const NewElement = defineCustomElement(NewComponent, ceOptions);
|
|
|
|
export const registerVueWebComponents = () => {
|
|
// ...existing registrations
|
|
if (!customElements.get('chatwoot-new-component')) {
|
|
customElements.define('chatwoot-new-component', NewElement);
|
|
}
|
|
};
|
|
```
|
|
|
|
3. **Create React Wrapper** (`components/NewComponentWrapper.jsx`):
|
|
```jsx
|
|
export const NewComponentWrapper = (props) => {
|
|
const elementRef = useRef();
|
|
|
|
useEffect(() => {
|
|
// Sync React props to WC properties
|
|
}, [props]);
|
|
|
|
return <chatwoot-new-component ref={elementRef} />;
|
|
};
|
|
```
|
|
|
|
4. **Export from index.jsx**:
|
|
```javascript
|
|
export { NewComponentWrapper } from './components/NewComponentWrapper';
|
|
```
|
|
|
|
### Adding New Global Configuration
|
|
|
|
1. Add prop to `ChatwootProvider.jsx`
|
|
2. Set corresponding `window.__WOOT_*` variable
|
|
3. Access in Vue components via `window.__WOOT_*`
|
|
|
|
### Adding Store Modules
|
|
|
|
If the new component needs additional Vuex state:
|
|
|
|
1. Ensure module is already in `dashboard/store/index.js`
|
|
2. Access via `useStore().dispatch('module/action')` or `useMapGetter('module/getter')`
|
|
|
|
---
|
|
|
|
## Known Limitations
|
|
|
|
### 1. Single Conversation Per Page
|
|
Currently, only one conversation can be displayed at a time due to global state (`__WOOT_CONVERSATION_ID__`).
|
|
|
|
**Workaround**: For multiple conversations, instantiate separate iframes.
|
|
|
|
### 2. Style Isolation
|
|
Shadow DOM prevents parent CSS from affecting components, but also means:
|
|
- Global CSS resets don't apply
|
|
- Most parent styles don't cascade
|
|
|
|
**Solution**: Message bubble colors and border radius can be customized via CSS custom properties (see [Runtime Theme Customization](#runtime-theme-customization)). CSS custom properties DO inherit through Shadow DOM boundaries.
|
|
|
|
### 3. React DevTools
|
|
Web Components don't appear in React DevTools component tree. Use browser's native Custom Elements inspector.
|
|
|
|
### 4. Hot Module Replacement
|
|
Vue components inside Web Components don't support HMR well. Full page reload often needed during development.
|
|
|
|
### 5. Bundle Size
|
|
The react-components bundle includes:
|
|
- Full Vuex store
|
|
- All SCSS styles
|
|
- I18n messages
|
|
|
|
**Future optimization**: Tree-shake unused store modules.
|
|
|
|
### 6. Single Language (English Only)
|
|
Currently hardcoded to English locale:
|
|
```javascript
|
|
const i18n = createI18n({
|
|
locale: 'en',
|
|
messages: { en },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging Tips
|
|
|
|
### Inspect Vuex Store
|
|
```javascript
|
|
// In browser console
|
|
window.__CHATWOOT_STORE__.state
|
|
window.__CHATWOOT_STORE__.getters.getConversationById(123)
|
|
```
|
|
|
|
### Check WebSocket Connection
|
|
```javascript
|
|
// ActionCable consumer status
|
|
window.__CHATWOOT_STORE__._modules.root._rawModule // Check if actions dispatch
|
|
```
|
|
|
|
### Verify Web Component Registration
|
|
```javascript
|
|
customElements.get('chatwoot-message-list') // Should return constructor
|
|
```
|
|
|
|
### Force Re-render
|
|
```javascript
|
|
// Dispatch any action to trigger reactivity
|
|
window.__CHATWOOT_STORE__.dispatch('setActiveChat', { data: { id: conversationId } });
|
|
```
|
|
|
|
---
|
|
|
|
## Related Files
|
|
|
|
- `CLAUDE.md` - General Chatwoot development guidelines
|
|
- `app/javascript/ui/usage.md` - UI components usage guide
|
|
- `vite.config.ts` - Build configuration documentation (inline comments)
|