Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cc02e83
wip
MartinSchoeler Oct 17, 2023
cb6f298
Merge remote-tracking branch 'origin/develop' into hooks-ts
MartinSchoeler Nov 9, 2023
55f7642
wip
MartinSchoeler Nov 9, 2023
8727742
Fix TS
MartinSchoeler Nov 10, 2023
dca38d5
Small Fixes
MartinSchoeler Nov 10, 2023
54b417a
Create orange-dragons-fly.md
MartinSchoeler Nov 10, 2023
d4e8f55
Merge remote-tracking branch 'origin/develop' into hooks-ts
MartinSchoeler Dec 21, 2023
f8a2c2b
Merge remote-tracking branch 'origin/develop' into hooks-ts
MartinSchoeler Dec 26, 2023
8a035e4
Merge branch 'develop' into hooks-ts
MartinSchoeler Jan 30, 2024
94fb80c
fix ts
MartinSchoeler Jan 30, 2024
bdab765
Merge branch 'hooks-ts' of https://github.com/RocketChat/Rocket.Chat …
MartinSchoeler Jan 30, 2024
4ceabb3
handle undefined
MartinSchoeler Jan 31, 2024
d6c2d1b
Merge branch 'develop' into hooks-ts
MartinSchoeler Jan 31, 2024
73d3f24
refactor: adjusted widget and hooks types
Feb 8, 2024
2aae9d1
chore: minor fixes
Feb 9, 2024
db0b6bd
chore: adjusted webpack config
Feb 10, 2024
cc6112f
fix: revert api method signature change
Feb 14, 2024
a1f6b9d
chore: added typing to the theme object
Feb 15, 2024
0617ce9
refactor: created ScreenContext and removed redundant connectors
Feb 15, 2024
573da9a
chore: adjusted setTheme method
Feb 16, 2024
82759a9
refactor: changed the way arguments are passed to parentCall
Feb 26, 2024
3eae6ea
fix: color priority being given to config instead of iframe
Feb 26, 2024
616afe1
Merge branch 'develop' into hooks-ts
Feb 26, 2024
654cb19
Apply suggestions from code review
ggazzo Feb 29, 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
6 changes: 6 additions & 0 deletions .changeset/orange-dragons-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/livechat": minor
---

chore: Refactor & Improve Livechat Widget API
Refactors and adds better error handling to the widget's API calls
5 changes: 4 additions & 1 deletion packages/livechat/.storybook/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { type DecoratorFunction } from '@storybook/csf';
import type { Args, PreactFramework } from '@storybook/preact';
import { loremIpsum as originalLoremIpsum } from 'lorem-ipsum';

import { ScreenContext } from '../src/components/Screen/ScreenProvider';
import gazzoAvatar from './assets/gazzo.jpg';
import martinAvatar from './assets/martin.jpg';
import tassoAvatar from './assets/tasso.jpg';

export const screenDecorator: DecoratorFunction<PreactFramework, Args> = (storyFn) => (
<div style={{ display: 'flex', width: 365, height: 500 }}>{storyFn()}</div>
<div style={{ display: 'flex', width: 365, height: 500 }}>
<ScreenContext.Provider value={screenProps()}>{storyFn()}</ScreenContext.Provider>
</div>
);

export const screenProps = () => ({
Expand Down
128 changes: 16 additions & 112 deletions packages/livechat/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import type { ILivechatTrigger } from '@rocket.chat/core-typings';
import i18next from 'i18next';
import { Component } from 'preact';
import Router, { route } from 'preact-router';
import { parse } from 'query-string';
import { withTranslation } from 'react-i18next';

import type { Department } from '../../definitions/departments';
import { setInitCookies } from '../../helpers/cookies';
import { isActiveSession } from '../../helpers/isActiveSession';
import { isRTL } from '../../helpers/isRTL';
import { visibility } from '../../helpers/visibility';
import history from '../../history';
Expand All @@ -25,7 +23,7 @@ import Register from '../../routes/Register';
import SwitchDepartment from '../../routes/SwitchDepartment';
import TriggerMessage from '../../routes/TriggerMessage';
import type { Dispatch } from '../../store';
import store from '../../store';
import { ScreenProvider } from '../Screen/ScreenProvider';

type AppProps = {
config: {
Expand Down Expand Up @@ -75,26 +73,6 @@ type AppState = {
poppedOut: boolean;
};

export type ScreenPropsType = {
notificationsEnabled: boolean;
minimized: boolean;
expanded: boolean;
windowed: boolean;
sound: unknown;
alerts: unknown;
modal: unknown;
nameDefault: string;
emailDefault: string;
departmentDefault: string;
onEnableNotifications: () => unknown;
onDisableNotifications: () => unknown;
onMinimize: () => unknown;
onRestore: () => unknown;
onOpenWindow: () => unknown;
onDismissAlert: () => unknown;
dismissNotification: () => void;
};

export class App extends Component<AppProps, AppState> {
state = {
initialized: false,
Expand Down Expand Up @@ -150,49 +128,6 @@ export class App extends Component<AppProps, AppState> {
Triggers.processTriggers();
}

protected handleEnableNotifications = () => {
const { dispatch, sound = {} } = this.props;
dispatch({ sound: { ...sound, enabled: true } });
};

protected handleDisableNotifications = () => {
const { dispatch, sound = {} } = this.props;
dispatch({ sound: { ...sound, enabled: false } });
};

protected handleMinimize = () => {
parentCall('minimizeWindow');
const { dispatch } = this.props;
dispatch({ minimized: true });
};

protected handleRestore = () => {
parentCall('restoreWindow');
const { dispatch, undocked } = this.props;
const dispatchRestore = () => dispatch({ minimized: false, undocked: false });
const dispatchEvent = () => {
dispatchRestore();
store.off('storageSynced', dispatchEvent);
};
if (undocked) {
store.on('storageSynced', dispatchEvent);
} else {
dispatchRestore();
}
Triggers.callbacks?.emit('chat-opened-by-visitor');
};

protected handleOpenWindow = () => {
parentCall('openPopout');
const { dispatch } = this.props;
dispatch({ undocked: true, minimized: false });
};

protected handleDismissAlert = (id: string) => {
const { dispatch, alerts = [] } = this.props;
dispatch({ alerts: alerts.filter((alert) => alert.id !== id) });
};

protected handleVisibilityChange = async () => {
const { dispatch } = this.props;
dispatch({ visible: !visibility.hidden });
Expand All @@ -202,19 +137,20 @@ export class App extends Component<AppProps, AppState> {
this.forceUpdate();
};

protected dismissNotification = () => !isActiveSession();

protected initWidget() {
const {
minimized,
iframe: { visible },
dispatch,
} = this.props;

parentCall(minimized ? 'minimizeWindow' : 'restoreWindow');
parentCall(visible ? 'showWidget' : 'hideWidget');

visibility.addListener(this.handleVisibilityChange);

this.handleVisibilityChange();

window.addEventListener('beforeunload', () => {
visibility.removeListener(this.handleVisibilityChange);
dispatch({ minimized: true, undocked: false });
Expand All @@ -223,16 +159,6 @@ export class App extends Component<AppProps, AppState> {
i18next.on('languageChanged', this.handleLanguageChange);
}

protected checkPoppedOutWindow() {
// Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose
const { dispatch } = this.props;
const poppedOut = parse(window.location.search).mode === 'popout';
this.setState({ poppedOut });
if (poppedOut) {
dispatch({ minimized: false });
}
}

protected async initialize() {
// TODO: split these behaviors into composable components
await Connection.init();
Expand All @@ -241,7 +167,6 @@ export class App extends Component<AppProps, AppState> {
Hooks.init();
this.handleTriggers();
this.initWidget();
this.checkPoppedOutWindow();
this.setState({ initialized: true });
parentCall('ready');
}
Expand All @@ -268,44 +193,23 @@ export class App extends Component<AppProps, AppState> {
}
}

render = ({ sound, undocked, minimized, expanded, alerts, modal, iframe }: AppProps, { initialized, poppedOut }: AppState) => {
render = (_: AppProps, { initialized }: AppState) => {
if (!initialized) {
return null;
}

const { department, name, email } = iframe.guest || {};

const screenProps = {
notificationsEnabled: sound?.enabled,
minimized: !poppedOut && (minimized || undocked),
expanded: !minimized && expanded,
windowed: !minimized && poppedOut,
sound,
alerts,
modal,
nameDefault: name,
emailDefault: email,
departmentDefault: department,
onEnableNotifications: this.handleEnableNotifications,
onDisableNotifications: this.handleDisableNotifications,
onMinimize: this.handleMinimize,
onRestore: this.handleRestore,
onOpenWindow: this.handleOpenWindow,
onDismissAlert: this.handleDismissAlert,
dismissNotification: this.dismissNotification,
};

return (
<Router history={history} onChange={this.handleRoute}>
<ChatConnector default path='/' {...screenProps} />
<ChatFinished path='/chat-finished' {...screenProps} />
<GDPRAgreement path='/gdpr' {...screenProps} />
{/* TODO: Find a better way to avoid prop drilling with that amout of props (perhaps create a screen context/provider) */}
<LeaveMessage path='/leave-message' screenProps={screenProps} />
<Register path='/register' screenProps={screenProps} />
<SwitchDepartment path='/switch-department' screenProps={screenProps} />
<TriggerMessage path='/trigger-messages' {...screenProps} />
</Router>
<ScreenProvider>
<Router history={history} onChange={this.handleRoute}>
<ChatConnector path='/' default />
<ChatFinished path='/chat-finished' />
<GDPRAgreement path='/gdpr' />
<LeaveMessage path='/leave-message' />
<Register path='/register' />
<SwitchDepartment path='/switch-department' />
<TriggerMessage path='/trigger-messages' />
</Router>
</ScreenProvider>
);
};
}
Expand Down
142 changes: 142 additions & 0 deletions packages/livechat/src/components/Screen/ScreenProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { FunctionalComponent } from 'preact';
import { createContext } from 'preact';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
import { parse } from 'query-string';

import { isActiveSession } from '../../helpers/isActiveSession';
import { parentCall } from '../../lib/parentCall';
import Triggers from '../../lib/triggers';
import store, { StoreContext } from '../../store';

export type ScreenContextValue = {
notificationsEnabled: boolean;
minimized: boolean;
expanded: boolean;
windowed: boolean;
sound: unknown;
alerts: unknown;
modal: unknown;
nameDefault: string;
emailDefault: string;
departmentDefault: string;
onEnableNotifications: () => unknown;
onDisableNotifications: () => unknown;
onMinimize: () => unknown;
onRestore: () => unknown;
onOpenWindow: () => unknown;
onDismissAlert: () => unknown;
dismissNotification: () => void;
theme?: {
color: string;
fontColor: string;
iconColor: string;
};
};

export const ScreenContext = createContext<ScreenContextValue>({
theme: {
color: '',
fontColor: '',
iconColor: '',
},
notificationsEnabled: true,
minimized: true,
windowed: false,
onEnableNotifications: () => undefined,
onDisableNotifications: () => undefined,
onMinimize: () => undefined,
onRestore: () => undefined,
onOpenWindow: () => undefined,
} as ScreenContextValue);

export const ScreenProvider: FunctionalComponent = ({ children }) => {
const { dispatch, config, sound, minimized = true, undocked, expanded = false, alerts, modal, iframe } = useContext(StoreContext);
const { department, name, email } = iframe.guest || {};
const { color } = config.theme || {};
const { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = iframe.theme || {};

const [poppedOut, setPopedOut] = useState(false);

const handleEnableNotifications = () => {
dispatch({ sound: { ...sound, enabled: true } });
};

const handleDisableNotifications = () => {
dispatch({ sound: { ...sound, enabled: false } });
};

const handleMinimize = () => {
parentCall('minimizeWindow');
dispatch({ minimized: true });
};

const handleRestore = () => {
parentCall('restoreWindow');
const dispatchRestore = () => dispatch({ minimized: false, undocked: false });

const dispatchEvent = () => {
dispatchRestore();
store.off('storageSynced', dispatchEvent);
};

if (undocked) {
store.on('storageSynced', dispatchEvent);
} else {
dispatchRestore();
}

Triggers.callbacks?.emit('chat-opened-by-visitor');
};

const handleOpenWindow = () => {
parentCall('openPopout');
dispatch({ undocked: true, minimized: false });
};

const handleDismissAlert = (id: string) => {
dispatch({ alerts: alerts.filter((alert) => alert.id !== id) });
};

const dismissNotification = () => !isActiveSession();

const checkPoppedOutWindow = useCallback(() => {
// Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose
const poppedOut = parse(window.location.search).mode === 'popout';
setPopedOut(poppedOut);

if (poppedOut) {
dispatch({ minimized: false });
}
}, [dispatch]);

useEffect(() => {
checkPoppedOutWindow();
}, [checkPoppedOutWindow]);

const screenProps = {
theme: {
color: customColor || color,
fontColor: customFontColor,
iconColor: customIconColor,
},
notificationsEnabled: sound?.enabled,
minimized: !poppedOut && (minimized || undocked),
expanded: !minimized && expanded,
windowed: !minimized && poppedOut,
sound,
alerts,
modal,
nameDefault: name,
emailDefault: email,
departmentDefault: department,
onEnableNotifications: handleEnableNotifications,
onDisableNotifications: handleDisableNotifications,
onMinimize: handleMinimize,
onRestore: handleRestore,
onOpenWindow: handleOpenWindow,
onDismissAlert: handleDismissAlert,
dismissNotification,
};

return <ScreenContext.Provider value={screenProps}>{children}</ScreenContext.Provider>;
};
Loading