chore: use new Livechat SDK Implementation (#29098)

Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com>
Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com>
pull/29318/head
Martin Schoeler 3 years ago committed by GitHub
parent cebe359d13
commit e006013e5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .changeset/early-turtles-report.md
  2. 1
      packages/api-client/src/RestClientInterface.ts
  3. 4
      packages/api-client/src/index.ts
  4. 5
      packages/ddp-client/__tests__/Connection.spec.ts
  5. 17
      packages/ddp-client/__tests__/DDPSDK.spec.ts
  6. 4
      packages/ddp-client/__tests__/helpers/index.ts
  7. 13
      packages/ddp-client/src/Connection.ts
  8. 5
      packages/ddp-client/src/DDPSDK.ts
  9. 1
      packages/ddp-client/src/index.ts
  10. 286
      packages/ddp-client/src/livechat/LivechatClientImpl.ts
  11. 71
      packages/ddp-client/src/livechat/types/LivechatSDK.ts
  12. 1
      packages/livechat/package.json
  13. 21
      packages/livechat/src/api.ts
  14. 7
      packages/livechat/src/components/Calls/CallIFrame.js
  15. 4
      packages/livechat/src/components/Calls/CallNotification.js
  16. 5
      packages/livechat/src/components/Calls/JoinCallButton.js
  17. 6
      packages/livechat/src/components/helpers.js
  18. 41
      packages/livechat/src/lib/connection.js
  19. 4
      packages/livechat/src/lib/customFields.js
  20. 2
      packages/livechat/src/lib/hooks.js
  21. 12
      packages/livechat/src/lib/room.js
  22. 27
      packages/livechat/src/lib/uiKit.js
  23. 10
      packages/livechat/src/routes/Chat/container.js
  24. 2
      packages/livechat/src/routes/Register/container.js
  25. 2
      packages/livechat/src/routes/SwitchDepartment/container.js
  26. 2
      packages/rest-typings/src/v1/omnichannel.ts
  27. 3
      yarn.lock

@ -0,0 +1,8 @@
---
"@rocket.chat/api-client": patch
"@rocket.chat/ddp-client": patch
"@rocket.chat/livechat": patch
"@rocket.chat/rest-typings": patch
---
chore: New Livechat SDK Implementation

@ -71,6 +71,7 @@ export interface RestClientInterface {
abort?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
error?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
},
options?: Omit<RequestInit, 'method'>,
): XMLHttpRequest;
getCredentials():

@ -267,7 +267,7 @@ export class RestClient implements RestClientInterface {
return data ? stringify(data, { arrayFormat: 'bracket' }) : '';
}
upload: RestClientInterface['upload'] = (endpoint, params, events) => {
upload: RestClientInterface['upload'] = (endpoint, params, events, options = {}) => {
if (!params) {
throw new Error('Missing params');
}
@ -283,7 +283,7 @@ export class RestClient implements RestClientInterface {
});
xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true);
Object.entries(this.getCredentialsAsHeaders()).forEach(([key, value]) => {
Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});

@ -6,12 +6,13 @@ import { handleConnection, handleConnectionAndRejects, handleMethod } from './he
let server: WS;
beforeEach(() => {
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
});
afterEach(() => {
server.close();
WS.clean();
jest.useRealTimers();
});
it('should connect', async () => {
@ -102,7 +103,7 @@ it('should handle reconnecting', async () => {
server.close();
WS.clean();
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
expect(connection.status).toBe('disconnected');

@ -23,7 +23,7 @@ const callXTimes = <F extends (...args: any) => any>(fn: F, times: number): F =>
};
beforeEach(async () => {
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
});
afterEach(() => {
@ -59,6 +59,7 @@ it('should handle a stream of messages', async () => {
expect(cb).toBeCalledTimes(3);
expect(cb).toHaveBeenNthCalledWith(1, 1);
sdk.connection.close();
});
it('should ignore messages other from changed', async () => {
@ -82,6 +83,7 @@ it('should ignore messages other from changed', async () => {
fireStreamRemove(server, streamName, streamParams);
expect(cb).toBeCalledTimes(0);
sdk.connection.close();
});
it('should handle streams after reconnect', async () => {
@ -115,7 +117,7 @@ it('should handle streams after reconnect', async () => {
server.close();
WS.clean();
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
@ -123,7 +125,7 @@ it('should handle streams after reconnect', async () => {
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
// the client should reconnect and resubscribe
await handleSubscription(server, result.id, streamName, streamParams);
await Promise.all([handleSubscription(server, result.id, streamName, streamParams), jest.advanceTimersByTimeAsync(1000)]);
fire(server, streamName, streamParams);
await jest.advanceTimersByTimeAsync(1000);
@ -131,6 +133,7 @@ it('should handle streams after reconnect', async () => {
expect(cb).toBeCalledTimes(6);
jest.useRealTimers();
sdk.connection.close();
});
it('should handle an unsubscribe stream after reconnect', async () => {
@ -166,7 +169,7 @@ it('should handle an unsubscribe stream after reconnect', async () => {
server.close();
WS.clean();
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
@ -192,6 +195,7 @@ it('should handle an unsubscribe stream after reconnect', async () => {
expect(sdk.client.subscriptions.size).toBe(0);
jest.useRealTimers();
sdk.connection.close();
sdk.connection.close();
});
it('should create and connect to a stream', async () => {
@ -215,7 +219,7 @@ describe('Method call and Disconnection cases', () => {
server.close();
WS.clean();
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
@ -249,7 +253,7 @@ describe('Method call and Disconnection cases', () => {
server.close();
WS.clean();
server = new WS('ws://localhost:1234');
server = new WS('ws://localhost:1234/websocket');
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
@ -264,5 +268,6 @@ describe('Method call and Disconnection cases', () => {
expect(result).toBe(1);
jest.useRealTimers();
sdk.connection.close();
});
});

@ -45,7 +45,9 @@ export const handleMethod = async (server: WS, method: string, params: string[],
export const handleSubscription = async (server: WS, id: string, streamName: string, streamParams: string) => {
await server.nextMessage.then(async (message) => {
await expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`);
await expect(message).toBe(
`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}",{"useCollection":false,"args":[null]}]}`,
);
server.send(`{"msg":"ready","subs":["${id}"]}`);
});
};

@ -34,6 +34,7 @@ export interface Connection
close: void;
}> {
url: string;
ssl: boolean;
session?: string;
@ -61,6 +62,10 @@ export class ConnectionImpl
}>
implements Connection
{
ssl: boolean;
url: string;
session?: string;
status: ConnectionStatus = 'idle';
@ -72,12 +77,14 @@ export class ConnectionImpl
public queue = new Set<string>();
constructor(
readonly url: string,
url: string,
private WS: WebSocketConstructor,
private client: DDPClient,
readonly retryOptions: RetryOptions = { retryCount: 0, retryTime: 1000 },
) {
super();
this.ssl = url.startsWith('https') || url.startsWith('wss');
this.url = url.replace(/^https?:\/\//, '').replace(/^wss?:\/\//, '');
this.client.onDispatchMessage((message: string) => {
if (this.ws && this.ws.readyState === this.ws.OPEN) {
@ -124,7 +131,7 @@ export class ConnectionImpl
this.emit('connecting');
this.emitStatus();
const ws = new this.WS(this.url);
const ws = new this.WS(`${this.ssl ? 'wss://' : 'ws://'}${this.url}/websocket`);
this.ws = ws;
@ -192,7 +199,7 @@ export class ConnectionImpl
this.retryOptions.retryTimer = setTimeout(() => {
this.reconnect();
}, this.retryOptions.retryTime);
}, this.retryOptions.retryTime * this.retryCount);
};
});
}

@ -36,8 +36,9 @@ export class DDPSDK implements SDK {
readonly rest: RestClient,
) {}
stream(name: string, key: unknown, cb: (...data: PublicationPayloads['fields']['args']) => void) {
const subscription = this.client.subscribe(`stream-${name}`, key);
stream(name: string, data: unknown, cb: (...data: PublicationPayloads['fields']['args']) => void) {
const [key, args] = Array.isArray(data) ? data : [data];
const subscription = this.client.subscribe(`stream-${name}`, key, { useCollection: false, args: [args] });
const stop = subscription.stop.bind(subscription);
const cancel = [

@ -1,2 +1,3 @@
export * from './DDPSDK';
export * from './legacy/RocketchatSDKLegacy';
export * from './livechat/LivechatClientImpl';

@ -1,10 +1,18 @@
import type { StreamNames, StreamKeys, StreamerCallbackArgs } from '@rocket.chat/ui-contexts/src/ServerContext/streams';
import type { ServerMethods, ServerMethodReturn } from '@rocket.chat/ui-contexts';
import { Emitter } from '@rocket.chat/emitter';
import { RestClient } from '@rocket.chat/api-client';
import type { IOmnichannelRoom, Serialized } from '@rocket.chat/core-typings';
import type { OperationParams, OperationResult } from '@rocket.chat/rest-typings';
import type { DDPDispatchOptions } from '../types/DDPClient';
import type { LivechatClient, LivechatRoomEvents } from './types/LivechatSDK';
import { DDPSDK } from '../DDPSDK';
import type { LivechatEndpoints, LivechatRoomEvents, LivechatStream } from './types/LivechatSDK';
import { DDPDispatcher } from '../DDPDispatcher';
import { ClientStreamImpl } from '../ClientStream';
import { ConnectionImpl } from '../Connection';
import { AccountImpl } from '../types/Account';
import { TimeoutControl } from '../TimeoutControl';
import type { ClientStream } from '../types/ClientStream';
declare module '../ClientStream' {
@ -25,13 +33,17 @@ declare module '../DDPSDK' {
interface DDPSDK {
stream<N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
key: K,
data: K | [K, unknown],
callback: (...args: StreamerCallbackArgs<N, K>) => void,
): ReturnType<ClientStream['subscribe']>;
}
}
export class LivechatClientImpl extends DDPSDK implements LivechatClient {
export class LivechatClientImpl extends DDPSDK implements LivechatStream, LivechatEndpoints {
private token?: string;
public readonly credentials: { token?: string } = { token: this.token };
private ev = new Emitter<{
typing: StreamerCallbackArgs<'notify-room', `${string}/typing`>;
message: StreamerCallbackArgs<'room-messages', string>;
@ -55,19 +67,19 @@ export class LivechatClientImpl extends DDPSDK implements LivechatClient {
}
onRoomMessage(rid: string, cb: (...args: StreamerCallbackArgs<'room-messages', string>) => void) {
return this.stream('room-messages', rid, cb).stop;
return this.stream('room-messages', [rid, { token: this.token, visitorToken: this.token }], cb).stop;
}
onRoomTyping(rid: string, cb: (...args: StreamerCallbackArgs<'notify-room', `${string}/typing`>) => void) {
return this.stream('notify-room', `${rid}/typing`, cb).stop;
return this.stream('notify-room', [`${rid}/typing`, { token: this.token, visitorToken: this.token }], cb).stop;
}
onRoomDeleteMessage(rid: string, cb: (...args: StreamerCallbackArgs<'notify-room', `${string}/deleteMessage`>) => void) {
return this.stream('notify-room', `${rid}/deleteMessage`, cb).stop;
return this.stream('notify-room', [`${rid}/deleteMessage`, { token: this.token, visitorToken: this.token }], cb).stop;
}
onAgentChange(rid: string, cb: (data: LivechatRoomEvents<'agentData'>) => void): () => void {
return this.stream('livechat-room', rid, (data) => {
return this.stream('livechat-room', [rid, { token: this.token, visitorToken: this.token }], (data) => {
if (data.type === 'agentData') {
cb(data.data);
}
@ -75,7 +87,7 @@ export class LivechatClientImpl extends DDPSDK implements LivechatClient {
}
onAgentStatusChange(rid: string, cb: (data: LivechatRoomEvents<'agentStatus'>) => void): () => void {
return this.stream('livechat-room', rid, (data) => {
return this.stream('livechat-room', [rid, { token: this.token, visitorToken: this.token }], (data) => {
if (data.type === 'agentStatus') {
cb(data.status);
}
@ -91,7 +103,7 @@ export class LivechatClientImpl extends DDPSDK implements LivechatClient {
}
onVisitorChange(rid: string, cb: (data: LivechatRoomEvents<'visitorData'>) => void): () => void {
return this.stream('livechat-room', rid, (data) => {
return this.stream('livechat-room', [rid, { token: this.token, visitorToken: this.token }], (data) => {
if (data.type === 'visitorData') {
cb(data.visitor);
}
@ -105,4 +117,260 @@ export class LivechatClientImpl extends DDPSDK implements LivechatClient {
notifyCallDeclined(rid: string) {
return this.client.callAsync('stream-notify-room', `${rid}/call`, 'decline');
}
// API GETTERS
async config(
params: OperationParams<'GET', '/v1/livechat/config'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/config'>['config']>> {
const { config } = await this.rest.get('/v1/livechat/config', params);
return config;
}
async room(params: OperationParams<'GET', '/v1/livechat/room'>): Promise<Serialized<IOmnichannelRoom>> {
if (!this.token) {
throw new Error('Invalid token');
}
const result = await this.rest.get('/v1/livechat/room', { ...params, token: this.token });
// TODO: On major version bump, normalize the return of /v1/livechat/room
function isRoomObject(
room: Serialized<IOmnichannelRoom> | { room: Serialized<IOmnichannelRoom> },
): room is { room: Serialized<IOmnichannelRoom> } {
return (room as { room: Serialized<IOmnichannelRoom> }).room !== undefined;
}
if (isRoomObject(result)) {
return result.room;
}
return result;
}
visitor(
params: OperationParams<'GET', '/v1/livechat/visitor'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/visitor'>['visitor']>> {
if (!this.token) {
throw new Error('Invalid token');
}
const endpoint = `/v1/livechat/visitor/${this.token}`;
return this.rest.get(endpoint as '/v1/livechat/visitor/:token', params);
}
nextAgent(
params: OperationParams<'GET', '/v1/livechat/agent.next/:token'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/agent.next/:token'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.get(`/v1/livechat/agent.next/${this.token}`, params);
}
async agent(rid: string): Promise<Serialized<OperationResult<'GET', '/v1/livechat/agent.info/:rid/:token'>['agent']>> {
if (!this.token) {
throw new Error('Invalid token');
}
const { agent } = await this.rest.get(`/v1/livechat/agent.info/${rid}/${this.token}`);
return agent;
}
message(
id: string,
params: OperationParams<'GET', '/v1/livechat/message/:_id'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/message/:_id'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.get(`/v1/livechat/message/${id}`, { ...params, token: this.token });
}
async loadMessages(
rid: string,
params: OperationParams<'GET', '/v1/livechat/messages.history/:rid'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/messages.history/:rid'>['messages']>> {
if (!this.token) {
throw new Error('Invalid token');
}
const { messages } = await this.rest.get(`/v1/livechat/messages.history/${rid}`, { ...params, token: this.token });
return messages;
}
// API POST
transferChat({
rid,
department,
}: {
rid: string;
department: string;
}): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.transfer'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/room.transfer', { rid, token: this.token, department });
}
async grantVisitor(
guest: OperationParams<'POST', '/v1/livechat/visitor'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor'>>> {
const result = await this.rest.post('/v1/livechat/visitor', guest);
this.token = result?.visitor.token;
return result;
}
login(guest: OperationParams<'POST', '/v1/livechat/visitor'>) {
return this.grantVisitor(guest);
}
closeChat({ rid }: { rid: string }): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.close'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/room.close', { rid, token: this.token });
}
chatSurvey(
params: OperationParams<'POST', '/v1/livechat/room.survey'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.survey'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/room.survey', { rid: params.rid, token: this.token, data: params.data });
}
updateCallStatus(
callStatus: string,
rid: string,
callId: string,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor.callStatus'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/visitor.callStatus', { token: this.token, callStatus, rid, callId });
}
sendMessage(
params: OperationParams<'POST', '/v1/livechat/message'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/message'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/message', { ...params, token: this.token });
}
sendOfflineMessage(
params: OperationParams<'POST', '/v1/livechat/offline.message'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/offline.message'>>> {
return this.rest.post('/v1/livechat/offline.message', params);
}
sendVisitorNavigation(
params: OperationParams<'POST', '/v1/livechat/page.visited'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/page.visited'>>> {
return this.rest.post('/v1/livechat/page.visited', params);
}
requestTranscript(email: string, { rid }: { rid: string }): Promise<Serialized<OperationResult<'POST', '/v1/livechat/transcript'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/transcript', { token: this.token, rid, email });
}
sendCustomField(
params: OperationParams<'POST', '/v1/livechat/custom.field'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/custom.field'>>> {
return this.rest.post('/v1/livechat/custom.field', params);
}
sendCustomFields(
params: OperationParams<'POST', '/v1/livechat/custom.fields'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/custom.fields'>>> {
return this.rest.post('/v1/livechat/custom.fields', params);
}
async updateVisitorStatus(newStatus: string): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor.status'>['status']>> {
if (!this.token) {
throw new Error('Invalid token');
}
const { status } = await this.rest.post('/v1/livechat/visitor.status', { token: this.token, status: newStatus });
return status;
}
uploadFile(rid: string, file: File): Promise<ProgressEvent<EventTarget>> {
if (!this.token) {
throw new Error('Invalid token');
}
if (!file) {
throw new Error('Invalid file');
}
return new Promise((resolve, reject) => {
if (!this.token) {
return reject(new Error('Invalid token'));
}
return this.rest.upload(
`/v1/livechat/upload/${rid}`,
{ file },
{
load: resolve,
error: reject,
},
{ headers: { 'x-visitor-token': this.token } },
);
});
}
// API DELETE
deleteMessage(id: string, { rid }: { rid: string }): Promise<Serialized<OperationResult<'DELETE', '/v1/livechat/message/:_id'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.delete(`/v1/livechat/message/${id}`, { rid, token: this.token });
}
deleteVisitor(): Promise<Serialized<OperationResult<'DELETE', '/v1/livechat/visitor'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.delete(`/v1/livechat/visitor/${this.token}` as any);
}
// API PUT
editMessage(
id: string,
params: OperationParams<'PUT', '/v1/livechat/message/:_id'>,
): Promise<Serialized<OperationResult<'PUT', '/v1/livechat/message/:_id'>>> {
return this.rest.put(`/v1/livechat/message/${id}`, params);
}
static create(url: string, retryOptions = { retryCount: 3, retryTime: 10000 }): LivechatClientImpl {
// TODO: Decide what to do with the EJSON objects
const ddp = new DDPDispatcher();
const connection = ConnectionImpl.create(url, WebSocket, ddp, retryOptions);
const stream = new ClientStreamImpl(ddp, ddp);
const account = new AccountImpl(stream);
const timeoutControl = TimeoutControl.create(ddp, connection);
const rest = new RestClient({ baseUrl: url.replace(/^ws/, 'http') });
const sdk = new LivechatClientImpl(connection, stream, account, timeoutControl, rest);
connection.on('connected', () => {
Object.entries(stream.subscriptions).forEach(([, sub]) => {
ddp.subscribeWithId(sub.id, sub.name, sub.params);
});
});
return sdk;
}
}

@ -1,3 +1,5 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type { OperationParams, OperationResult } from '@rocket.chat/rest-typings';
import type { StreamerCallbackArgs } from '@rocket.chat/ui-contexts/src/ServerContext/streams';
export type LivechatRoomEvents<T> = StreamerCallbackArgs<'livechat-room', `${string}`> extends [infer A]
@ -10,7 +12,7 @@ export type LivechatRoomEvents<T> = StreamerCallbackArgs<'livechat-room', `${str
: never
: never;
export interface LivechatClient {
export interface LivechatStream {
notifyVisitorTyping(rid: string, username: string, typing: boolean): Promise<unknown>;
notifyCallDeclined(rid: string): Promise<unknown>;
@ -27,3 +29,70 @@ export interface LivechatClient {
onQueuePositionChange(rid: string, cb: (args: LivechatRoomEvents<'queueData' | 'agentData'>) => void): () => void;
onVisitorChange(rid: string, cb: (data: LivechatRoomEvents<'visitorData'>) => void): () => void;
}
export interface LivechatEndpoints {
// GET
config(args: OperationParams<'GET', '/v1/livechat/config'>): Promise<Serialized<OperationResult<'GET', '/v1/livechat/config'>['config']>>;
room(args: OperationParams<'GET', '/v1/livechat/room'>): Promise<Serialized<OperationResult<'GET', '/v1/livechat/room'>>>;
visitor(
args: OperationParams<'GET', '/v1/livechat/visitor/:token'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/visitor/:token'>>>;
nextAgent(
args: OperationParams<'GET', '/v1/livechat/agent.next/:token'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/agent.next/:token'>>>;
agent(rid: string): Promise<Serialized<OperationResult<'GET', '/v1/livechat/agent.info/:rid/:token'>['agent']>>;
message(
id: string,
args: OperationParams<'GET', '/v1/livechat/message/:_id'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/message/:_id'>>>;
loadMessages(
rid: string,
args: OperationParams<'GET', '/v1/livechat/messages.history/:rid'>,
): Promise<Serialized<OperationResult<'GET', '/v1/livechat/messages.history/:rid'>['messages']>>;
// videoCall(args: OperationParams<'GET', '/v1/livechat/video.call/:token'>): Promise<void>;
// POST
transferChat(
args: OperationParams<'POST', '/v1/livechat/room.transfer'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.transfer'>>>;
grantVisitor(
guest: OperationParams<'POST', '/v1/livechat/visitor'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor'>>>;
login(guest: OperationParams<'POST', '/v1/livechat/visitor'>): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor'>>>;
closeChat(args: { rid: string }): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.close'>>>;
// shareScreen(args: OperationParams<'POST', '/v1/livechat/room.shareScreen'>): Promise<void>;
chatSurvey(
args: OperationParams<'POST', '/v1/livechat/room.survey'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.survey'>>>;
updateVisitorStatus(status: string): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor.status'>['status']>>;
updateCallStatus(
callStatus: string,
rid: string,
callId: string,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor.callStatus'>>>;
sendMessage(args: OperationParams<'POST', '/v1/livechat/message'>): Promise<Serialized<OperationResult<'POST', '/v1/livechat/message'>>>;
sendOfflineMessage(
args: OperationParams<'POST', '/v1/livechat/offline.message'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/offline.message'>>>;
sendVisitorNavigation(
args: OperationParams<'POST', '/v1/livechat/page.visited'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/page.visited'>>>;
requestTranscript(email: string, { rid }: { rid: string }): Promise<Serialized<OperationResult<'POST', '/v1/livechat/transcript'>>>;
sendCustomField(
params: OperationParams<'POST', '/v1/livechat/custom.field'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/custom.field'>>>;
sendCustomFields(
params: OperationParams<'POST', '/v1/livechat/custom.fields'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/custom.fields'>>>;
uploadFile(rid: string, file: File): Promise<ProgressEvent<EventTarget>>;
// DELETE
deleteMessage(id: string, { rid }: { rid: string }): Promise<Serialized<OperationResult<'DELETE', '/v1/livechat/message/:_id'>>>;
deleteVisitor(args: OperationParams<'DELETE', '/v1/livechat/visitor/:token'>): Promise<void>;
// PUT
editMessage(
id: string,
args: OperationParams<'PUT', '/v1/livechat/message/:_id'>,
): Promise<Serialized<OperationResult<'PUT', '/v1/livechat/message/:_id'>>>;
}

@ -26,6 +26,7 @@
"devDependencies": {
"@babel/eslint-parser": "^7.19.1",
"@babel/preset-env": "^7.20.2",
"@rocket.chat/ddp-client": "workspace:^",
"@rocket.chat/eslint-config": "workspace:^",
"@rocket.chat/fuselage-tokens": "next",
"@rocket.chat/logo": "next",

@ -1,8 +1,23 @@
import LivechatClient from '@rocket.chat/sdk/lib/clients/Livechat';
import { LivechatClientImpl } from '@rocket.chat/ddp-client';
import { parse } from 'query-string';
const host =
window.SERVER_URL || parse(window.location.search).serverUrl || (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null);
window.SERVER_URL ?? parse(window.location.search).serverUrl ?? (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null);
export const useSsl = Boolean((Array.isArray(host) ? host[0] : host)?.match(/^https:/));
export const Livechat = new LivechatClient({ host, protocol: 'ddp', useSsl });
export const Livechat = LivechatClientImpl.create(host.replace(/^http/, 'ws'));
Livechat.rest.use(async function (request, next) {
try {
return await next(...request);
} catch (error) {
if (error instanceof Response) {
const e = await error.json();
throw e;
}
throw error;
}
});
Livechat.connection.connect();

@ -1,15 +1,14 @@
import { Livechat } from '../../api';
import store from '../../store';
import { createClassName } from '../helpers';
import { createClassName, getConnectionBaseUrl } from '../helpers';
import { CallStatus } from './CallStatus';
import styles from './styles.scss';
export const CallIframe = () => {
const { token, room, incomingCallAlert, ongoingCall } = store.state;
const url = `${Livechat.client.host}/meet/${room._id}?token=${token}&layout=embedded`;
const url = `${getConnectionBaseUrl()}/meet/${room._id}?token=${token}&layout=embedded`;
window.handleIframeClose = () => store.setState({ incomingCallAlert: { ...incomingCallAlert, show: false } });
window.expandCall = () => {
window.open(`${Livechat.client.host}/meet/${room._id}?token=${token}`, room._id);
window.open(`${getConnectionBaseUrl()}/meet/${room._id}?token=${token}`, room._id);
return store.setState({
incomingCallAlert: { ...incomingCallAlert, show: false },
ongoingCall: {

@ -8,7 +8,7 @@ import constants from '../../lib/constants';
import store from '../../store';
import { Avatar } from '../Avatar';
import { Button } from '../Button';
import { createClassName, getAvatarUrl, isMobileDevice } from '../helpers';
import { createClassName, getAvatarUrl, getConnectionBaseUrl, isMobileDevice } from '../helpers';
import { CallStatus } from './CallStatus';
import styles from './styles.scss';
@ -17,7 +17,7 @@ const CallNotification = ({ callProvider, callerUsername, url, dispatch, time, r
const callInNewTab = async () => {
const { token } = store.state;
const url = `${Livechat.client.host}/meet/${rid}?token=${token}`;
const url = `${getConnectionBaseUrl()}/meet/${rid}?token=${token}`;
await dispatch({
ongoingCall: {
callStatus: CallStatus.IN_PROGRESS_DIFFERENT_TAB,

@ -1,11 +1,10 @@
import { withTranslation } from 'react-i18next';
import { Livechat } from '../../api';
import VideoIcon from '../../icons/video.svg';
import constants from '../../lib/constants';
import store from '../../store';
import { Button } from '../Button';
import { createClassName } from '../helpers';
import { createClassName, getConnectionBaseUrl } from '../helpers';
import { isCallOngoing } from './CallStatus';
import styles from './styles.scss';
@ -19,7 +18,7 @@ export const JoinCallButton = ({ t, ...props }) => {
break;
}
case constants.webRTCCallStartedMessageType: {
window.open(`${Livechat.client.host}/meet/${room._id}?token=${token}`, room._id);
window.open(`${getConnectionBaseUrl()}/meet/${room._id}?token=${token}`, room._id);
break;
}
}

@ -124,6 +124,8 @@ export function upsert(array = [], item, predicate, ranking) {
// like a click. Secure flag is required when SameSite is set to None
const getSecureCookieSettings = () => (useSsl ? 'SameSite=None; Secure;' : '');
export const getConnectionBaseUrl = () => `http${Livechat.connection.ssl ? 's' : ''}://${Livechat.connection.url}`;
export const setInitCookies = () => {
document.cookie = `rc_is_widget=t; path=/; ${getSecureCookieSettings()}`;
document.cookie = `rc_room_type=l; path=/; ${getSecureCookieSettings()}`;
@ -135,7 +137,7 @@ export const setCookies = (rid, token) => {
document.cookie = `rc_room_type=l; path=/; ${getSecureCookieSettings()}`;
};
export const getAvatarUrl = (username) => (username ? `${Livechat.client.host}/avatar/${username}` : null);
export const getAvatarUrl = (username) => (username ? `${getConnectionBaseUrl()}/avatar/${username}` : null);
export const msgTypesNotRendered = [
MESSAGE_VIDEO_CALL,
@ -152,7 +154,7 @@ export const msgTypesNotRendered = [
export const canRenderMessage = ({ t }) => !msgTypesNotRendered.includes(t);
export const getAttachmentUrl = (url) => new URL(url, Livechat.client.host).toString();
export const getAttachmentUrl = (url) => new URL(url, getConnectionBaseUrl()).toString();
export const sortArrayByColumn = (array, column, inverted) =>
array.sort((a, b) => {

@ -7,7 +7,6 @@ import { loadConfig } from './main';
import { loadMessages } from './room';
let self;
let timer;
let connectedListener;
let disconnectedListener;
let initiated = false;
@ -30,7 +29,7 @@ const Connection = {
this.clearListeners();
await loadConfig();
await import('../i18next');
await Livechat.connect();
// await Livechat.connection.connect();
this.addListeners();
this.clearAlerts();
} catch (e) {
@ -38,22 +37,22 @@ const Connection = {
}
},
reconnect() {
if (timer) {
return;
}
timer = setTimeout(async () => {
try {
clearTimeout(timer);
timer = false;
await this.connect();
await loadMessages();
} catch (e) {
console.error('Reconecting error: ', e);
this.reconnect();
}
}, 5000);
},
// reconnect() {
// if (timer) {
// return;
// }
// timer = setTimeout(async () => {
// try {
// clearTimeout(timer);
// timer = false;
// await this.connect();
// await loadMessages();
// } catch (e) {
// console.error('Reconecting error: ', e);
// this.reconnect();
// }
// }, 5000);
// },
async clearAlerts() {
const { alerts } = store.state;
@ -74,16 +73,16 @@ const Connection = {
async handleDisconnected() {
await self.clearAlerts();
await self.displayAlert({ id: livechatDisconnectedAlertId, children: i18next.t('livechat_is_not_connected'), error: true, timeout: 0 });
self.reconnect();
// self.reconnect();
},
addListeners() {
if (!connectedListener) {
connectedListener = Livechat.onStreamData('connected', this.handleConnected);
connectedListener = Livechat.connection.on('connected', this.handleConnected);
}
if (!disconnectedListener) {
disconnectedListener = Livechat.onStreamData('close', this.handleDisconnected);
disconnectedListener = Livechat.connection.on('disconnected', this.handleDisconnected);
}
},

@ -20,7 +20,7 @@ class CustomFields {
this._initiated = true;
const { token } = store.state;
Livechat.credentials.token = token;
Livechat.token = token;
store.on('change', this.handleStoreChange);
}
@ -63,7 +63,7 @@ class CustomFields {
return;
}
const { token } = Livechat.credentials;
const { token } = Livechat;
Livechat.sendCustomField({ token, key, value, overwrite });
}
}

@ -12,7 +12,7 @@ import Triggers from './triggers';
const createOrUpdateGuest = async (guest) => {
const { token } = guest;
token && (await store.setState({ token }));
const user = await Livechat.grantVisitor({ visitor: { ...guest } });
const { visitor: user } = await Livechat.grantVisitor({ visitor: { ...guest } });
store.setState({ user });
};

@ -17,8 +17,6 @@ import { handleTranscript } from './transcript';
const commands = new Commands();
export const closeChat = async ({ transcriptRequested } = {}) => {
Livechat.unsubscribeAll();
if (!transcriptRequested) {
await handleTranscript();
}
@ -124,8 +122,6 @@ export const initRoom = async () => {
return;
}
Livechat.unsubscribeAll();
const {
token,
agent,
@ -137,7 +133,7 @@ export const initRoom = async () => {
let roomAgent = agent;
if (!roomAgent) {
if (servedBy) {
roomAgent = await Livechat.agent({ rid });
roomAgent = await Livechat.agent(rid);
await store.setState({ agent: roomAgent, queueInfo: null });
parentCall('callback', ['assign-agent', normalizeAgent(roomAgent)]);
}
@ -202,9 +198,13 @@ Livechat.onTyping((username, isTyping) => {
}
});
Livechat.onMessage(async (message) => {
Livechat.onMessage(async (originalMessage) => {
let message = JSON.parse(JSON.stringify(originalMessage));
if (message.ts instanceof Date) {
message.ts = message.ts.toISOString();
} else {
message.ts = message.ts.$date ? new Date(message.ts.$date).toISOString() : new Date(message.ts).toISOString();
}
message = await normalizeMessage(message);

@ -103,23 +103,18 @@ export const triggerAction = async ({ appId, type, actionId, rid, mid, viewId, c
const triggerId = generateTriggerId(appId);
try {
const params = {
type,
actionId,
rid,
mid,
viewId,
container,
triggerId,
payload,
};
const result = await Promise.race([
fetch(`${Livechat.client.host}/api/${encodeURI(`apps/ui.interaction/${appId}`)}`, {
method: 'POST',
body: Livechat.client.getBody(params),
headers: Object.assign({ 'x-visitor-token': Livechat.credentials.token }, Livechat.client.getHeaders()),
}).then(Livechat.client.handle),
Livechat.rest.post(`/apps/ui.interaction/${appId}`, {
type,
actionId,
rid,
mid,
viewId,
container,
triggerId,
payload,
}),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(triggerId));

@ -57,7 +57,7 @@ class ChatContainer extends Component {
}
const visitor = { token, ...guest };
const newUser = await Livechat.grantVisitor({ visitor });
const { visitor: newUser } = await Livechat.grantVisitor({ visitor });
await dispatch({ user: newUser });
};
@ -79,9 +79,7 @@ class ChatContainer extends Component {
parentCall('callback', 'chat-started');
return newRoom;
} catch (error) {
const {
data: { error: reason },
} = error;
const reason = error ? error.error : '';
const alert = {
id: createToken(),
children: i18n.t('error_starting_a_new_conversation_reason', { reason }),
@ -132,7 +130,7 @@ class ChatContainer extends Component {
this.stopTypingDebounced.stop();
await Promise.all([this.stopTyping({ rid, username: user.username }), Livechat.sendMessage({ msg, token, rid })]);
} catch (error) {
const reason = error?.data?.error ?? error.message;
const reason = error?.error ?? error.message;
const alert = { id: createToken(), children: reason, error: true, timeout: 5000 };
await dispatch({ alerts: (alerts.push(alert), alerts) });
}
@ -143,7 +141,7 @@ class ChatContainer extends Component {
const { alerts, dispatch, i18n } = this.props;
try {
await Livechat.uploadFile({ rid, file });
await Livechat.uploadFile(rid, file);
} catch (error) {
const {
data: { reason, sizeAllowed },

@ -28,7 +28,7 @@ export class RegisterContainer extends Component {
await dispatch({ loading: true, department });
try {
const user = await Livechat.grantVisitor({ visitor: { ...fields, token } });
const { visitor: user } = await Livechat.grantVisitor({ visitor: { ...fields, token } });
await dispatch({ user });
parentCall('callback', ['pre-chat-form-submit', fields]);
this.registerCustomFields(customFields);

@ -28,7 +28,7 @@ class SwitchDepartmentContainer extends Component {
}
if (!room) {
const user = await Livechat.grantVisitor({ visitor: { department, token } });
const { visitor: user } = await Livechat.grantVisitor({ visitor: { department, token } });
await dispatch({ user, alerts: (alerts.push({ id: createToken(), children: t('department_switched'), success: true }), alerts) });
return route('/');
}

@ -3340,7 +3340,7 @@ export type OmnichannelEndpoints = {
GET: () => { settings: ISetting[] };
};
'/v1/livechat/upload/:rid': {
POST: () => IMessage & { newRoom: boolean; showConnecting: boolean };
POST: (params: { file: File }) => IMessage & { newRoom: boolean; showConnecting: boolean };
};
'/v1/livechat/inquiries.list': {
GET: (params: GETLivechatInquiriesListParams) => PaginatedResult<{ inquiries: ILivechatInquiryRecord[] }>;

@ -7985,7 +7985,7 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/ddp-client@workspace:packages/ddp-client":
"@rocket.chat/ddp-client@workspace:^, @rocket.chat/ddp-client@workspace:packages/ddp-client":
version: 0.0.0-use.local
resolution: "@rocket.chat/ddp-client@workspace:packages/ddp-client"
dependencies:
@ -8413,6 +8413,7 @@ __metadata:
dependencies:
"@babel/eslint-parser": ^7.19.1
"@babel/preset-env": ^7.20.2
"@rocket.chat/ddp-client": "workspace:^"
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/fuselage-tokens": next
"@rocket.chat/logo": next

Loading…
Cancel
Save