Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions packages/livechat/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,14 @@ export class App extends Component<AppProps, AppState> {
iframe: { visible },
config: { theme },
dispatch,
undocked,
} = this.props;

parentCall(minimized ? 'minimizeWindow' : 'restoreWindow');
parentCall(visible ? 'showWidget' : 'hideWidget');
parentCall('setWidgetPosition', theme.position || 'right');
if (!undocked) {
parentCall(minimized ? 'minimizeWindow' : 'restoreWindow');
parentCall(visible ? 'showWidget' : 'hideWidget');
parentCall('setWidgetPosition', theme.position || 'right');
}

visibility.addListener(this.handleVisibilityChange);

Expand Down
41 changes: 7 additions & 34 deletions packages/livechat/src/components/Screen/ScreenProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { FunctionalComponent } from 'preact';
import { createContext } from 'preact';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
import { useContext, useEffect, useState } from 'preact/hooks';
import { parse } from 'query-string';

import { isActiveSession } from '../../helpers/isActiveSession';
import { createOrUpdateGuest, evaluateChangesAndLoadConfigByFields } from '../../lib/hooks';
import { loadConfig } from '../../lib/main';
import { parentCall } from '../../lib/parentCall';
import { loadMessages } from '../../lib/room';
Expand Down Expand Up @@ -63,21 +62,11 @@ export const ScreenContext = createContext<ScreenContextValue>({
} as ScreenContextValue);

export const ScreenProvider: FunctionalComponent = ({ children }) => {
const {
dispatch,
config,
sound,
minimized = true,
undocked,
expanded = false,
alerts,
modal,
iframe,
...store
} = useContext(StoreContext);
const store = useContext(StoreContext);
const { token, dispatch, config, sound, minimized = true, undocked, expanded = false, alerts, modal, iframe, customFieldsQueue } = store;
const { department, name, email } = iframe.guest || {};
const { color, position: configPosition, background } = config.theme || {};
const { livechatLogo, hideWatermark = false, registrationForm } = config.settings || {};
const { livechatLogo, hideWatermark = false } = config.settings || {};

const {
color: customColor,
Expand Down Expand Up @@ -128,7 +117,7 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => {
};

const handleOpenWindow = () => {
parentCall('openPopout', store.token);
parentCall('openPopout', { token, iframe, customFieldsQueue });
dispatch({ undocked: true, minimized: false });
};

Expand All @@ -138,30 +127,14 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => {

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

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

if (poppedOut) {
dispatch({ minimized: false, undocked: true });
}

if (token && typeof token === 'string') {
if (registrationForm && !name && !email) {
dispatch({ token });
return;
}
await evaluateChangesAndLoadConfigByFields(async () => {
await createOrUpdateGuest({ token });
});
}
}, [dispatch, email, name, registrationForm]);

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

const screenProps = {
theme: {
Expand Down
27 changes: 22 additions & 5 deletions packages/livechat/src/lib/customFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class CustomFields {
reset() {
this._initiated = false;
this._started = false;
this._queue = {};
store.off('change', this.handleStoreChange);
}

Expand All @@ -48,18 +47,36 @@ class CustomFields {
CustomFields.instance.processCustomFields();
}

addToQueue(key, value, overwrite) {
const { customFieldsQueue } = store.state;
store.setState({
customFieldsQueue: {
...customFieldsQueue,
[key]: { value, overwrite },
},
});
}

getQueue() {
return store.state.customFieldsQueue;
}

clearQueue() {
store.setState({ customFieldsQueue: {} });
}

processCustomFields() {
Object.keys(this._queue).forEach((key) => {
const { value, overwrite } = this._queue[key];
const queue = this.getQueue();
Object.entries(queue).forEach(([key, { value, overwrite }]) => {
this.setCustomField(key, value, overwrite);
});

this._queue = {};
this.clearQueue();
}

setCustomField(key, value, overwrite = true) {
if (!this._started) {
this._queue[key] = { value, overwrite };
this.addToQueue(key, value, overwrite);
return;
}

Expand Down
16 changes: 16 additions & 0 deletions packages/livechat/src/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ const updateIframeData = (data: Partial<StoreState['iframe']>) => {
};

const api = {
syncState(data: Partial<StoreState>) {
if (!data || typeof data !== 'object') {
return;
}

void evaluateChangesAndLoadConfigByFields(async () => {
const { user } = store.state;

if (user && data.token && user.token !== data.token) {
await createOrUpdateGuest({ token: data.token });
}

store.setState(data);
});
},

pageVisited(info: { change: string; title: string; location: { href: string } }) {
const { token, room } = store.state;
const { _id: rid } = room || {};
Expand Down
11 changes: 10 additions & 1 deletion packages/livechat/src/lib/parentCall.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { VALID_CALLBACKS } from '../widget';

const getParentWindowTarget = () => {
if (window.opener && !window.opener.closed) {
return window.opener;
}

return window.parent;
};

export const parentCall = (method: string, ...args: any[]) => {
const data = {
src: 'rocketchat',
fn: method,
args,
};

const target = getParentWindowTarget();
// TODO: This lgtm ignoring deserves more attention urgently!
window.parent.postMessage(data, '*'); // lgtm [js/cross-window-information-leak]
target.postMessage(data, '*'); // lgtm [js/cross-window-information-leak]
};

export const runCallbackEventEmitter = (callbackName: string, data: unknown) =>
Expand Down
8 changes: 7 additions & 1 deletion packages/livechat/src/store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import type { ComponentChildren } from 'preact';
import { Component, createContext } from 'preact';
import { useContext } from 'preact/hooks';

import Store from './Store';
import type { CustomField } from '../components/Form/CustomFields';
import type { Agent } from '../definitions/agents';
import type { Department } from '../definitions/departments';
import type { TriggerMessage } from '../definitions/triggerMessage';
import { parentCall } from '../lib/parentCall';
import { createToken } from '../lib/random';
import Store from './Store';

export type LivechatHiddenSytemMessageType =
| 'uj' // User joined
Expand Down Expand Up @@ -123,6 +123,7 @@ export type StoreState = {
connecting?: boolean;
messageListPosition?: 'top' | 'bottom' | 'free';
renderedTriggers: TriggerMessage[];
customFieldsQueue: Record<string, { value: string; overwrite: boolean }>;
};

export const initialState = (): StoreState => ({
Expand Down Expand Up @@ -164,6 +165,7 @@ export const initialState = (): StoreState => ({
ongoingCall: null, // TODO: store call info like url, startTime, timeout, etc here
businessUnit: null,
renderedTriggers: [],
customFieldsQueue: {},
});

const dontPersist = [
Expand Down Expand Up @@ -191,6 +193,10 @@ window.addEventListener('load', () => {
});

window.addEventListener('visibilitychange', () => {
if (store.state.undocked) {
return;
}

!store.state.minimized && !store.state.triggered && parentCall('openWidget');
store.state.iframe.visible ? parentCall('showWidget') : parentCall('hideWidget');
});
Expand Down
78 changes: 52 additions & 26 deletions packages/livechat/src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import type { HooksWidgetAPI } from './lib/hooks';
import type { StoreState } from './store';

type InternalWidgetAPI = {
popup: Window | null;
ready: () => void;
minimizeWindow: () => void;
restoreWindow: () => void;
openPopout: (token?: string) => void;
openPopout: (state: StoreState) => void;
openWidget: () => void;
resizeWidget: (height: number) => void;
removeWidget: () => void;
Expand All @@ -23,10 +22,10 @@ type InternalWidgetAPI = {
setWidgetPosition: (position: 'left' | 'right') => void;
};

export type LivechatMessageEventData<ApiType extends Record<string, any>> = {
export type LivechatMessageEventData<ApiType extends Record<string, any>, Fn extends keyof ApiType = keyof ApiType> = {
src?: string;
fn: keyof ApiType;
args: Parameters<ApiType[keyof ApiType]>;
fn: Fn;
args: Parameters<ApiType[Fn]>;
};

type InitializeParams = {
Expand Down Expand Up @@ -61,6 +60,7 @@ let ready = false;
let smallScreen = false;
let scrollPosition: number;
let widgetHeight: number;
let popoutWindow: Window | null = null;

export const VALID_CALLBACKS = [
'chat-maximized',
Expand Down Expand Up @@ -103,6 +103,12 @@ function clearAllCallbacks() {
});
}

const formatMessage = (action: keyof HooksWidgetAPI, ...params: Parameters<HooksWidgetAPI[keyof HooksWidgetAPI]>) => ({
src: 'rocketchat',
fn: action,
args: params,
});

// hooks
function callHook(action: keyof HooksWidgetAPI, ...params: Parameters<HooksWidgetAPI[keyof HooksWidgetAPI]>) {
if (!ready) {
Expand All @@ -113,11 +119,7 @@ function callHook(action: keyof HooksWidgetAPI, ...params: Parameters<HooksWidge
throw new Error('Widget is not initialized');
}

const data = {
src: 'rocketchat',
fn: action,
args: params,
};
const data = formatMessage(action, ...params);

iframe.contentWindow?.postMessage(data, '*');
}
Expand Down Expand Up @@ -461,8 +463,6 @@ function initialize(initParams: Partial<InitializeParams>) {
}

const api: InternalWidgetAPI = {
popup: null,

openWidget,

resizeWidget,
Expand All @@ -475,27 +475,29 @@ const api: InternalWidgetAPI = {
minimizeWindow() {
closeWidget();
},

restoreWindow() {
if (api.popup && api.popup.closed !== true) {
api.popup.close();
api.popup = null;
if (popoutWindow && popoutWindow.closed !== true) {
popoutWindow.close();
popoutWindow = null;
}
openWidget();
},

openPopout(token = '') {
openPopout(state: Partial<StoreState>) {
closeWidget();

if (!config.url) {
throw new Error('Config.url is not set!');
}
const urlToken = token && `&token=${token}`;

api.popup = window.open(
`${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout${urlToken}`,
'livechat-popout',
`width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`,
);
const url = new URL(config.url);
url.searchParams.append('mode', 'popout');

listenForMessageOnce('ready', () => {
popoutWindow?.postMessage(formatMessage('syncState', state), '*');
});

popoutWindow = window.open(url, 'livechat-popout', `width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`);
},

removeWidget() {
Expand Down Expand Up @@ -628,16 +630,24 @@ const currentPage: { href: string | null; title: string | null } = {
title: null,
};

function onNewMessage(event: MessageEvent<LivechatMessageEventData<Omit<InternalWidgetAPI, 'popup'>>>) {
function isValidMessage(event: MessageEvent<LivechatMessageEventData<InternalWidgetAPI>>) {
if (event.source === event.target) {
return;
return false;
}

if (!event.data || typeof event.data !== 'object') {
return;
return false;
}

if (!event.data.src || event.data.src !== 'rocketchat') {
return false;
}

return true;
}

function onNewMessage(event: MessageEvent<LivechatMessageEventData<InternalWidgetAPI>>) {
if (!isValidMessage(event)) {
return;
}

Expand All @@ -652,6 +662,22 @@ function onNewMessage(event: MessageEvent<LivechatMessageEventData<Omit<Internal
api[fn](...args);
}

function listenForMessageOnce<K extends keyof InternalWidgetAPI>(
key: K,
callback: (data: LivechatMessageEventData<InternalWidgetAPI, K>) => void,
): void {
const listener = (event: MessageEvent<LivechatMessageEventData<InternalWidgetAPI, K>>) => {
if (!isValidMessage(event) || event.data.fn !== key) {
return;
}

callback(event.data);
window.removeEventListener('message', listener);
};

window.addEventListener('message', listener);
}

const attachMessageListener = () => {
window.addEventListener('message', onNewMessage, false);
};
Expand Down
Loading