Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
66a2069
Upgrade JSX and storybook compilation
Fabilin Feb 20, 2024
52a2b12
Remove now unused React imports
Fabilin Feb 21, 2024
b910b9c
resolves #159: add custom renderer config for images
Fabilin Feb 21, 2024
2ee76a8
Add useMetadata hook for renderers inside messages
Fabilin Feb 21, 2024
23b13a7
resolves #158: add custom renderer config for text
Fabilin Feb 22, 2024
504cd3a
Update dependencies
Fabilin Feb 23, 2024
750d179
Reformat source code
Fabilin Feb 23, 2024
fcbaf81
Add imageContainer override, clean up exports
Fabilin Feb 23, 2024
2996c90
Fix a package resolution issue with yarn classic
Fabilin Feb 23, 2024
f89016a
Fix typing in local storage utils
Fabilin Feb 23, 2024
94d65ff
Make pre-commit hook check build validity
Fabilin Feb 23, 2024
a4ce455
Revert image zoom-in feature
Fabilin Feb 27, 2024
ca3ce4a
Fix button tabindex
Fabilin Feb 28, 2024
ae23384
Fix build
Fabilin Feb 28, 2024
f91261b
Reduce number of text renderers
Fabilin Feb 28, 2024
0255a60
Remove out of scope CSS overrides
Fabilin Feb 28, 2024
a91e30e
Pass common HTML attributes to image renderers
Fabilin Feb 29, 2024
6794a0e
Add back user-specific text renderer
Fabilin Feb 29, 2024
965f1d8
Update README
Fabilin Feb 29, 2024
3f69c9d
move renderers to their own components
Fabilin Mar 1, 2024
29848aa
Merge branch 'master' into 159-158-custom-renderers
Fabilin Mar 1, 2024
e414aa6
Merge branch 'master' into 159-158-custom-renderers
Fabilin Mar 1, 2024
e4b9980
Fix postback buttons' onClick
Fabilin Mar 1, 2024
a5dbb98
Update readme
Fabilin Mar 5, 2024
c716dcb
Optimize button dynamic styles
Fabilin Mar 5, 2024
428a3a7
Improve typing of ImageRendererProps
Fabilin Mar 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .babelrc.json

This file was deleted.

8 changes: 8 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ module.exports = {

rules: {
'@emotion/pkg-renaming': 'error',
'@emotion/no-vanilla': 'error',
'@emotion/import-from-emotion': 'error',
'@emotion/styled-import': 'error',
// Modern JSX transform does not require explicit React import
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
// Ignore the css property added to React components by Emotion
'react/no-unknown-property': ['error', { ignore: ['css'] }],
'@typescript-eslint/ban-types': [
'error',
{
Expand Down
8 changes: 7 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ const config: StorybookConfig = {

framework: {
name: '@storybook/react-webpack5',
options: {},
options: {
strictMode: true,
fastRefresh: true,
builder: {
useSWC: true,
},
},
},

docs: {
Expand Down
10 changes: 10 additions & 0 deletions .swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"jsc": {
"transform": {
"react": {
"runtime": "automatic",
"importSource": "@emotion/react"
}
}
}
}
98 changes: 79 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,25 @@ renderChat(document.getElementById('chat'), '<TOCK_BOT_API_URL>', 'referralParam

## Customize interface

If the chat does not suit your needs you can also use the components separately.
If the chat does not suit your needs, there are two main ways to customize the interface rendering.

### Configure custom renderers

Custom rendering can currently be defined for text and images. Here are some examples of what this enables:

- processing custom markup in the text of any component
- stripping harmful HTML tags and attributes when the backend is untrustworthy
- dynamically decorating text messages
- automatically embedding SVG images into the DOM
- implementing fallback behavior when an image fails to load
- using [metadata](#message-metadata) sent by the server to set image properties like width and height

See the [`TockSettings`](#renderersettings) API reference for the details of available renderers.

### Use the chat components separately

The `tock-react-kit` exports its main components, so you can re-use them to build your own chat interface.
This approach can be used alongside custom renderers to control more granular aspects of rendering.

## Message Metadata

Expand All @@ -180,44 +198,45 @@ ensure data stays available if required.

The metadata from each response is also attached to the corresponding messages.
This metadata is persisted with the messages, including through page reloads if [local storage history](#local-storage-history) is enabled.
At the current time, it is only available to custom React-based frontends that handle message rendering themselves.
It is available to [custom renderers](#configure-custom-renderers) through the use of the `useMessageMetadata` hook,
as well as to custom React-based frontends that handle message rendering themselves.

## API Reference

### `renderChat(element, tockBotApiUrl, referralParameter, theme, options)`

Renders an entire chat in a target element.

| Argument name | Type | Description |
| ------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------- |
| `element` | [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element) | Target element where the chat will be rendered |
| `tockBotApiUrl` | `string` | URL to the Tock Bot REST API |
| `referralParameter` | `string` | Optional referal parameter |
| `theme` | `TockTheme` | Optional theme object |
| `options` | `TockOptions` | Optional options object |
| Argument name | Type | Description |
|---------------------|-----------------------------------------------------------------------|------------------------------------------------|
| `element` | [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element) | Target element where the chat will be rendered |
| `tockBotApiUrl` | `string` | URL to the Tock Bot REST API |
| `referralParameter` | `string` | Optional referal parameter |
| `theme` | `TockTheme` | Optional theme object |
| `options` | `TockOptions` | Optional options object |

### `useTock(tockBotApiUrl, extraHeadersProvider, disableSse, localStorageHistory)`

Hook that provides chat history and methods to communicate with the Tock Bot. It must be used alongside with `TockContext`. Returns a useTock interface.

| Argument name | Type | Description |
| ---------------------- | --------------------------------------- | ------------------------------------------------------------- |
|------------------------|-----------------------------------------|---------------------------------------------------------------|
| `tockBotApiUrl` | `string` | URL to the Tock Bot REST API |
| `extraHeadersProvider` | `() => Promise<Record<string, string>>` | Optional Provider of extra HTTP headers for outgoing requests |
| `disableSse` | `boolean` | Optional Force-disabling of SSE mode |
| `localStorageHistory` | `TockLocalStorage` | Optional Configuration for LocalStorage history |
| `disableSse` | `boolean` | Optional Force-disabling of SSE mode |
| `localStorageHistory` | `TockLocalStorage` | Optional Configuration for LocalStorage history |

### `TockTheme`

A `TockTheme` can be used as a value of a `ThemeProvider` of [`emotion-theming`](https://emotion.sh/docs/theming) (bundled with the library) or as a third argument of `renderChat`.

| Property name | Type | Description |
|---------------------|-------------------|-----------------------------------------------------------|
| `palette` | `Palette` | Object for customising colors (see below) |
| `sizing` | `Sizing` | Object for customising elements sizing (see below) |
| `typography` | `Typography` | Object for customising typographies (see below) |
| `overrides` | `Overrides?` | Object allowing further styling (see below) |
| `inlineQuickReplies`| `boolean?` | Displaying quick replies inline (by default false) |
| Property name | Type | Description |
|----------------------|--------------|----------------------------------------------------|
| `palette` | `Palette` | Object for customising colors (see below) |
| `sizing` | `Sizing` | Object for customising elements sizing (see below) |
| `typography` | `Typography` | Object for customising typographies (see below) |
| `overrides` | `Overrides?` | Object allowing further styling (see below) |
| `inlineQuickReplies` | `boolean?` | Displaying quick replies inline (by default false) |

#### `Palette`

Expand Down Expand Up @@ -317,13 +336,54 @@ A `TockTheme` can be used as a value of a `ThemeProvider` of [`emotion-theming`]
|----------------|-------------------------|------------------------------------------------------|
| `locale` | `string?` | Optional user language, as an *RFC 5646* code |
| `localStorage` | `LocalStorageSettings?` | Configuration for use of localStorage by the library |
| `renderers` | `RendererSettings?` | Configuration for custom image and text renderers |

#### `LocalStorageSettings`

| Property name | Type | Description |
|-----------------|-----------|-----------------------------------------------------------------------------------------------|
| `storagePrefix` | `string?` | Prefix for local storage keys allowing communication with different bots from the same domain |

#### `RendererSettings`

| Property name | Type | Description |
|------------------|--------------------------|-------------------------------------------------------------------------------|
| `imageRenderers` | `ImageRendererSettings?` | Configuration of renderers for dynamic images displayed in the chat interface |
| `textRenderers` | `TextRendererSettings?` | Configuration of renderers for dynamic text displayed in the chat interface |

#### `ImageRendererSettings`

Image renderers all implement the `ImageRenderer` interface.
They are tasked with rendering a graphical component using a source URL, a description, a class name, and other generic HTML attributes.
The passed in class name provides the default style for the rendered component, as well as applicable [overrides](#overrides).

| Property name | Description |
|---------------|---------------------------------------------------------------------------------------------------|
| `default` | The fallback renderer. By default, renders a single `img` component using the provided properties |
| `standalone` | Renders images in the dedicated image component, including the zoomed-in view |
| `card` | Renders images in the card component (including in carousels) |
| `buttonIcon` | Renders icons in quick replies, URL buttons, and postback buttons |

#### `TextRendererSettings`

Text renderers all implement the `TextRenderer` interface.
They are tasked with rendering a string into a text component.

A renderer can be restricted in the kind of HTML nodes it emits depending on the context in which it is invoked.
Most text renderers should only emit [phrasing content](https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content)
that is also [non-interactive](https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#interactive_content).
However, some contexts allow interactive phrasing content, or even any [flow content](https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#flow_content).

Some renderers are expected to handle rich text, that is text that already contains HTML formatting.
Such rich text renderers may strip HTML tags or attributes that are deemed dangerous to add to the DOM.

| Property name | Type of content | Description |
|---------------|--------------------------|-----------------------------------------------------------------------------------------------------------|
| `default` | non-interactive phrasing | The fallback renderer. By default, renders the whole string as a single text node |
| `html` | flow | The fallback renderer for rich text. By default, renders the string into a `div` with `innerHTML` |
| `htmlPhrase` | phrasing | The fallback renderer for inline rich text. By default, renders the string into a `span` with `innerHTML` |
| `userContent` | phrasing | Renders text in user messages. If unspecified, falls back to `default` |

### `TockOptions`

Contains all the properties from [`TockSettings`](#tocksettings) as well as the following:
Expand Down
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"build/tock-react-kit.d.ts",
"build/tock-react-kit.esm.js",
"build/tock-react-kit.esm.js.map",
"build/tock-react-kit.umd.js",
"build/tock-react-kit-standalone.umd.js",
"build/tock-react-kit-standalone.esm.js"
],
Expand All @@ -33,16 +32,13 @@
},
"dependencies": {
"deepmerge": "^4.2.2",
"linkifyjs": "^2.1.9",
"linkify-html": "^3.0.5",
"linkifyjs": "^3.0.5",
"polished": "^3.6.5",
"react-feather": "^2.0.8",
"styled-tools": "^1.7.2"
},
"devDependencies": {
"@babel/core": "^7.22.15",
"@babel/preset-env": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.22.15",
"@emotion/eslint-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
Expand All @@ -65,7 +61,6 @@
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"auto-changelog": "^2.4.0",
"babel-loader": "^9.1.3",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
Expand Down
21 changes: 8 additions & 13 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,15 @@ import typescript from '@rollup/plugin-typescript';
export default [
{
input: 'src/index.ts',
external: ['react', 'react-dom', '@emotion/react', '@emotion/styled'],
external: [
'react',
'react/jsx-runtime',
'react-dom',
'@emotion/react',
'@emotion/react/jsx-runtime',
'@emotion/styled',
],
output: [
{
// TODO deprecated build output, remove in 24.x
file: 'build/tock-react-kit.umd.js',
format: 'umd',
name: 'TockReact',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'@emotion/styled': 'emotionStyled',
'@emotion/react': 'emotionReact',
},
},
{
file: 'build/tock-react-kit.esm.js',
format: 'esm',
Expand Down
12 changes: 12 additions & 0 deletions src/MessageMetadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Context, createContext, useContext } from 'react';

const MessageMetadataCtx: Context<Record<string, string>> = createContext({});

export const MessageMetadataContext = MessageMetadataCtx.Provider;

/**
* Returns the metadata associated with the surrounding message
*/
export const useMessageMetadata = (): Record<string, string> => {
return useContext(MessageMetadataCtx);
};
2 changes: 1 addition & 1 deletion src/PostInitContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface TockHistoryData {
readonly quickReplies: QuickReply[];
}

export interface PostInitContext extends UseLocalTools {
export default interface PostInitContext extends UseLocalTools {
/**
* The full chat history at the time the Chat component is initialized, which includes messages from local storage
* and/or from TockContext, or null if there is no chat history at all.
Expand Down
37 changes: 11 additions & 26 deletions src/TockContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {
import {
Context,
createContext,
Dispatch,
Expand All @@ -12,6 +12,7 @@ import { PartialDeep } from 'type-fest';
import { retrieveUserId } from './utils';
import { QuickReply } from './model/buttons';
import { Message } from './model/messages';
import TockSettings, { defaultSettings } from './settings/TockSettings';

export const TockSettingsContext: Context<TockSettings | undefined> =
createContext<TockSettings | undefined>(undefined);
Expand Down Expand Up @@ -47,15 +48,6 @@ export const useTockDispatch: () => Dispatch<TockAction> = () => {
return dispatch;
};

export interface LocalStorageSettings {
prefix?: string;
}

export interface TockSettings {
localStorage: LocalStorageSettings;
locale?: string;
}

export interface TockState {
quickReplies: QuickReply[];
messages: Message[];
Expand Down Expand Up @@ -139,10 +131,6 @@ const tockReducer: Reducer<TockState, TockAction> = (
return state;
};

const defaultSettings: TockSettings = {
localStorage: {},
};

const TockContext: (props: {
children?: ReactNode;
settings?: PartialDeep<TockSettings>;
Expand All @@ -153,18 +141,15 @@ const TockContext: (props: {
children?: ReactNode;
settings: PartialDeep<TockSettings>;
}) => {
const mergedSettings = deepmerge(defaultSettings, settings);
const [state, dispatch]: [TockState, Dispatch<TockAction>] = useReducer(
tockReducer,
{
quickReplies: [],
messages: [],
userId: retrieveUserId(mergedSettings.localStorage.prefix),
loading: false,
sseInitializing: false,
metadata: {},
},
);
const mergedSettings = deepmerge(defaultSettings, settings) as TockSettings;
const [state, dispatch] = useReducer(tockReducer, {
quickReplies: [],
messages: [],
userId: retrieveUserId(mergedSettings.localStorage.prefix),
loading: false,
sseInitializing: false,
metadata: {},
});
return (
<TockSettingsContext.Provider value={mergedSettings}>
<TockStateContext.Provider value={state}>
Expand Down
4 changes: 2 additions & 2 deletions src/TockOptions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PartialDeep } from 'type-fest';
import TockAccessibility from './TockAccessibility';
import TockLocalStorage from './TockLocalStorage';
import { TockSettings } from './TockContext';
import { PostInitContext } from './PostInitContext';
import TockSettings from './settings/TockSettings';
import PostInitContext from './PostInitContext';

export interface TockOptions extends PartialDeep<TockSettings> {
// a callback that will be executed once the chat is able to send and receive messages
Expand Down
Loading